feat(mcp): add per-line account overrides to create_supplier_invoice_from_inbox (#805)
Adds a line_overrides param (array of {line_number, account_number}) so agents
can override the BAS account on individual lines rather than every line
inheriting the supplier default. Priority chain: line_overrides → extracted
accountSuggestion → supplier default → 4000.
Signed-off-by: Jonas Flodén <jonas@floden.nu>
This commit is contained in:
@@ -175,6 +175,29 @@ describe('gnubok_create_supplier_invoice_from_inbox — execute', () => {
|
|||||||
expect(result.preview.supplier_resolution).toBe('lookup_org_number')
|
expect(result.preview.supplier_resolution).toBe('lookup_org_number')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('falls through to name lookup when org_number returns null', async () => {
|
||||||
|
const supabase = makeMock({
|
||||||
|
inbox: {
|
||||||
|
id: 'inbox-7',
|
||||||
|
status: 'received',
|
||||||
|
extracted_data: baseExtracted,
|
||||||
|
matched_supplier_id: null,
|
||||||
|
created_supplier_invoice_id: null,
|
||||||
|
document_id: 'doc-7',
|
||||||
|
},
|
||||||
|
supplierByOrg: null,
|
||||||
|
supplierByName: { id: 'supplier-name-lookup' },
|
||||||
|
})
|
||||||
|
const tool = tools.find((t) => t.name === 'gnubok_create_supplier_invoice_from_inbox')!
|
||||||
|
const result = (await tool.execute(
|
||||||
|
{ inbox_item_id: 'inbox-7', dry_run: true },
|
||||||
|
'company-1', 'user-1', supabase,
|
||||||
|
)) as { preview: { supplier_resolution: string; supplier_id: string } }
|
||||||
|
|
||||||
|
expect(result.preview.supplier_id).toBe('supplier-name-lookup')
|
||||||
|
expect(result.preview.supplier_resolution).toBe('lookup_name')
|
||||||
|
})
|
||||||
|
|
||||||
it('throws when inbox item already converted', async () => {
|
it('throws when inbox item already converted', async () => {
|
||||||
const supabase = makeMock({
|
const supabase = makeMock({
|
||||||
inbox: {
|
inbox: {
|
||||||
@@ -211,6 +234,39 @@ describe('gnubok_create_supplier_invoice_from_inbox — execute', () => {
|
|||||||
).rejects.toThrow(/Cannot resolve supplier/)
|
).rejects.toThrow(/Cannot resolve supplier/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('applies line_overrides — overridden account wins over extracted accountSuggestion', async () => {
|
||||||
|
const extractedWithSuggestion = {
|
||||||
|
...baseExtracted,
|
||||||
|
lineItems: [
|
||||||
|
{ description: 'Line A', quantity: 1, unit_price: 400, line_total: 400, vat_rate: 25, vat_amount: 100, accountSuggestion: '6550' },
|
||||||
|
{ description: 'Line B', quantity: 1, unit_price: 600, line_total: 600, vat_rate: 25, vat_amount: 150, accountSuggestion: '6550' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const supabase = makeMock({
|
||||||
|
inbox: {
|
||||||
|
id: 'inbox-6',
|
||||||
|
status: 'received',
|
||||||
|
extracted_data: extractedWithSuggestion,
|
||||||
|
matched_supplier_id: 'supplier-1',
|
||||||
|
created_supplier_invoice_id: null,
|
||||||
|
document_id: 'doc-6',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const tool = tools.find((t) => t.name === 'gnubok_create_supplier_invoice_from_inbox')!
|
||||||
|
const result = (await tool.execute(
|
||||||
|
{
|
||||||
|
inbox_item_id: 'inbox-6',
|
||||||
|
dry_run: true,
|
||||||
|
line_overrides: [{ line_number: 2, account_number: '6420' }],
|
||||||
|
},
|
||||||
|
'company-1', 'user-1', supabase,
|
||||||
|
)) as { preview: { items_preview: Array<{ line_number: number; account_number: string }> } }
|
||||||
|
|
||||||
|
const items = result.preview.items_preview
|
||||||
|
expect(items[0].account_number).toBe('6550') // untouched — extracted suggestion wins
|
||||||
|
expect(items[1].account_number).toBe('6420') // override wins over extracted suggestion
|
||||||
|
})
|
||||||
|
|
||||||
it('throws when extracted_data is missing', async () => {
|
it('throws when extracted_data is missing', async () => {
|
||||||
const supabase = makeMock({
|
const supabase = makeMock({
|
||||||
inbox: {
|
inbox: {
|
||||||
|
|||||||
@@ -6288,6 +6288,19 @@ export const tools: McpTool[] = [
|
|||||||
supplier_id_override: { type: 'string', description: 'Force this supplier UUID instead of the matched/extracted one' },
|
supplier_id_override: { type: 'string', description: 'Force this supplier UUID instead of the matched/extracted one' },
|
||||||
vat_treatment_override: { type: 'string', enum: ['standard_25', 'reduced_12', 'reduced_6', 'reverse_charge', 'export', 'exempt'], description: 'Override extracted VAT treatment' },
|
vat_treatment_override: { type: 'string', enum: ['standard_25', 'reduced_12', 'reduced_6', 'reverse_charge', 'export', 'exempt'], description: 'Override extracted VAT treatment' },
|
||||||
due_date_override: { type: 'string', description: 'Override extracted due date (YYYY-MM-DD)' },
|
due_date_override: { type: 'string', description: 'Override extracted due date (YYYY-MM-DD)' },
|
||||||
|
line_overrides: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Per-line account overrides (1-based line_number). Wins over accountSuggestion and supplier default.',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
line_number: { type: 'number', description: '1-based index matching items_preview' },
|
||||||
|
account_number: { type: 'string', description: 'BAS account number for this line (e.g. "6420")' },
|
||||||
|
},
|
||||||
|
required: ['line_number', 'account_number'],
|
||||||
|
},
|
||||||
|
},
|
||||||
notes: { type: 'string', description: 'Optional notes appended to the supplier invoice' },
|
notes: { type: 'string', description: 'Optional notes appended to the supplier invoice' },
|
||||||
dry_run: { type: 'boolean', description: 'If true, return the assembled payload without staging (default false)' },
|
dry_run: { type: 'boolean', description: 'If true, return the assembled payload without staging (default false)' },
|
||||||
idempotency_key: { type: 'string', description: 'UUID. Repeat calls with same key + payload return cached response.' },
|
idempotency_key: { type: 'string', description: 'UUID. Repeat calls with same key + payload return cached response.' },
|
||||||
@@ -6407,19 +6420,26 @@ export const tools: McpTool[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build a lookup for per-line account overrides keyed by 1-based line number.
|
||||||
|
const rawLineOverrides = (args.line_overrides as Array<{ line_number: number; account_number: string }> | undefined) ?? []
|
||||||
|
const lineOverrideMap = new Map(rawLineOverrides.map((o) => [o.line_number, o.account_number]))
|
||||||
|
|
||||||
// Translate extracted line items into the supplier_invoice_items shape.
|
// Translate extracted line items into the supplier_invoice_items shape.
|
||||||
// Priority: per-line accountSuggestion → supplier.default_expense_account → 4000.
|
// Priority: line_overrides → per-line accountSuggestion → supplier.default_expense_account → 4000.
|
||||||
const lineItems = lineItemsExt.map((li, idx) => ({
|
const lineItems = lineItemsExt.map((li, idx) => {
|
||||||
line_number: idx + 1,
|
const lineNumber = idx + 1
|
||||||
description: (li.description as string) ?? `Position ${idx + 1}`,
|
return {
|
||||||
quantity: Number(li.quantity) || 1,
|
line_number: lineNumber,
|
||||||
unit: (li.unit as string) ?? 'st',
|
description: (li.description as string) ?? `Position ${lineNumber}`,
|
||||||
unit_price: Number(li.unit_price ?? li.unitPrice ?? li.amount) || 0,
|
quantity: Number(li.quantity) || 1,
|
||||||
line_total: Number(li.line_total ?? li.lineTotal ?? li.amount) || 0,
|
unit: (li.unit as string) ?? 'st',
|
||||||
account_number: (li.accountSuggestion as string | null) ?? supplierDefaultExpenseAccount ?? '4000',
|
unit_price: Number(li.unit_price ?? li.unitPrice ?? li.amount) || 0,
|
||||||
vat_rate: Number(li.vat_rate ?? li.vatRate) || 0,
|
line_total: Number(li.line_total ?? li.lineTotal ?? li.amount) || 0,
|
||||||
vat_amount: Number(li.vat_amount ?? li.vatAmount) || 0,
|
account_number: lineOverrideMap.get(lineNumber) ?? (li.accountSuggestion as string | null) ?? supplierDefaultExpenseAccount ?? '4000',
|
||||||
}))
|
vat_rate: Number(li.vat_rate ?? li.vatRate) || 0,
|
||||||
|
vat_amount: Number(li.vat_amount ?? li.vatAmount) || 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
inbox_item_id: inboxItemId,
|
inbox_item_id: inboxItemId,
|
||||||
|
|||||||
Reference in New Issue
Block a user