Files
accounted/lib/invoices/display.ts
T
Jakob Wennberg 78525bd391 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>
2026-08-24 13:18:57 +02:00

50 lines
1.9 KiB
TypeScript

export const INVOICE_NUMBER_DRAFT_LABEL = '(Utkast)'
export function invoiceNumberDisplay(value: string | null | undefined): string {
return value ?? INVOICE_NUMBER_DRAFT_LABEL
}
/**
* The number to show for an invoice. Self-billing invoices we received carry
* the counterparty's number in `external_invoice_number` (our own
* `invoice_number` is null by design), so fall back to it before the draft
* label.
*/
export function invoiceDisplayNumber(invoice: {
invoice_number?: string | null
external_invoice_number?: string | null
}): string {
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
* (line_type 'text') always qualify; so do product rows carrying no amounts
* at all (quantity and unit price both zero/absent). Users write free-text
* lines via the article picker's "Egen rad (fri text)" and leave antal/pris
* at zero; printing "0 / 0,00 SEK / 0,00 SEK" on those is noise (issue #1053).
*/
export function isTextLikeLine(item: {
line_type?: 'product' | 'text' | null
quantity?: number | null
unit_price?: number | null
}): boolean {
return item.line_type === 'text' || (!item.quantity && !item.unit_price)
}