0bb482bf6e
* feat(bookkeeping): edit the lines of a proposed kontering via Andra rader Proposal views (AI suggestion, static template, counterparty template with or without a line pattern) previously offered only accept-or-start-over: the verifikation preview was pure rendering and the only line-editable path was library templates. This adds an "Andra rader" affordance to the proposal view in QuickReviewDialog that hands the COMPUTED lines (accounts, SEK amounts, VAT legs, exactly what the preview shows) into TransactionBookingDialog / JournalEntryForm as an editable prefill, reusing the same initialLines mechanism library templates already use. - lib/bookkeeping/proposal-lines.ts: line computation extracted from JournalEntryPreview into computeProposalLines() (single source for preview and prefill, so they cannot drift) plus proposalLinesToFormLines() mapping to the JournalEntryForm prefill shape. The settlement leg is flagged so the booking dialog swaps in the transaction's resolved cash account and stamps currency metadata, mirroring buildInitialLinesFromTemplate. - JournalEntryPreview now renders computeProposalLines() output unchanged. - TransactionBookingDialog accepts proposalLines (takes precedence over preselectedTemplate); the booking still goes through JournalEntryForm's normal manual validation and the engine, no validation bypassed. - Ore rounding funnels through roundOre(); guard baseline ratcheted down. - New strings in messages/sv.json and messages/en.json (tx_quick_review). - Unit tests for all three proposal branches incl. VAT legs, reverse charge, multi-line patterns, 3740 rounding diff and FX metadata. Fixes #1878 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): make the Andra rader prefill engine-exact (skeptic findings) Three skeptics refuted the first cut of #1878: the extracted preview math was a lossy approximation of the engine, and making it bookable made every loss a real booking defect. This commit closes each refuted scenario by mirroring the exact engine path per proposal branch: - Balance: VAT is single-rounded and the net leg is gross minus that VAT (transaction-entries.ts semantics). Independently rounded net+VAT went off by 1 ore for 12% grosses at 14 mod 28 ore (e.g. 102.06, 100.94), prefillling an unbookable verifikat. - 'Ingen moms' deviation: the dialog resolves the UI 'none' sentinel via resolveExplicitVat before computing lines, so an explicit no-VAT choice prefills no VAT line instead of re-deriving the 25% category default into a bookable 2641 leg (ruta 48 inflation on e.g. loan repayments). - Ore parity: engineRound (plain Math.round(x*100)/100, matching the engine) replaces roundOre where the engine is naive; roundOre kept only where the engine uses it (category VAT leg). No more 1-ore drift between preview, prefill and the booked verifikat (8.62 RC, 34.30@12%). - Legacy counterparty pairs: new counterpartyLegacy mode mirrors the legacy booking path: reverse charge emits the 2645/2614 fiktiv-moms pair (previously dropped: an RC expense would have booked without fiktiv moms, understating rutor 30/48), VAT on expenses only, income gross, and sign-mismatched matches mirrored like buildLegacyMismatchResult. - Pattern mirror: sign-mismatched line patterns flip learned sides like buildMultiLineMappingResult; ratio allocation filters business/tax types. - Entity accounts: static template accounts resolve debit/credit_account_ab for aktiebolag (resolveTemplateAccountsForEntity), so an AB no longer previews or books EF-only accounts like 2013. - Settlement swap: only a literal-1930 settlement leg is swapped to the resolved cash account (applySettlementAccount parity); learned non-1930 money legs (1510/2440/2890/19xx) stay authoritative. - FX: QuickReviewDialog hands its enriched transaction row to the booking dialog so the settlement leg's exchange_rate metadata matches the rate the SEK amounts were computed with. 34 unit tests incl. every skeptic counterexample; guard baseline ratcheted to 622 (below main's 626). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): line-pattern settlement leg uses the learned legacy pair (skeptic refutation) Two independent skeptics refuted the pattern branch: the engine books the money leg on the counterparty template's learned legacy account (credit for an expense, debit for an income, mirror-swapped, falling back to 1930), while the preview/prefill defaulted to 1930. A SIE-learned pattern settling on 2440 showed kredit 1930 in the preview but booked kredit 2440 on confirm. QuickReviewDialog now passes the learned pair raw (no entity resolution, engine parity) and computeProposalLines selects the settlement account exactly like buildTransactionEntryLines; the literal-1930 swap to the resolved cash account is unchanged. CodeRabbit findings declined deliberately (see DECISIONS.md): the 3740 rounding line keeps the engine's business-side placement for both diff signs (parity contract; an unbalanced set is rejected at commit), and the naiveOreRound baseline stays at 622 (engineRound is a documented parity exception). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
105 lines
4.3 KiB
TypeScript
105 lines
4.3 KiB
TypeScript
import {
|
|
getDefaultAccountForCategory,
|
|
getDefaultVatTreatmentForCategory,
|
|
} from '@/lib/bookkeeping/category-mapping'
|
|
import type { TransactionCategory, VatTreatment } from '@/types'
|
|
|
|
/**
|
|
* The template shape the transaction review dialog actually reads.
|
|
*
|
|
* Deliberately NOT `BookingTemplate`: the review dialog is opened from three
|
|
* sources, and only one of them has a full catalog template behind it.
|
|
*
|
|
* - a static catalog template (`BookingTemplate`, structurally assignable),
|
|
* - a user library template converted to the same shape,
|
|
* - a learned counterparty template ("Tidigare motparter"), which has no
|
|
* catalog entry at all: it carries a name and a debit/credit pair and
|
|
* nothing else.
|
|
*
|
|
* The counterparty case used to be forced into `BookingTemplate` with an
|
|
* `as BookingTemplate` cast on a two-field object literal. The cast made
|
|
* every missing field look present to the compiler, so `template.debit_account`
|
|
* read `undefined` at runtime, flowed into the dialog's `defaultAccount` prop
|
|
* (typed `string`), and crashed the page on `accountOverride.startsWith('2')`.
|
|
* Typing the optional fields as optional is what makes that class of bug a
|
|
* compile error instead of an error boundary.
|
|
*/
|
|
export interface ReviewTemplate {
|
|
id: string
|
|
name_sv: string
|
|
debit_account?: string
|
|
credit_account?: string
|
|
/** AB-specific account overrides (static catalog templates carry these;
|
|
* the booking engine substitutes them for aktiebolag). */
|
|
debit_account_ab?: string
|
|
credit_account_ab?: string
|
|
vat_treatment?: VatTreatment | null
|
|
vat_rate?: number
|
|
special_rules_sv?: string
|
|
deductibility_note_sv?: string
|
|
requires_vat_registration_data?: boolean
|
|
reverse_charge_supplier_type?: 'eu_business' | 'non_eu_business' | 'swedish_business'
|
|
}
|
|
|
|
export interface QuickReviewDefaults {
|
|
account: string
|
|
vat: VatTreatment | 'none'
|
|
}
|
|
|
|
/**
|
|
* Resolve the account + VAT the review dialog starts on.
|
|
*
|
|
* A template without a `templateId` (library or counterparty template) is not
|
|
* validated server-side against the static catalog, so its own debit account
|
|
* and VAT treatment seed the form. Anything the template leaves unset falls
|
|
* back to the transaction category's defaults, and finally to an empty
|
|
* account / no VAT: the caller feeds a required `string` prop, and an
|
|
* `undefined` there is what took the page down.
|
|
*/
|
|
export function resolveQuickReviewDefaults(
|
|
template: ReviewTemplate | null | undefined,
|
|
templateId: string | undefined,
|
|
category: TransactionCategory | null | undefined,
|
|
): QuickReviewDefaults {
|
|
const useTemplateDefaults = !templateId && !!template
|
|
|
|
const account =
|
|
(useTemplateDefaults ? template.debit_account : undefined) ||
|
|
(category ? getDefaultAccountForCategory(category) : '') ||
|
|
''
|
|
|
|
const vat: VatTreatment | 'none' = useTemplateDefaults
|
|
? (template.vat_treatment ?? 'none')
|
|
: (category ? (getDefaultVatTreatmentForCategory(category) ?? 'none') : 'none')
|
|
|
|
return { account, vat }
|
|
}
|
|
|
|
/**
|
|
* Resolve the VAT treatment the review dialog should put on the wire.
|
|
*
|
|
* 'none' is a UI-only sentinel with two meanings that must not be conflated:
|
|
*
|
|
* - As the SEEDED default (exempt categories like bank fees, or a template
|
|
* without a treatment): send nothing and let the server derive, exactly as
|
|
* before. The derivation yields no VAT line for those categories, and
|
|
* keeping the wire empty preserves byte-identical bookings for the common
|
|
* untouched case.
|
|
* - As a DEVIATION (the user explicitly picked "Ingen moms" on a category
|
|
* whose default carries VAT, or the dialog auto-set 'none' for a class-2
|
|
* account override): send 'exempt'. The old collapse to undefined made the
|
|
* server re-derive the default, silently booking 25% moms against an
|
|
* explicit no-VAT choice, while the preview showed no VAT line. An explicit
|
|
* 'exempt' books no VAT line and records the classification the
|
|
* momsdeklaration should see (see buildMappingResultFromCategory's note);
|
|
* for income it also lands revenue on 3004 instead of a rate-bearing
|
|
* account.
|
|
*/
|
|
export function resolveExplicitVat(
|
|
selected: VatTreatment | 'none',
|
|
seededDefault: VatTreatment | 'none',
|
|
): VatTreatment | undefined {
|
|
if (selected !== 'none') return selected
|
|
return seededDefault === 'none' ? undefined : 'exempt'
|
|
}
|