feat(invoices): allow BAS class 1-3 posting-account overrides and complete the aktiekapital note (#1121)
- invoice/article posting-account overrides accept active class 1-3 accounts; class 1-2 (balance-sheet) accounts are rejected on VAT-bearing lines so the ruta 05 tax base always books to a 3xxx account - shared posting-account regex across server schemas, pending-operation re-validation, and client forms - share-capital settings (aktiekapital/antal_aktier) feed the annual-report note; kvotvarde derived per ABL 1 kap 6 $; all-or-nothing pair constraint - signed per-rate VAT breakdown on credit-note PDFs; U+2212 to ASCII hyphen Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b0044bfe98
commit
43f7ccab9e
@@ -995,6 +995,84 @@ describe('POST /api/v1/companies/:companyId/invoices', () => {
|
||||
expect(insertedItems![0].revenue_account).toBe('3041')
|
||||
})
|
||||
|
||||
it('persists a class-2 posting account on an invoice item', async () => {
|
||||
withInvoiceWriteScope()
|
||||
const createdInvoice = { id: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee', status: 'draft' }
|
||||
let insertedItems: Array<Record<string, unknown>> | null = null
|
||||
mockServiceClient.mockReturnValue({
|
||||
from: (table: string) => {
|
||||
const handler: ProxyHandler<object> = {
|
||||
get(_target, prop) {
|
||||
if (prop === 'insert') {
|
||||
return (rows: Record<string, unknown> | Array<Record<string, unknown>>) => {
|
||||
if (table === 'invoice_items') insertedItems = rows as Array<Record<string, unknown>>
|
||||
return new Proxy({}, handler)
|
||||
}
|
||||
}
|
||||
if (prop === 'then') {
|
||||
const data = table === 'company_members'
|
||||
? { company_id: COMPANY_ID, role: 'owner' }
|
||||
: table === 'customers'
|
||||
? SWEDISH_BUSINESS_CUSTOMER
|
||||
: table === 'chart_of_accounts'
|
||||
? [{ account_number: '2897' }]
|
||||
: table === 'invoices'
|
||||
? createdInvoice
|
||||
: null
|
||||
return (resolve: (value: unknown) => void) => resolve({ data, error: null })
|
||||
}
|
||||
return () => new Proxy({}, handler)
|
||||
},
|
||||
}
|
||||
return new Proxy({}, handler)
|
||||
},
|
||||
})
|
||||
|
||||
const res = await createInvoice(
|
||||
makePostInvoice(`https://x.test/api/v1/companies/${COMPANY_ID}/invoices`, {
|
||||
customer_id: CUSTOMER_ID,
|
||||
invoice_date: '2026-05-12',
|
||||
due_date: '2026-06-11',
|
||||
currency: 'SEK',
|
||||
items: [
|
||||
{ description: 'Deposition', quantity: 1, unit: 'st', unit_price: 1000, vat_rate: 0, revenue_account: '2897' },
|
||||
],
|
||||
}),
|
||||
companyParams(COMPANY_ID),
|
||||
)
|
||||
|
||||
expect(res.status).toBe(201)
|
||||
expect(insertedItems![0].revenue_account).toBe('2897')
|
||||
})
|
||||
|
||||
it('rejects a class-2 posting account on a VAT-bearing line', async () => {
|
||||
withInvoiceWriteScope()
|
||||
mockServiceClient.mockReturnValue(
|
||||
makeFlexibleSupabase({
|
||||
company_members: { data: { company_id: COMPANY_ID, role: 'owner' }, error: null },
|
||||
customers: { data: SWEDISH_BUSINESS_CUSTOMER, error: null },
|
||||
chart_of_accounts: { data: [{ account_number: '2897' }], error: null },
|
||||
}),
|
||||
)
|
||||
|
||||
const res = await createInvoice(
|
||||
makePostInvoice(`https://x.test/api/v1/companies/${COMPANY_ID}/invoices`, {
|
||||
customer_id: CUSTOMER_ID,
|
||||
invoice_date: '2026-05-12',
|
||||
due_date: '2026-06-11',
|
||||
currency: 'SEK',
|
||||
items: [
|
||||
{ description: 'Deposition', quantity: 1, unit: 'st', unit_price: 1000, vat_rate: 25, revenue_account: '2897' },
|
||||
],
|
||||
}),
|
||||
companyParams(COMPANY_ID),
|
||||
)
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
const body = await res.json()
|
||||
expect(body.error.code).toBe('INVOICE_CREATE_POSTING_ACCOUNT_VAT_CONFLICT')
|
||||
})
|
||||
|
||||
it('rejects a revenue_account not in the chart of accounts', async () => {
|
||||
withInvoiceWriteScope()
|
||||
mockServiceClient.mockReturnValue(
|
||||
|
||||
@@ -384,7 +384,7 @@ registerEndpoint({
|
||||
'is_self_billed=true registers a self-billing invoice your CUSTOMER issued on your behalf (a sale for you). It is booked immediately (not a draft, no F-number), so external_invoice_number and received_date are required and it is NOT dry-run-free of side effects on the live call. Do NOT set it for a normal invoice you issue yourself.',
|
||||
'Project/cost-center tagging: pass default_dimensions ({"6":"P001"} = project, {"1":"KS01"} = kostnadsställe) for the whole invoice and/or items[].dimensions per line (per-line wins per key). Tags are stored on the draft and applied to the journal entry lines when the invoice is sent. When the company has the dimension registry enabled, unknown or archived codes are rejected at :send with 400 DIMENSION_VALIDATION_FAILED — list valid codes via GET /dimensions.',
|
||||
'ROT/RUT: set items[].deduction_type ("rot"|"rut") on labor lines plus labor_hours and work_type (Skatteverket arbetstypskod). The invoice must carry deduction_personnummer AND housing info: deduction_housing_designation (fastighetsbeteckning) for småhus, or deduction_apartment_number + deduction_brf_org_number for bostadsrätt. deduction_amount is computed server-side and cannot be set by the caller; the response exposes deduction_total and remaining_amount = total - deduction_total (Skatteverket pays the rest via 1513). Validation failures return 400 INVOICE_CREATE_ROT_RUT_VALIDATION.',
|
||||
'Articles: pass items[].article_id (from the artikelregister, GET /articles) to link a line to a catalog article; price/description are still taken from the request body (the API never auto-fills from the article: send the values you want on the invoice). items[].revenue_account optionally overrides the BAS class-3 account and is validated against the chart of accounts.',
|
||||
'Articles: pass items[].article_id (from the artikelregister, GET /articles) to link a line to a catalog article; price/description are still taken from the request body (the API never auto-fills from the article: send the values you want on the invoice). items[].revenue_account is the legacy wire name for an optional BAS class 1-3 posting-account override and is validated against the chart of accounts.',
|
||||
],
|
||||
example: {
|
||||
request: {
|
||||
|
||||
Reference in New Issue
Block a user