fix(invoices): honour defer_invoice_booking on MCP, REST v1 and inbox convert (#1921)
The #967 "Registrera men bokför inte" setting was only respected by the dashboard routes. Six other paths decided whether to post the issue-time verifikat with `accounting_method === 'accrual'` alone, so a company that had switched booking to the explicit Bokför step still got vouchers posted at issue through MCP, the REST v1 API and the invoice-inbox convert route: - lib/pending-operations/commit.ts: send_invoice, mark_invoice_sent, create_supplier_invoice_from_inbox executors - app/api/v1/.../invoices/[id]/send and mark-sent (commit + dry-run preview) - app/api/v1/.../supplier-invoices POST - extensions/general/invoice-inbox convert All of them now call booksInvoicesOnIssue() from lib/bookkeeping/booking-mode, the helper the dashboard already uses, and select defer_invoice_booking where the settings projection did not include it. Behaviour for accrual companies without the flag and for kontantmetoden companies is unchanged. Tests: one deferred-company case per door (8 new), verified to fail without the fix. skills/accounted-api regenerated for the changed v1 descriptions. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
8ddc77fdfd
commit
f08fc2c274
@@ -794,3 +794,44 @@ describe('commitPendingOperation: create_supplier_invoice_from_inbox: dimensions
|
||||
expect(captured.items![0]).toMatchObject({ dimensions: {} })
|
||||
})
|
||||
})
|
||||
|
||||
describe('commitPendingOperation: create_supplier_invoice_from_inbox honours defer_invoice_booking (#967)', () => {
|
||||
it('registers WITHOUT the registration JE when the company defers invoice booking', async () => {
|
||||
vi.mocked(createSupplierInvoiceRegistrationEntry).mockClear()
|
||||
vi.mocked(linkToJournalEntry).mockClear()
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueue({ data: { id: 'op-1' }, error: null })
|
||||
enqueue({
|
||||
data: { id: 'inbox-1', created_supplier_invoice_id: null, status: 'ready' },
|
||||
error: null,
|
||||
})
|
||||
enqueue({
|
||||
data: { id: 'supplier-1', name: 'Acme AB', supplier_type: 'swedish_business' },
|
||||
error: null,
|
||||
})
|
||||
enqueue({ data: 42, error: null }) // arrival number
|
||||
enqueue({
|
||||
data: makeSupplierInvoice({ id: 'inv-deferred', supplier_invoice_number: 'INV-100' }),
|
||||
error: null,
|
||||
}) // invoice insert
|
||||
enqueue({ data: null, error: null }) // items insert
|
||||
enqueue({ data: { accounting_method: 'accrual', defer_invoice_booking: true }, error: null }) // company_settings
|
||||
enqueue({ data: null, error: null }) // invoice_inbox_items update
|
||||
enqueue({ data: null, error: null }) // dispatcher's commit update
|
||||
|
||||
const result = await commitPendingOperation(
|
||||
supabase as never,
|
||||
'user-1',
|
||||
'company-1',
|
||||
makePendingOp(),
|
||||
)
|
||||
|
||||
expect(result.status).toBe('committed')
|
||||
expect(result.data).toMatchObject({
|
||||
supplier_invoice_id: 'inv-deferred',
|
||||
registration_journal_entry_id: null,
|
||||
})
|
||||
expect(createSupplierInvoiceRegistrationEntry).not.toHaveBeenCalled()
|
||||
expect(linkToJournalEntry).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1727,3 +1727,46 @@ describe('commitPendingOperation: categorize_transaction account_override', () =
|
||||
expect(opts.accountOverride).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('commitPendingOperation: mark_invoice_sent honours defer_invoice_booking (#967)', () => {
|
||||
it('marks the invoice sent WITHOUT booking when the company defers invoice booking', async () => {
|
||||
const invoiceEntries = await import('@/lib/bookkeeping/invoice-entries')
|
||||
const bookSpy = vi.spyOn(invoiceEntries, 'createInvoiceJournalEntry')
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim
|
||||
enqueue({
|
||||
data: makeInvoice({
|
||||
id: 'invoice-1',
|
||||
status: 'draft',
|
||||
invoice_number: 'F-2026001',
|
||||
credited_invoice_id: null,
|
||||
}),
|
||||
error: null,
|
||||
})
|
||||
enqueue({
|
||||
data: {
|
||||
accounting_method: 'accrual',
|
||||
defer_invoice_booking: true,
|
||||
entity_type: 'enskild_firma',
|
||||
bankgiro: '123-4567',
|
||||
},
|
||||
error: null,
|
||||
})
|
||||
enqueue({ data: null, error: null }) // status update
|
||||
enqueue({ data: null, error: null }) // dispatcher update
|
||||
|
||||
const op = makePendingOp({
|
||||
operation_type: 'mark_invoice_sent',
|
||||
params: { invoice_id: 'invoice-1' },
|
||||
})
|
||||
|
||||
const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op)
|
||||
|
||||
expect(result.status).toBe('committed')
|
||||
expect(result.data).toMatchObject({ status: 'sent', journal_entry_id: null })
|
||||
// Same gate as the dashboard: deferred companies book via the explicit
|
||||
// Bokför step, never at mark-sent.
|
||||
expect(bookSpy).not.toHaveBeenCalled()
|
||||
bookSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -51,7 +51,7 @@ import {
|
||||
import { resolveSettlementAccount } from '@/lib/bookkeeping/settlement-account'
|
||||
import { buildInvoicePaymentClearingLines } from '@/lib/bookkeeping/invoice-payment-lines'
|
||||
import { resolveSekAmount } from '@/lib/bookkeeping/currency-utils'
|
||||
import { cashPartialBlockReason, supplierCreditNoteNeedsJournalEntry } from '@/lib/bookkeeping/booking-mode'
|
||||
import { booksInvoicesOnIssue, cashPartialBlockReason, supplierCreditNoteNeedsJournalEntry } from '@/lib/bookkeeping/booking-mode'
|
||||
import { ensureManualCashAccount } from '@/lib/cash-accounts/service'
|
||||
import { createJournalEntry, findFiscalPeriod, getSwedishLocalDate, reverseEntry, validateBalance } from '@/lib/bookkeeping/engine'
|
||||
import {
|
||||
@@ -2554,7 +2554,9 @@ async function commitSendInvoice(
|
||||
|
||||
const isRealInvoice = !invoice.document_type || invoice.document_type === 'invoice'
|
||||
let createdJournalEntryId: string | undefined
|
||||
if (isRealInvoice && (company.accounting_method === 'accrual' || !company.accounting_method)) {
|
||||
// #967: kontantmetoden and defer_invoice_booking companies send WITHOUT
|
||||
// booking; the verifikat comes at payment or via the explicit Bokför step.
|
||||
if (isRealInvoice && booksInvoicesOnIssue(company)) {
|
||||
try {
|
||||
const je = await createInvoiceJournalEntry(
|
||||
supabase, companyId, userId, invoice as Invoice, (company as CompanySettings).entity_type
|
||||
@@ -2612,7 +2614,7 @@ async function commitMarkInvoiceSent(
|
||||
|
||||
const { data: settings, error: settingsError } = await supabase
|
||||
.from('company_settings')
|
||||
.select('accounting_method, entity_type, invoice_payment_accounts, bank_name, clearing_number, account_number, bankgiro, plusgiro, swish, iban, bic')
|
||||
.select('accounting_method, defer_invoice_booking, entity_type, invoice_payment_accounts, bank_name, clearing_number, account_number, bankgiro, plusgiro, swish, iban, bic')
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
@@ -2653,7 +2655,8 @@ async function commitMarkInvoiceSent(
|
||||
const isRealInvoice = !invoice.document_type || invoice.document_type === 'invoice'
|
||||
let journalEntryId: string | null = null
|
||||
|
||||
if (isRealInvoice && (settings?.accounting_method === 'accrual' || !settings?.accounting_method)) {
|
||||
// #967: same gate as the dashboard mark-sent path (issue-and-book-invoice.ts).
|
||||
if (isRealInvoice && booksInvoicesOnIssue(settings)) {
|
||||
try {
|
||||
const je = await createInvoiceJournalEntry(
|
||||
supabase, companyId, userId, invoice as Invoice,
|
||||
@@ -4163,14 +4166,15 @@ async function commitCreateSupplierInvoiceFromInbox(
|
||||
|
||||
const { data: settings } = await supabase
|
||||
.from('company_settings')
|
||||
.select('accounting_method')
|
||||
.select('accounting_method, defer_invoice_booking')
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
const accountingMethod = (settings?.accounting_method as AccountingMethod) || 'accrual'
|
||||
let registrationJournalEntryId: string | null = null
|
||||
|
||||
if (accountingMethod === 'accrual') {
|
||||
// #967: deferred companies register WITHOUT booking (same gate as
|
||||
// POST /api/supplier-invoices); ekonomi books later via the Bokför step.
|
||||
if (booksInvoicesOnIssue(settings)) {
|
||||
try {
|
||||
const journalEntry = await createSupplierInvoiceRegistrationEntry(
|
||||
supabase,
|
||||
|
||||
Reference in New Issue
Block a user