fix(received-invoices): treat amount as gross (VAT-inclusive), back-calc VAT
The amount on a received invoice is the GROSS total the user pays (VAT included), and VAT is the portion contained within it — not a net base with VAT added on top. Switch the formula from amount*rate/100 to the VAT-inclusive amount*rate/(100+rate) via a shared, tested vatFromGross() helper used by both the create (manual upload + AI import share this endpoint) and edit paths. Rate 0 → no VAT. - AI extraction now asks Claude for the total INCLUDING VAT (was "základ bez DPH"), reinforced in the structured-output schema description. - Label the amount field "Částka s DPH" in the AI review card and the manual upload/edit forms so the gross convention is explicit. - Add unit tests with the user-confirmed figures (22 542,91 @ 21% → 3 912,41, base 18 630,50) plus other rates and the no-VAT case. Existing rows keep their stored (gross) amounts; their vat_amount recomputes on next edit. A one-shot backfill can correct historical vat_amount if wanted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,15 @@ const VALID_STATUSES = ["unpaid", "paid"] as const;
|
||||
function roundMoney(n: number): number {
|
||||
return Math.round(n * 100) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* VAT contained WITHIN a gross (VAT-inclusive) amount. `amount` is the total to
|
||||
* pay including tax, so the tax portion is gross * rate / (100 + rate) — e.g.
|
||||
* 22542.91 @ 21% → 3912.41 (base 18630.50). Returns 0 when there is no VAT.
|
||||
*/
|
||||
export function vatFromGross(gross: number, rate: number): number {
|
||||
return rate > 0 ? roundMoney((gross * rate) / (100 + rate)) : 0;
|
||||
}
|
||||
const ALLOWED_SORT_FIELDS = [
|
||||
"id",
|
||||
"supplier_name",
|
||||
@@ -284,11 +293,8 @@ export default async function receivedInvoicesRoutes(
|
||||
const meta = invoicesMeta[i] || {};
|
||||
const amount = Number(meta.amount ?? 0);
|
||||
const vatRate = Number(meta.vat_rate ?? 21);
|
||||
// Amount is net — VAT = amount * rate / 100
|
||||
const vatAmount =
|
||||
vatRate > 0
|
||||
? Math.round(((amount * vatRate) / 100) * 100) / 100
|
||||
: 0;
|
||||
// `amount` is the GROSS total (VAT included); VAT is the portion within it.
|
||||
const vatAmount = vatFromGross(amount, vatRate);
|
||||
|
||||
const issueDate = meta.issue_date
|
||||
? new Date(String(meta.issue_date))
|
||||
@@ -448,7 +454,7 @@ export default async function receivedInvoicesRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate vat_amount when amount or vat_rate changes (matching PHP)
|
||||
// Recalculate vat_amount when amount (gross) or vat_rate changes.
|
||||
const finalAmount =
|
||||
body.amount !== undefined
|
||||
? Number(body.amount)
|
||||
@@ -457,9 +463,8 @@ export default async function receivedInvoicesRoutes(
|
||||
body.vat_rate !== undefined
|
||||
? Number(body.vat_rate)
|
||||
: Number(existing.vat_rate);
|
||||
// Amount is net — VAT = amount * rate / 100
|
||||
const computedVat =
|
||||
finalVatRate > 0 ? roundMoney((finalAmount * finalVatRate) / 100) : 0;
|
||||
// `amount` is the GROSS total (VAT included); VAT is the portion within it.
|
||||
const computedVat = vatFromGross(finalAmount, finalVatRate);
|
||||
|
||||
// Auto-set paid_date when status transitions to paid (matching PHP)
|
||||
const newStatus =
|
||||
|
||||
Reference in New Issue
Block a user