Compare commits
2 Commits
cd9d7b165f
...
24c509c0d5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24c509c0d5 | ||
|
|
4185efb83a |
@@ -1,5 +1,14 @@
|
|||||||
import { buildCsv, buildPdf, buildReceivableRows, buildRows } from './cashbox-export.utils';
|
import { buildCsv, buildPdf, buildReceivableRows, buildRows } from './cashbox-export.utils';
|
||||||
|
|
||||||
|
// Reads the page count directly out of the raw PDF bytes instead of pulling in
|
||||||
|
// a parser dependency. Coupled to pdfkit's current /Pages dict serialization -
|
||||||
|
// a pdfkit upgrade that reorders/reflows it could require adjusting this regex.
|
||||||
|
function pdfPageCount(buffer: Buffer): number {
|
||||||
|
const match = buffer.toString('latin1').match(/\/Type\s*\/Pages[\s\S]{0,80}?\/Count\s+(\d+)/);
|
||||||
|
if (!match) throw new Error('Could not find page count in PDF buffer');
|
||||||
|
return Number(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
describe('buildRows', () => {
|
describe('buildRows', () => {
|
||||||
const team = (overrides: Partial<{ transactions: any[]; players: any[] }> = {}) => ({
|
const team = (overrides: Partial<{ transactions: any[]; players: any[] }> = {}) => ({
|
||||||
id: 5,
|
id: 5,
|
||||||
@@ -10,12 +19,15 @@ describe('buildRows', () => {
|
|||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
it('includes team-wallet credit and expense rows as "Teamkasse"', () => {
|
it('includes team-wallet credit rows as-is and negates expense rows so they reduce the budget', () => {
|
||||||
|
// DB stores TeamWalletTransaction.amount as a positive number even for
|
||||||
|
// expenses (see team-wallet-transaction.entity.ts setBalance()); buildRows
|
||||||
|
// must negate expenses itself so they subtract from the running total.
|
||||||
const rows = buildRows(
|
const rows = buildRows(
|
||||||
team({
|
team({
|
||||||
transactions: [
|
transactions: [
|
||||||
{ date: '2026-08-05T00:00:00.000Z', amount: 100, note: 'Sponsoring', type: { name: 'credit' } },
|
{ date: '2026-08-05T00:00:00.000Z', amount: 100, note: 'Sponsoring', type: { name: 'credit' } },
|
||||||
{ date: '2026-08-10T00:00:00.000Z', amount: -20, note: 'Bälle', type: { name: 'expense' } },
|
{ date: '2026-08-10T00:00:00.000Z', amount: 20, note: 'Bälle', type: { name: 'expense' } },
|
||||||
],
|
],
|
||||||
}) as any,
|
}) as any,
|
||||||
'2026-08-01',
|
'2026-08-01',
|
||||||
@@ -297,6 +309,19 @@ describe('buildPdf', () => {
|
|||||||
const buffer = await buildPdf({ name: 'Team A' } as any, [], [], '2026-08-01', '2026-08-31');
|
const buffer = await buildPdf({ name: 'Team A' } as any, [], [], '2026-08-01', '2026-08-31');
|
||||||
|
|
||||||
expect(buffer.subarray(0, 5).toString('utf-8')).toBe('%PDF-');
|
expect(buffer.subarray(0, 5).toString('utf-8')).toBe('%PDF-');
|
||||||
|
expect(pdfPageCount(buffer)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not append blank trailing pages when content fits on a single page', async () => {
|
||||||
|
const buffer = await buildPdf(
|
||||||
|
{ name: 'Team A' } as any,
|
||||||
|
[cashRow],
|
||||||
|
[receivableRow],
|
||||||
|
'2026-08-01',
|
||||||
|
'2026-08-31',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(pdfPageCount(buffer)).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('paginates correctly and stays a valid PDF for many rows', async () => {
|
it('paginates correctly and stays a valid PDF for many rows', async () => {
|
||||||
@@ -322,5 +347,8 @@ describe('buildPdf', () => {
|
|||||||
|
|
||||||
expect(buffer.subarray(0, 5).toString('utf-8')).toBe('%PDF-');
|
expect(buffer.subarray(0, 5).toString('utf-8')).toBe('%PDF-');
|
||||||
expect(buffer.length).toBeGreaterThan(2000);
|
expect(buffer.length).toBeGreaterThan(2000);
|
||||||
|
// Guards against the footer loop reintroducing blank trailing pages: with
|
||||||
|
// the bug, this dataset produced 12 pages (3x the real content pages).
|
||||||
|
expect(pdfPageCount(buffer)).toBe(4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ export function buildRows(team: Team, from: string, to: string): CashboxExportRo
|
|||||||
|
|
||||||
for (const transaction of team.transactions ?? []) {
|
for (const transaction of team.transactions ?? []) {
|
||||||
if (!transaction.type) continue;
|
if (!transaction.type) continue;
|
||||||
|
const amount = Number(transaction.amount);
|
||||||
raw.push({
|
raw.push({
|
||||||
date: transaction.date,
|
date: transaction.date,
|
||||||
type: transaction.type.name,
|
type: transaction.type.name,
|
||||||
who: 'Teamkasse',
|
who: 'Teamkasse',
|
||||||
note: transaction.note,
|
note: transaction.note,
|
||||||
amount: Number(transaction.amount),
|
amount: transaction.type.name === 'expense' ? -amount : amount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,12 +313,19 @@ function addFooters(doc: PDFKit.PDFDocument, teamName: string): void {
|
|||||||
for (let i = range.start; i < range.start + range.count; i++) {
|
for (let i = range.start; i < range.start + range.count; i++) {
|
||||||
doc.switchToPage(i);
|
doc.switchToPage(i);
|
||||||
const footerY = doc.page.height - 25;
|
const footerY = doc.page.height - 25;
|
||||||
|
// footerY sits inside the reserved bottom margin (below pdfkit's page
|
||||||
|
// maxY()). Without an explicit `height`, pdfkit's LineWrapper measures
|
||||||
|
// overflow against the full-page maxY() and calls addPage() here on every
|
||||||
|
// iteration - silently appending blank trailing pages. Bounding the text
|
||||||
|
// to its own small box (well over the 8pt single-line height needed)
|
||||||
|
// keeps the overflow check local and stops that auto-pagination.
|
||||||
doc
|
doc
|
||||||
.fontSize(8)
|
.fontSize(8)
|
||||||
.font('Helvetica')
|
.font('Helvetica')
|
||||||
.fillColor(COLORS.footerText)
|
.fillColor(COLORS.footerText)
|
||||||
.text(`${teamName} – Kassenbuch-Report, erstellt am ${generatedAt}`, PAGE_MARGIN, footerY, {
|
.text(`${teamName} – Kassenbuch-Report, erstellt am ${generatedAt}`, PAGE_MARGIN, footerY, {
|
||||||
width: doc.page.width - PAGE_MARGIN * 2 - 60,
|
width: doc.page.width - PAGE_MARGIN * 2 - 60,
|
||||||
|
height: 20,
|
||||||
lineBreak: false,
|
lineBreak: false,
|
||||||
});
|
});
|
||||||
doc
|
doc
|
||||||
@@ -325,6 +333,7 @@ function addFooters(doc: PDFKit.PDFDocument, teamName: string): void {
|
|||||||
.fillColor(COLORS.footerText)
|
.fillColor(COLORS.footerText)
|
||||||
.text(`Seite ${i - range.start + 1} von ${range.count}`, doc.page.width - PAGE_MARGIN - 60, footerY, {
|
.text(`Seite ${i - range.start + 1} von ${range.count}`, doc.page.width - PAGE_MARGIN - 60, footerY, {
|
||||||
width: 60,
|
width: 60,
|
||||||
|
height: 20,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
lineBreak: false,
|
lineBreak: false,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user