fix: improve VAT rounding and add per-line VAT display support

Fix calculateVat/calculateTotal to use proper monetary rounding. Add
getVatSummaryFromItems helper for deriving VAT labels from mixed-rate items.
Update invoice preview, review, and detail pages to show per-rate VAT
breakdown instead of a single aggregated rate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Emil
2026-02-23 12:58:21 +01:00
co-authored by Claude Opus 4.6
parent 3626f14e45
commit 3c036059ab
6 changed files with 111 additions and 62 deletions
+1 -1
View File
@@ -440,7 +440,7 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
))
) : (
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Moms ({invoice.vat_rate ?? 25}%):</Text>
<Text style={styles.totalLabel}>Moms ({invoice.vat_rate ?? (vatByRate.size === 1 ? vatByRate.keys().next().value : 25)}%):</Text>
<Text style={styles.totalValue}>{formatCurrency(invoice.vat_amount, invoice.currency)}</Text>
</View>
)}
+32 -2
View File
@@ -118,14 +118,14 @@ export function getVatRules(
* Calculate VAT amount
*/
export function calculateVat(subtotal: number, vatRate: number): number {
return subtotal * (vatRate / 100)
return Math.round(subtotal * vatRate) / 100
}
/**
* Calculate total including VAT
*/
export function calculateTotal(subtotal: number, vatRate: number): number {
return subtotal + calculateVat(subtotal, vatRate)
return Math.round((subtotal + calculateVat(subtotal, vatRate)) * 100) / 100
}
/**
@@ -153,6 +153,36 @@ export function getVatTreatmentLabel(treatment: VatTreatment): string {
return labels[treatment]
}
/**
* Derive a display-friendly VAT summary from invoice line items.
*
* - If all items share a single rate → returns that rate's label and treatment
* - If items have mixed rates → returns "Blandade momssatser" with null rate/treatment
*/
export function getVatSummaryFromItems(
items: { vat_rate?: number | null }[]
): { label: string; treatment: VatTreatment | null; rate: number | null; isMixed: boolean } {
const rates = new Set(items.map((item) => item.vat_rate ?? 25))
if (rates.size === 1) {
const rate = rates.values().next().value!
const treatment = getVatTreatmentForRate(rate)
return {
label: getVatTreatmentLabel(treatment),
treatment,
rate,
isMixed: false,
}
}
return {
label: 'Blandade momssatser',
treatment: null,
rate: null,
isMixed: true,
}
}
/**
* Get moms ruta description
*/