diff --git a/DECISIONS.md b/DECISIONS.md index 6d632175..4436c50f 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -645,6 +645,18 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-07-28] The "senaste bokförda verifikat" line in the balans-/resultatrapport header (#1267) reads MAX(voucher_number) over posted entries, never voucher_sequences.last_number. The sequence counter is an allocation high-water mark that provably drifts from the books in both directions: next_voucher_number burns a number when the follow-up insert fails (the reversal path in engine.ts does exactly that), delete_last_voucher decrements blindly by one instead of resetting to the new MAX, and pre-RPC SIE imports left it behind MAX. Since the whole point of the line is avstämning, printing an allocated number would send a reconciler chasing a gap that does not exist, so the label states plainly that the number is the last posted one. Scoped to the report's own date range rather than the fiscal year, so a Q1 report printed in November says something true about Q1; the balansrapport keeps the fiscal-year start as its lower bound because it accumulates. Skipped entirely on a dimension-filtered resultatrapport: that report already discloses it is partial, and an unfiltered voucher range beside a filtered result invites the wrong conclusion. No new i18n keys: both report views and the PDF template are hard-coded Swedish, per the "stays Swedish" report surfaces in .claude/rules/i18n.md, so the issue's acceptance criterion asking for sv+en strings does not apply here. +[2026-07-29] FX revaluation population fixed as-of-balansdagen + per-row booked gate: the revaluation queried LIVE open invoices with no date scoping, so rows issued after balansdagen (or never booked) were revalued into the closed year, fabricating 1510/2440 write-downs. Chose reconstruction via the existing reskontra-payments helpers over snapshotting live status. First attempt gated on accounting_method (cash -> skip the company entirely); adversarial review overturned that: BFL 5 kap 2 § 3 st requires kontantmetoden companies to book outstanding fordringar/skulder at balansdagen, so a company-level skip would leave real 1510 exposure unvalued. The predicate is therefore per row (is this invoice booked?) not per company. The invoice_date ceiling is unconditional, not historical-only, because post-dated invoices make the bug reachable for a current period too. The year-end readiness WARNING is deliberately left ungated: an unbooked FX row still has a remedy (/book posts at invoice_date, into the year being closed) that lockPeriod/closePeriod then destroys forever. fetchFxExposureScope never throws, preserving previewCurrencyRevaluation's read-only render-always contract. ROT/RUT 1513 overstatement and splitting the close into two staged approval runs deliberately left out (own fixes). + +[2026-07-29] Parallel issue work runs in three permanent lane worktrees (erp-lanes/lane-1..3, dev ports 3001-3003) with one GitHub issue per branch per PR, rather than per-issue worktree provisioning inside /fix (tried and removed 2026-07-28 as too heavy) or several issues stacked on one shared branch (which corrupted commits across sessions the same day: swept files, a 33-file amend, HEAD moving mid-task). Lanes are one-time infrastructure so no install/port cost is paid per issue, /fix stays issue -> steps -> 'it works' -> commit and only gains a branch step, and small single-issue PRs keep each CI cycle and each origin/main re-sync short. npm ci only in lanes: bun-isolated trees rendered a wrong settings UI on 2026-07-28 and pnpm ignores the npm overrides. + +[2026-07-29] Semesteravsattning under sammaloneregeln: the tillagg is now valued on the MONTHLY salary per vacation day (Semesterlagen 16a §) and accrues one month's earned share (dagar/12) per run, replacing dagslon x rate x hela arets dagar in both calculation-engine call sites. Two errors that partly cancelled, which is why the monthly figure looked plausible. Decisive evidence that the old base was wrong: absence-calculator.ts already paid the tillagg out of 2920 on the monthly base, so provision and payout disagreed and the liability drifted negative on every taken day; a regression test now pins accrual-per-day == payout-per-day. Deliberately forward-only: posted runs are not re-touched (BFL 5 kap 5 §, and correcting them is not the sanctioned path), and semesterberedning.ts already books a 7290/2920 drift adjustment against the actual booked balance at vacation-year close, so history self-corrects through existing sanctioned mechanics rather than a migration. dailyDivisor dropped from calculateVacationAccrual entirely: semestertillagg does not vary with workdays per week, and the old signature invited exactly that error. + +[2026-07-29] Betalda semesterdagar pro-rated per Semesterlagen 7 § (anstallningsdagar / dagar i intjanandearet x semesterdagar, rounded UP) applied ONLY on the statutory Apr-Mar basis; sammanfallande calendar-year companies keep the flat entitlement. Under sammanfallande the employee earns and takes in the same year, commonly with forskottssemester, so how a mid-year hire is treated is a collective-agreement question rather than a statutory one, and guessing there would understate a real entitlement. Two terms of 7 § left unmodelled and documented at the call site: unpaid full-day absence (no day source in the ledger) and the 4 § five-day cap (which bounds semesterledighet, a different quantity from paid days). Both omissions can only overstate, never understate, so neither can silently deny an employee a paid day. A cutover opening balance still outranks the computation. + +[2026-07-29] pain.001 to SEB: shipped an honest warning plus corrected per-bank instructions instead of a SEB CSV generator. SEB Business Arena rejects .xml on upload and its SEPA-CSV is EUR-only, so the previous copy walked the user into a failure discovered on the pay date. Declined to build the bank-specific CSV without SEB's actual file specification: a guessed payment format either fails at the bank or, worse, pays the wrong amounts, and no primary spec was available in-session. Bankgirot LB is the working path for inrikes SEK today and the corrected copy points there, but it is a bridge, not a fix: the banks sunset LB during 2026 (Swedbank 1 Aug 2026), so the CSV generator is still owed once the spec is in hand. + +[2026-07-29] Konto 8022 in a verifikat rattelse: made the account reachable instead of adding it to the shared BAS catalog. Verified against the official bas.se BAS 2026 v1.1 kontoplan that 8022 appears nowhere in it (kontogrupp 80 is exactly the 13 accounts lib/bookkeeping/bas-data already ships, and the full catalog matches BAS 2026 1:1 at 1290 vs 1286 accounts); it existed in BAS 2018 together with 8013, 8014, 8019, 8023 and 8073, all since retired. Seeding a retired account into the reference would push it onto every company's picker, so the fix instead wires the existing "Skapa konto" affordance into StrikeLinesDialog and CorrectionEntryDialog, which were the only account pickers in the app without it. BAS permits company-specific underkonton and correct_entry_lines_inline validates chart membership rather than BAS membership, so an own 8022 posts and exports to SIE unchanged. Separately noted, not fixed here: .claude/skills/modifier/holding-ab/SKILL.md still names 8022 as a bas_account_signal and instructs booking dotterbolagsforsaljning on "8020 / 8022", so our own domain skill points agents at an account the catalog lacks. + [2026-07-28] Recurring-schedule dims PR ships API/MCP/generator only, no schedule-dialog pickers: UI needs visual sign-off per house rule; substrate stops the cron-spawned-invoices-born-untagged leak now. [2026-07-29] Retired the generic design skills now that emilkowalski/skills is installed globally (animation-vocabulary, apple-design, emil-design-eng, find-animation-opportunities, improve-animations, pick-ui-library, prototype, review-animations in ~/.claude/skills). Deleted .claude/skills/mobile-ux-core (52 lines of universal mobile UX whose file triggers are *.dart/*.swift/*Activity.kt, paths that do not exist in this repo; superseded by design.md's accessibility section plus apple-design) and .claude/skills/scout-design (a design scan that filed Linear tickets via mcp__claude_ai_Linear__save_issue, while this project files GitHub issues and loop-design-scan is the same scan with the right output; loop-design-scan's sibling reference updated). Kept web-design-guidelines: it is a Vercel-plugin symlink, cheap to keep, and may regenerate anyway. Also removed the global ui-ux-pro-max skill, a 67-style/96-palette catalogue that pulls against a locked editorial-monochrome system. diff --git a/components/bookkeeping/AccountCombobox.tsx b/components/bookkeeping/AccountCombobox.tsx index 6453b954..dbf93397 100644 --- a/components/bookkeeping/AccountCombobox.tsx +++ b/components/bookkeeping/AccountCombobox.tsx @@ -216,9 +216,20 @@ export default function AccountCombobox({ value, accounts, onChange, onCommit, o // editing an already-committed number doesn't keep stealing focus. On // commit, close the dropdown too: focus advances to the amount field, so // a lingering open list would just cover the rows below. + // + // Unless the number matches nothing. Then the dropdown is showing the + // empty state, which carries the only way forward for a number outside + // BAS (a retired account such as 8022, or a company-specific + // underkonto): the "Skapa konto" affordance. Closing on the fourth + // keystroke used to hide it before it was ever painted, which made the + // affordance unreachable for exactly the numbers that need it. if (newValue !== value) { onCommit?.(newValue) - setIsOpen(false) + if (searchAccounts(accountIndex, newValue).length > 0) { + setIsOpen(false) + return + } + setIsOpen(true) return } } diff --git a/components/bookkeeping/CorrectionEntryDialog.tsx b/components/bookkeeping/CorrectionEntryDialog.tsx index 89afb4d6..115d2053 100644 --- a/components/bookkeeping/CorrectionEntryDialog.tsx +++ b/components/bookkeeping/CorrectionEntryDialog.tsx @@ -14,6 +14,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import AccountCombobox from '@/components/bookkeeping/AccountCombobox' +import { AddAccountDialog } from '@/components/bookkeeping/AddAccountDialog' import CorrectionPreview from '@/components/bookkeeping/CorrectionPreview' import { autoCorrectionDescription, @@ -29,6 +30,7 @@ import { changeCorrectionLineAccount, getSelectableCorrectionCatalog, } from '@/lib/bookkeeping/correction-line-account' +import { splitCreateAccountPrefill } from '@/lib/bookkeeping/create-account-prefill' import { loadBasCatalog, type CatalogAccount } from '@/lib/bookkeeping/bas-catalog-client' import type { JournalEntry, JournalEntryLine, BASAccount } from '@/types' @@ -56,6 +58,10 @@ export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCor const [lines, setLines] = useState([]) const [description, setDescription] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) + // Index of the line whose combobox opened the create dialog, and the search + // string it was showing. Null index = the dialog is closed. + const [creatingAccountForLine, setCreatingAccountForLine] = useState(null) + const [createAccountPrefill, setCreateAccountPrefill] = useState('') const activeAccounts = useMemo( () => accounts.filter((account) => account.is_active), @@ -149,6 +155,34 @@ export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCor setLines((prev) => prev.filter((_, i) => i !== index)) } + const closeCreateAccount = () => { + setCreatingAccountForLine(null) + setCreateAccountPrefill('') + } + + // A number that is neither in the company chart nor in BAS 2026 (a retired + // account such as 8022, or a company-specific underkonto) would otherwise be + // a dead end here: the rättelse can only post to accounts that exist in the + // chart. Creating it inline keeps the half-finished rättelse intact. + const handleAccountCreated = async (account: { account_number: string; account_name?: string }) => { + await fetchAccounts() + if (creatingAccountForLine != null) { + // fetchAccounts' state update is not visible in this closure, so the + // fresh account's own name is passed alongside the stale sources. The + // reactivate path reports no name, but that account is already in + // `accounts` (the fetch includes deactivated rows). + const created = account.account_name + ? [{ account_number: account.account_number, account_name: account.account_name }] + : [] + setLines((prev) => prev.map((line, index) => ( + index === creatingAccountForLine + ? changeCorrectionLineAccount(line, account.account_number, [...accountNameSources, ...created]) + : line + ))) + } + closeCreateAccount() + } + const totalDebit = lines.reduce((sum, l) => sum + (parseFloat(l.debit_amount) || 0), 0) const totalCredit = lines.reduce((sum, l) => sum + (parseFloat(l.credit_amount) || 0), 0) const roundedDebit = Math.round(totalDebit * 100) / 100 @@ -298,6 +332,10 @@ export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCor accounts={activeAccounts} catalog={selectableCatalog} onChange={(v) => updateLineAccount(index, v)} + onCreateAccount={(prefill) => { + setCreatingAccountForLine(index) + setCreateAccountPrefill(prefill) + }} disabled={accountsStatus !== 'ready'} />