fix(invoices): make self-billed invoices creditable and their dates visible (#1827)
A self-billed invoice has invoice_number null by design (the counterparty's number lives in external_invoice_number), which broke the whole credit flow: the confirm input was disabled and compared against null, the API minted the literal number 'KR-null', and the credit-note PDF dropped its ML 17 kap 22 reference to the original. The editor also hid fakturadatum inside the collapsed Forval panel, so self-billed invoices silently registered with today's date and, being immutable, could not be corrected. - creditConfirmNumber() falls back to external_invoice_number; the credit page uses it for reason default, subtitle, original row, preview, confirm label/placeholder/disabled, mismatch check and submit gate - createCreditNote numbers 'KR-<external>' for self-billed originals and refuses with typed 400 INVOICE_CREDIT_NO_NUMBER when no number exists - mark-sent and send select external_invoice_number and fall back for the credit-note PDF's reference to the original - the Forval chip line now shows the invoice date in every mode, and self-billed mode renders fakturadatum + mottagningsdatum uncollapsed as transcription fields next to the external number Fixes #1820 Claude-Session: https://claude.ai/code/session_01SyDuePXxUFowaPBKpAv8SF 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
21c63b8b12
commit
78525bd391
@@ -926,6 +926,11 @@ const INVOICE: Record<string, StructuredErrorEntry> = {
|
||||
message_sv: 'Endast skickade, betalda eller förfallna fakturor kan krediteras.',
|
||||
message_en: 'Only sent, paid, or overdue invoices can be credited.',
|
||||
},
|
||||
INVOICE_CREDIT_NO_NUMBER: {
|
||||
httpStatus: 400,
|
||||
message_sv: 'Ursprungsfakturan saknar fakturanummer och kan inte krediteras.',
|
||||
message_en: 'The original invoice has no invoice number and cannot be credited.',
|
||||
},
|
||||
INVOICE_CREDIT_ISSUE_INCOMPLETE: {
|
||||
httpStatus: 500,
|
||||
message_sv:
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { isTextLikeLine } from '@/lib/invoices/display'
|
||||
import { creditConfirmNumber, isTextLikeLine } from '@/lib/invoices/display'
|
||||
|
||||
describe('creditConfirmNumber', () => {
|
||||
it('uses our own invoice number when present', () => {
|
||||
expect(
|
||||
creditConfirmNumber({ invoice_number: 'F-2026010', external_invoice_number: null }),
|
||||
).toBe('F-2026010')
|
||||
})
|
||||
|
||||
it('falls back to the external number for self-billed invoices (issue #1820)', () => {
|
||||
expect(
|
||||
creditConfirmNumber({ invoice_number: null, external_invoice_number: 'SB-2026-17' }),
|
||||
).toBe('SB-2026-17')
|
||||
})
|
||||
|
||||
it('returns null when the invoice carries no number at all', () => {
|
||||
expect(creditConfirmNumber({ invoice_number: null, external_invoice_number: null })).toBeNull()
|
||||
expect(creditConfirmNumber({})).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isTextLikeLine', () => {
|
||||
it('is true for explicit text rows regardless of amounts', () => {
|
||||
|
||||
@@ -17,6 +17,21 @@ export function invoiceDisplayNumber(invoice: {
|
||||
return invoice.invoice_number ?? invoice.external_invoice_number ?? INVOICE_NUMBER_DRAFT_LABEL
|
||||
}
|
||||
|
||||
/**
|
||||
* The number a user must type to confirm crediting an invoice. Regular
|
||||
* invoices confirm with their own `invoice_number`; self-billed invoices have
|
||||
* `invoice_number` null by design, so the counterparty's
|
||||
* `external_invoice_number` (the number shown everywhere in the UI) is the
|
||||
* one to type. Null when the invoice carries no number at all: the credit
|
||||
* flow must stay disabled then.
|
||||
*/
|
||||
export function creditConfirmNumber(invoice: {
|
||||
invoice_number?: string | null
|
||||
external_invoice_number?: string | null
|
||||
}): string | null {
|
||||
return invoice.invoice_number ?? invoice.external_invoice_number ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* True when an invoice line should render as a pure text row: description
|
||||
* only, no quantity/unit/price/amount columns. Explicit text rows
|
||||
|
||||
Reference in New Issue
Block a user