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]>
37 lines
1.5 KiB
TypeScript
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),
|
|
}));
|
|
}
|