fix: stop dropping VAT on MCP inbox-converted supplier invoices (#896)
gnubok_create_supplier_invoice_from_inbox sourced the invoice's header vat_amount from the OCR-extracted totals.vat field instead of summing the per-line vat_amount values. That header field is never reconciled with the line items, so per-line VAT customization (or a mis-extracted document total) could leave it at a stale or zero value. createSupplierInvoiceRegistrationEntry (and the cash/privately-paid variants) then gated the entire 2641 ingående moms posting on invoice.vat_amount > 0, so a stale header silently suppressed a correct per-line VAT split with no error. Confirmed via the ledger: this exact defect hit Glesys 623884, DNB 9664449205, and ComputerSalg 500265968 (all already manually corrected via storno+correction). Fix: derive vat_amount from summed lineItems in the MCP tool, and switch the three registration-entry gates from the header field to itemsHaveVat(items), so the engine itself can no longer be fooled by an unreconciled aggregate regardless of which caller populates it. Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
This commit is contained in:
co-authored by
Jakob Wennberg
parent
a1fad3193e
commit
31b244acf5
@@ -203,6 +203,41 @@ describe('gnubok_create_supplier_invoice_from_inbox: execute', () => {
|
||||
expect(result.preview.total).toBe(1250)
|
||||
})
|
||||
|
||||
it('derives vat_amount from summed line items, not the OCR totals block (regression: per-line VAT customization silently dropped VAT)', async () => {
|
||||
// Reported bug: totals.vat is an independent OCR-extracted field that was
|
||||
// never reconciled with lineItems. A stale/mis-extracted document total
|
||||
// (or a per-line VAT customization that didn't also fix up totals.vat)
|
||||
// used to flow straight through as the staged vat_amount, which becomes
|
||||
// the invoice header field that gates the WHOLE 2641 posting downstream
|
||||
// in createSupplierInvoiceRegistrationEntry. Deriving it from the items
|
||||
// instead keeps the header honest regardless of what the header block said.
|
||||
const staleTotals = {
|
||||
...baseExtracted,
|
||||
totals: { subtotal: 1000, vat: 0, total: 1000 }, // stale: doesn't match the line below
|
||||
lineItems: [
|
||||
{ description: 'Konsulttimmar', quantity: 10, unit_price: 100, line_total: 1000, vat_rate: 25, vat_amount: 250 },
|
||||
],
|
||||
}
|
||||
const supabase = makeMock({
|
||||
inbox: {
|
||||
id: 'inbox-10',
|
||||
status: 'received',
|
||||
extracted_data: staleTotals,
|
||||
matched_supplier_id: 'supplier-1',
|
||||
created_supplier_invoice_id: null,
|
||||
document_id: 'doc-10',
|
||||
},
|
||||
})
|
||||
const tool = tools.find((t) => t.name === 'gnubok_create_supplier_invoice_from_inbox')!
|
||||
const result = (await tool.execute(
|
||||
{ inbox_item_id: 'inbox-10', dry_run: true },
|
||||
'company-1', 'user-1', supabase,
|
||||
)) as { preview: { vat_amount: number } }
|
||||
|
||||
// Must reflect the item's real VAT (250), not the stale header total (0).
|
||||
expect(result.preview.vat_amount).toBe(250)
|
||||
})
|
||||
|
||||
it('falls through to org_number lookup when no matched supplier', async () => {
|
||||
const supabase = makeMock({
|
||||
inbox: {
|
||||
|
||||
@@ -7771,7 +7771,6 @@ export const tools: McpTool[] = [
|
||||
|
||||
const total = Number(totalsExt?.total) || 0
|
||||
const subtotal = Number(totalsExt?.subtotal) || 0
|
||||
const vatAmount = Number(totalsExt?.vat) || 0
|
||||
|
||||
// VAT treatment: explicit override wins, else heuristic from extracted data
|
||||
const vatTreatment = (args.vat_treatment_override as string | undefined)
|
||||
@@ -7831,6 +7830,16 @@ export const tools: McpTool[] = [
|
||||
}
|
||||
})
|
||||
|
||||
// Derive from the actual per-line VAT rather than trusting
|
||||
// totalsExt.vat: that header figure comes straight from OCR/agent-
|
||||
// supplied extracted_data and is never reconciled against lineItems.
|
||||
// Per-line VAT customization (edited rate/amount on a line without
|
||||
// also fixing up the document totals) used to leave this at a stale
|
||||
// or zero value, and createSupplierInvoiceRegistrationEntry gates the
|
||||
// whole 2641 posting on invoice.vat_amount > 0: a stale header meant
|
||||
// the correct per-line VAT was silently never booked.
|
||||
const vatAmount = lineItems.reduce((sum, li) => sum + li.vat_amount, 0)
|
||||
|
||||
const params = {
|
||||
inbox_item_id: inboxItemId,
|
||||
supplier_id: supplierId,
|
||||
|
||||
Reference in New Issue
Block a user