feat: Steuersatz je Kategorie und je Produkt, Rechnung weist mehrere Saetze aus
Ein einziger Satz fuer den ganzen Shop war fachlich falsch: der Steuersatz haengt in Deutschland an der Art der Ware, nicht am Betrieb. Honig ist ein Lebensmittel und ermaessigt besteuert (Anlage 2 zum UStG), eine Bienenwachskerze nicht. Der Satz wird jetzt in drei Stufen bestimmt, die erste gewinnt: 1. Produkt — neues Feld "Umsatzsteuersatz" unter Admin -> Produkte, fuer Ausnahmen, die sich nicht aus der Kategorie ergeben. 2. Kategorie — VAT_RATE_PERCENT_HONIG / _KERZEN / _PROPOLIS. 3. VAT_RATE_PERCENT als gemeinsamer Rueckfallwert. Die Kategorie-Stufe ist der Normalfall: aendern sich die gesetzlichen Saetze, genuegt eine Aenderung an der Konfiguration statt an jedem Produkt. Leer am Produkt heisst ausdruecklich "Satz der Kategorie" und NICHT 0 — 0 bedeutet steuerfrei nach § 19 UStG. Das Formularfeld hat dafuer einen eigenen Zweig im Validierungsschema, sonst haette z.coerce.number() aus dem leeren Feld eine 0 gemacht. Damit kann eine Rechnung mehrere Saetze tragen, sobald Honig und Kerze im selben Paket liegen. Sie weist deshalb je Satz eine eigene Netto- und Steuerzeile aus. Versand und Rabatt verteilen sich im Verhaeltnis der Warenwerte: der Versand ist umsatzsteuerlich eine Nebenleistung und teilt das Schicksal der Hauptleistung — gibt es mehrere, teilt er sich anteilig auf. Die Verteilung nutzt Hare-Niemeyer, damit dabei keine Cent verschwinden oder entstehen; zugesichert ist, dass die Summe aller Zeilen exakt den Rechnungsbetrag ergibt. Der Satz wandert von der Rechnung an die BESTELLPOSITION (OrderItem.vatRatePercent) und gehoert damit zum Snapshot wie der Preis: wird ein Produkt spaeter umkategorisiert, bleiben bestehende Rechnungen unveraendert. Invoice.vatRatePercent entfaellt wieder — ein einzelner Satz je Rechnung ist bei gemischten Saetzen nicht darstellbar. Die Spalte entstand in der vorigen Migration und wurde nie in Betrieb beschrieben. VAT_RATE_PERCENT_PROPOLIS steht auf 19, weil der volle Satz im Zweifel der sicherere ist — als Lebensmittel waeren 7 richtig, als Nahrungsergaenzungs- oder Kosmetikprodukt 19. Im README als zu pruefen vermerkt. Der Manifest-Test hat die neuen Variablen zunaechst als "liest niemand" gemeldet: lib/vat.ts bekommt die Umgebung als Parameter, damit sich die Saetze ohne globalen Zustand testen lassen, und greift auf die Kategorie-Saetze ueber einen berechneten Schluessel zu — beides kann eine Regex nicht sehen. Dafuer gibt es jetzt eine begruendete Ausnahmeliste. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
7c59283ede
commit
625b73a8c8
20 files changed
+518
-70
No files matched your search
@@ -6,7 +6,7 @@ import { getCurrentCustomer } from "@/lib/auth";
|
||||
import { getInvoiceForCustomer } from "@/lib/account-data";
|
||||
import { formatPrice, formatDate } from "@/lib/format";
|
||||
import { getCompanyInfo } from "@/lib/company-info";
|
||||
import { splitGross } from "@/lib/vat";
|
||||
import { invoiceVatBreakdown } from "@/lib/vat";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
|
||||
@@ -24,12 +24,11 @@ export default async function InvoicePage(props: PageProps<"/rechnung/[invoiceNu
|
||||
|
||||
const { order } = invoice;
|
||||
const company = await getCompanyInfo();
|
||||
// Nur einmal aufteilen statt je Zeile: derselbe Betrag zweimal zu zerlegen
|
||||
// koennte bei spaeteren Aenderungen auseinanderlaufen.
|
||||
const vat =
|
||||
invoice.vatRatePercent != null && invoice.vatRatePercent > 0
|
||||
? splitGross(invoice.totalCents, invoice.vatRatePercent)
|
||||
: null;
|
||||
// Aufstellung je Steuersatz — eine Bestellung kann Honig (ermaessigt) und
|
||||
// eine Bienenwachskerze (voll) zugleich enthalten. Dieselbe Funktion nutzt
|
||||
// das PDF, damit beide Darstellungen desselben Belegs uebereinstimmen.
|
||||
const vatLines = invoiceVatBreakdown(order);
|
||||
const isSmallBusiness = vatLines.length > 0 && vatLines.every((line) => line.ratePercent === 0);
|
||||
|
||||
return (
|
||||
<div className="py-16 print:py-0">
|
||||
@@ -109,25 +108,27 @@ export default async function InvoicePage(props: PageProps<"/rechnung/[invoiceNu
|
||||
{/* Steuerausweis mit demselben an der Rechnung festgeschriebenen
|
||||
Satz wie im PDF — beide Darstellungen desselben Belegs muessen
|
||||
uebereinstimmen. */}
|
||||
{vat ? (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink/70">{t.invoicePage.netLabel}</span>
|
||||
<span>{formatPrice(vat.netCents, order.currency)}</span>
|
||||
{vatLines
|
||||
.filter((line) => line.ratePercent > 0)
|
||||
.map((line) => (
|
||||
<div key={line.ratePercent}>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink/70">{t.invoicePage.netLabel}</span>
|
||||
<span>{formatPrice(line.netCents, order.currency)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink/70">{t.invoicePage.vatLabel(line.ratePercent)}</span>
|
||||
<span>{formatPrice(line.vatCents, order.currency)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink/70">{t.invoicePage.vatLabel(invoice.vatRatePercent ?? 0)}</span>
|
||||
<span>{formatPrice(vat.vatCents, order.currency)}</span>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
))}
|
||||
<div className="flex justify-between text-forest pt-2 border-t border-ink/10">
|
||||
<span>{t.invoicePage.totalLabel}</span>
|
||||
<span>{formatPrice(invoice.totalCents, order.currency)}</span>
|
||||
</div>
|
||||
{/* Pflichtangabe bei Kleinunternehmerregelung — steht anstelle der
|
||||
Steuerzeilen, nicht zusaetzlich. */}
|
||||
{invoice.vatRatePercent === 0 ? (
|
||||
{isSmallBusiness ? (
|
||||
<p className="pt-2 text-xs text-ink/65">{t.invoicePage.smallBusinessNote}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,6 @@ export async function GET(
|
||||
invoiceNumber: invoice.invoiceNumber,
|
||||
issuedAt: invoice.issuedAt,
|
||||
totalCents: invoice.totalCents,
|
||||
vatRatePercent: invoice.vatRatePercent,
|
||||
order: invoice.order,
|
||||
}));
|
||||
|
||||
|
||||
Reference in new issue
Block a user