Files
Wabenhain/src/lib/invoice-groups.ts
T
Nick SzaunigandClaude Opus 5.5 77bdd9958c feat(rechnung): branded invoice with VAT ID and per-rate grouping
The invoice PDF and the invoice page now share one branded layout:
logo (uploaded brand logo, otherwise the drawn honeycomb mark) and
wordmark in the header, honey accents, a forest-green table header,
a meta box with invoice number, date, order number and VAT ID, and a
footer with address, contact, VAT ID and page numbers on every page.

A mixed cart (e.g. honey at 7 % and a beeswax candle at 19 %) lists
its items in one block per VAT rate with a subtotal each. Next to the
totals, a VAT table shows net, VAT and gross per rate plus a sum row.
Long invoices continue on further pages with the table header repeated.

The VAT identification number is a new company field
(company.vatId), editable in Admin > Inhalte, defaulting to the
number already stated in the imprint.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-01 00:49:59 +02:00

37 lines
1.5 KiB
TypeScript

export interface InvoiceItemGroup<T> {
/** Steuersatz der Gruppe; null bei Positionen aus der Zeit vor der Steuerausweisung. */
ratePercent: number | null;
items: T[];
/** Summe der Positionen dieser Gruppe (brutto, wie an den Positionen gespeichert). */
totalCents: number;
}
/**
* Teilt die Positionen einer Rechnung nach Steuersatz auf, damit ein
* gemischter Korb (Honig 7 %, Bienenwachskerze 19 %) auf der Rechnung in
* getrennten Bloecken steht — jede Position laesst sich so auf einen Blick
* ihrem Satz zuordnen, und die Steueraufstellung darunter (lib/vat.ts,
* invoiceVatBreakdown) hat ein sichtbares Gegenstueck.
*
* Gruppen aufsteigend nach Satz, Positionen innerhalb einer Gruppe in ihrer
* urspruenglichen Reihenfolge. Positionen ohne Satz bilden eine eigene
* Gruppe am Ende, statt einem Satz zugeschlagen zu werden, den sie nie hatten.
* Rechnungsansicht und PDF nutzen dieselbe Funktion.
*/
export function groupInvoiceItemsByVatRate<T extends { totalCents: number; vatRatePercent: number | null }>(
items: readonly T[]
): InvoiceItemGroup<T>[] {
const byRate = new Map<number | null, T[]>();
for (const item of items) {
byRate.set(item.vatRatePercent, [...(byRate.get(item.vatRatePercent) ?? []), item]);
}
return [...byRate.entries()]
.sort(([a], [b]) => (a ?? Number.POSITIVE_INFINITY) - (b ?? Number.POSITIVE_INFINITY))
.map(([ratePercent, groupItems]) => ({
ratePercent,
items: groupItems,
totalCents: groupItems.reduce((sum, item) => sum + item.totalCents, 0),
}));
}