# bokf — project state Settled decisions live in `docs/DECISIONS.md`; this file is the live snapshot. Read with `AGENTS.md` (rules) and `docs/TUI-GUIDELINES.md` (UI conventions). ## Status Working self-hosted bookkeeping system, not production-proven. Backend ledger core is complete, as are invoicing, the payroll engine and the payroll TUI; filing/year-end work remains. TUI is usable and exercised by `make test-pty` (golden screen-text scenarios, now including the payroll run and the lönebesked); `make test` covers the server/protocol/ledger, the TUI widget unit tests and the docs consistency check. ## Resume here (2026-09-22) - **Deklaration 2025/2026 (org 2, 2026-09-22)**: the imported year is closed by the source's `Stäng intäktskonton/kostnadskonton`, so the resultatrapport (and the INK2/SRU derived from it) showed 0. `report.income_statement` now skips those vouchers (deployed in `v0.1.65`), which gives **241 817,48 kr** resultat efter skatt för 2025/2026. In the TUI the "Bokfört resultat" section is absent for such years (the close goes straight to 2099). Two **data drifts** (since resolved, see the v0.1.66 entry): the imported FY 2022/2023 result transfer is **17 857,63 kr** short of that year's P&L (its tax was never booked as an expense), so the derived balance sheet does not balance by that amount; and 2099 is 1,07 kr off the P&L result. Locked years stay locked — a correction belongs in the open year (2026/2027). The owner can now run Bokslutshubben → Inkomstdeklaration. - **Deployed**: `v0.1.66`, healthy on `nas` (Alpine runtime, static aarch64 binaries cross-compiled on this machine — ~20 s, the host only assembles the image; image 33.7 MB, no `libssl3`). `v0.1.55` brought the payroll server waves (schema v11: employees, tax tables, lönebesked) and the payroll TUI; `v0.1.56` the **Bolaget** hub; `v0.1.57` the flat main menu without section headings, the **System** hub (Skattetabeller, Revision) and the client-side remembered attachment directory; `v0.1.58` loads the system CA bundle explicitly so the static binaries can verify TLS (the Skatteverket fetch, SMTP, static clients); `v0.1.59` adds the keyboard protocols (though gnome-terminal/VTE cannot send Ctrl+Enter); `v0.1.60` the per-feature voucher series (Bolaget → Verifikationsserier, IB configurable with legacy `IB` still recognized), Nästa fakturanummer in Fakturauppgifter and `F9`-only save hints; `v0.1.61` lets underlag be granskade (text in a pager, other files in the desktop viewer) or nedladdade from the voucher detail and the inbox; `v0.1.62` brings invoice **text rows**, `u = duplicera`, `b = kvittera betalning` (`invoice.pay` links a payment voucher) and rejects name-like `smtp_from` values (schema v12: `invoice_rows.is_text`, `invoices.paid_date`/ `payment_voucher_id`); `v0.1.63` prints the organization name in the invoice and lönebesked headers and adds the shared document colour setting `document_header_color`; `v0.1.64` is the TUI interaction-model spec (docs only, no runtime change); `v0.1.65` carries earlier IB vouchers forward, restarts P&L at each year, skips imported `Stäng` closings in the income statement and the momsomföring in the VAT report; `v0.1.66` ignores IB rows on P&L accounts. The live database migrated v9 → v11 on the v0.1.55 startup and v11 → v12 on the v0.1.62 startup, each with the automatic pre-migration snapshot in `var/db/backup/`. `main` and the tags are pushed to `nas`. - **Invoices (2026-09-22)**: the follow-ups are done and deployed in `v0.1.62` — free-text rows, `u = duplicera`, `b = kvittera betalning` (`invoice.pay`, strict receivable check) and `smtp_from` address validation. `test_core` has `invoice_extras`; the pty suite has `invoice-duplicate` and `invoice-pay`. Remaining invoice work is in the backlog (credit notes, per-row account, kundreskontra, partial payments). - **Document header (2026-09-22, deployed in `v0.1.63`)**: the invoice header bar prints the organization name instead of the hard-coded `MAKANDRA AB` outline (scaled down and truncated with `...` if long). The shared document setting `document_header_color` (`#rrggbb`, default `#314c59`, editable in Bolaget → Fakturauppgifter) colours the header bar of both the invoice and the lönebesked, and the lönebesked prints the employer name there too (it previously used the Comfortaa wordmark). The invoice's `FAKTURA` outline is unchanged; the `MAKANDRA AB` outlines in `src/wordmark.h` are now unused at runtime. - **TUI interaction model (2026-09-22)**: the UX session settled two focus modes, three entity lifecycles, explicit `Spara` in every savable form and `` drafts persisted to `$XDG_CACHE_HOME/bokf/drafts.json` (deletable from both the list and the editor), plus one `tui_action` registry per screen behind `F2` (no `§`). The **Kunder pilot is implemented** (`clients/drafts.[ch]`, `tui_action_menu()`, `tui_action_hint()`, ``, `Spara` row, F2 menu, delete draft) with unit tests, the pty scenarios `customer-draft`/`customer-draft-save` and a green `make gate`. It is in `main` (`07f5b14`), pushed and part of `v0.1.65`. **Next session**: get the human's Ctrl+R test feedback (Bolaget → Kunder: Ctrl+N, type, Esc, F2, Spara, Radera utkast) and then continue the rollout per backlog item 17: the other registers, explicit `Spara` in the settings forms, `tui_rt` action menus. Review points kept in `TUI-GUIDELINES.md`: drafts of encrypted fields (an employee's personnummer) and `Enter` on action rows. Spec in `TUI-GUIDELINES.md` "Interaction model"; decisions in `DECISIONS.md` #28. - **IB carry-forward (2026-09-22, deployed in `v0.1.65`)**: reports and `sie.export` carry earlier years' `IB` vouchers into the opening balance (Makandra's 2021 IB voucher held aktiekapital 2081/1940, which vanished from every later year) and restart P&L accounts at each fiscal-year start (they used to accumulate since 2021). SIE writes `#IB`/`#UB` only for balance accounts and `#RES` only for P&L. Makandra's opening balances now show the 17 857,63 kr 2022/23 gap openly (balance accounts' IB sums to that instead of 0). Merged together with `eff/imported-closings`, whose "Stäng" skip now applies only to `sie_import` vouchers. `test_core` has `ib_carry` and `imported_closings`. - **Momsrapport (2026-09-22, deployed in `v0.1.65`)**: `report.vat` (and `report.vat_eskd`) skips the momsomföring (vouchers with a 2650 row) and SIE-imported `Stäng` closings. Makandra's 2025/26 report was all zeros because V107 (Momsdeklaration) and V109 were counted; the rules themselves were fine. `test_core` has `vat_settlement`. - **IB rows on P&L accounts (2026-09-22, deployed in `v0.1.66`)**: reports ignore `IB` rows on P&L accounts (the balancing side of an IB delta); `bokfctl --dry-run` sets the request's `dry_run`. Makandra data, booked by the owner: V 25 in 2021/22 (8910/2512 17 857,63 — the tax voucher missing from the source's SIE export), IB 2 (2512 D / 8999 K 17 857,63) and IB 3 (2081 D / 1940 K 25 000) in 2026/27. Every year now balances, 2026/27 opens exactly on the filed 2025/26 UB; only öre diffs remain in history (2512 1,63, 2099 1,07, moms 0,56). 2021/22 was reopened for V 25 and must be closed again by the owner. - **Whole-krona rounding (2026-09-22, not deployed)**: the SRU export and the TUI årsredovisning round every account to whole kronor (`util_round_kr`) and sum those, so their lines, totals and INK2 fields agree to the krona (the SRU export used to truncate each field, the årsredovisning rounded each line). The balance sheet's rounding krona goes to Balanserat resultat / 7302, and 2099 is replaced by the year's result in both. The årsredovisning's "Stäng" skip never matched (`strncmp` with 7 bytes for the 6-byte "Stäng"), so imported years showed a zero result; it now follows the server rule (SIE-imported only) via the new `source` field on `report.general_ledger` rows. The total "Summa eget kapital, avsättningar och skulder" no longer counts 2099 on top of the result. - **Mail configuration**: Makandra AB (org 2) has **no** `smtp_*` settings in bokf, so `invoice.send` there is `SMTP_NOT_CONFIGURED` (fine if invoices are sent elsewhere — set them up when wanted). Mock AB (org 1) has host/port/user/password but its stored `smtp_from` is the name "Anders Bergsten": change it to an e-mail address in **Bolaget → E-post (SMTP)** before sending; the new validation refuses names on save. - **Next tasks (payroll follow-ups)**: fold `payroll.settings_get/set` into `settings.get/set` (cmd_settings.c was busy during wave 1); settle the over-80k % rule (SKV 433 leaves it ambiguous — currently a clear `INVALID_ARGS`); move `invoice.send` to the shared `src/mail.c`. The TUI has no screen for the payroll account/rate settings yet (they are defaults and can stay server-side until the fold). - **Done in the TUI wave**: `clients/screens_payroll.c` with Lönekörningar (list + Ctrl+N, run screen with F5 preview, Ctrl+Enter post after confirmation and the action rows Lönebesked, AGI-underlag, Betala skatt & avgifter), Anställda under Bolaget and the Skattetabeller fetch/import/status screen under System. A pty scenario posts a run and fetches its lönebesked, so the payslip PDF path is exercised end to end. - **Menu (2026-09-21)**: the dashboard is one flat list without section headings: Verifikat, Underlag, Bankavstämning, Mallar, Fakturor, Lönekörningar, Rapporter, Bokslut, Bolaget, System, Ingående balans, Räkenskapsår, Logga ut. **Bolaget** is the master-data hub (Företagsuppgifter, Fakturauppgifter with Nästa fakturanummer, **Verifikationsserier**, E-post (SMTP), Styrelseledamöter and the registers Anställda, Kunder, Momsregler); **System** holds Skattetabeller and Revision. Inställningar is gone, and bilagornas mapp is no longer a setting: the file browser remembers the last pick directory in `tui.conf` and falls back to `$HOME`. `--screen settings` remains an alias for Bolaget. ## Open decisions None open. Completed items that used to be listed here are archived in `docs/DECISIONS.md`. ## Backlog (prioritized, from COMPLIANCE.md §10 and the audit) 1. ~~eSKD file generation~~ done: `report.vat_eskd`. 2. ~~Bokslut automation~~ done: `bokslut.post` + TUI Bokslut screen. 3. ~~K2 årsredovisning draft and SRU files~~ done: TUI draft + `sru.export`. 4. ~~`audit.verify` voucher chain~~ done (also attachments with `full:true`). 5. ~~`report.general_ledger` / `report.voucher_list`~~ done. 6. ~~`describe` argument schemas~~ done (`CMD_ARGS`). 7. ~~Pre-migration `VACUUM INTO` snapshot~~ done. 8. ~~Docker image + compose~~ done (no registry, no systemd unit). 9. Password change, user disable, TOTP. 10. ~~Bank import/reconciliation (CSV first)~~ phase 1 (SEB CSV + matching, schema v8) and phase 2 (Ctrl+N prefills the voucher and auto-matches it, `9b1021e`) done. Phase 3 (`bank_rule.*`) **dropped by the user 2026-09-21**; PSD2 only if it ever hurts. 11a. Imported history years with source P&L closings: decided, no importer change (see DECISIONS.md). 11. ~~SIE import only into an empty fiscal year~~ chronological multi-year import works; each year must still target an empty fiscal year. 12. TUI polish: horizontal scrolling in long text fields, bracketed paste. 13. ~~Payroll TUI (wave 3)~~ done (`clients/screens_payroll.c`); the remaining payroll follow-ups are listed under "Resume here". No employee is registered yet in the real orgs. 14. ~~Invoice text rows, duplicate and payment registration~~ done in `v0.1.62` (schema v12). Remaining invoice follow-ups when needed: credit notes (`invoice.credit`), per-row account in the invoice form, kundreskontra view, partial payments. 15. `make test-pty` speed if it ever exceeds ~60 s: parallelise the independent scenarios (each has its own rig) and add `--only` symmetry; measure first. 16. Test fixtures (`t_fresh_org()`) + one test file per domain (`tests/core_.c`) so `--only` stops cascading; pilot with one domain. 17. Interaction-model rollout (spec: `TUI-GUIDELINES.md` "Interaction model", decisions #28): **Kunder done** 2026-09-22 (widget layer `tui_action`/`F2`, `clients/drafts.[ch]`, drafts/``/`Spara`, pty scenarios), awaiting the human's Ctrl+R test feedback. Remaining: the other register screens, explicit `Spara` in the settings forms (replacing per-field autosave), and `tui_rt` action menus. Original entries for the struck items are in `docs/DECISIONS.md`. ## Environment / how to run - **Deployed**: `scripts/deploy.sh` (latest `v0.1.66`, healthy on nas). Live daemon `tls:bokf.makandra.eu:8788`, token `~/.config/bokf/migration-token` (scopes `read,write`; owner-only actions like closing years must be done by the human in the TUI). Git remote `origin` is `nas:/mnt/data/git-repos/bokf.git` (push `main` and tags). `sh scripts/install-hooks.sh` once per checkout points `core.hooksPath` at `.githooks/`, so every push runs `make gate` (clean `-Werror` build in `build-gate/`, the suite and ASan); it is installed here. `deploy/Dockerfile.cross` + `deploy/cross-build.sh` produce static aarch64 `bokfd`/`bokfctl` in ~20 s when the host architecture differs; the image is assembled on the host from `.prebuilt` (see docs/DEPLOY.md). - **Local test rig** (transient, `/tmp`): daemon `./build/bokfd --db /tmp/opencode/bokf-local/t.db --socket /tmp/opencode/bokf-local/sock`, org 1, login `admin`/`testpass123`. Drive the TUI over a pty with `scripts/tui-sandbox.sh -- ./build/bokftui --socket /tmp/opencode/bokf-local/sock --org 1 --fy 1 ...` plus a small driver that feeds keys and an ANSI renderer (recreate if gone; arrows are `ESC O B/A`, Tab `\t`, `^X` `\x18`, `^Enter` `ESC[27;5;13~`, F5 `ESC[15~`). Never test against the live daemon. - Demo: db `~/bokf-demo/bokfd.db`, socket `~/bokf-demo/bokfd.sock`, pid file `~/bokf-demo/bokfd.pid`; login `admin` / `demo1234`. Start TUI: `cd ~/work/bokf && BOKFD_SOCKET=$HOME/bokf-demo/bokfd.sock \ BOKFD_USER=admin BOKFD_PASSWORD=demo1234 ./build/bokftui` - Restart daemon: kill the pid file's process, then `BOKFD_BACKUP_DIR=$HOME/bokf-demo/backup \ BOKFD_EXPORT_DIR=$HOME/bokf-demo/export setsid nohup \ ./build/bokfd --db $HOME/bokf-demo/bokfd.db \ --socket $HOME/bokf-demo/bokfd.sock > $HOME/bokf-demo/daemon.log 2>&1 &` - The user's own early instance was `/tmp/x.db` + `/tmp/bokfd.sock` (schema v1, old binary) — recreate or migrate it with the current build if it is still wanted. - TUI smoke tests: drive over a pty with `script -qec`; function-key escape sequences are timing-sensitive there (not an app bug). Arrows arrive as application-mode sequences (`ESC O B` for Down), not `ESC [ B`, because curses enables the keypad. `Ctrl+N/C/F` are single bytes and reliable. Always wrap the run in `scripts/tui-sandbox.sh -- ./build/bokftui ...`: it isolates `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` so a test can never overwrite the real `~/.config/bokf/tui.conf` or `~/.cache/bokf/tui.log`. ## Known caveats - Saving with Ctrl+Enter needs a terminal that speaks xterm `modifyOtherKeys` level 2 or the Kitty keyboard protocol; gnome-terminal/VTE sends neither, so the hints advertise `F9`, which works everywhere. - Developer tooling: the `g_cmd_[]` tables in `src/cmd_*.c` carry declarative argument schemas (`CMD_ARGS`); `describe` emits them and the dispatcher validates before the handler runs. `make check` (part of `make test`) fails when a command or error code is missing from `PROTOCOL.md` (`scripts/check-consistency.sh`) or when the generated command catalogue is stale (`make gen-protocol`). `make test-asan`/ `test-ubsan` build `test_core` with sanitizers; `make test-pty` runs `scripts/tui-golden.py` against a throwaway `/tmp` daemon; `make gate` is the pre-push check (clean `-Werror` build in `build-gate/` + tests + ASan). - Never commit unless the human asks. - SQLite files must not be backed up live with restic; use `backup.snapshot` (`VACUUM INTO`) and point restic at the snapshots. - Schema version is 12 (v3 moms rules; v4/v6 year info; v5 org description/shares + board members; v7 attachments append-only triggers; v8 bank reconciliation; v9 invoicing + widened `vouchers.source` with a table rebuild; v10 payroll + `payroll`/`payroll_tax` sources, same rebuild; v11 `employees.email`; v12 `invoice_rows.is_text` and `invoices.paid_date`/`payment_voucher_id`); forward migrations are in `db.c`. ## Makandra driftstatus (org 2) - **Org**: Makandra AB, org 2. Räkenskapsår (id): 2022=3, 2023=4, 2024=5, 2025=6, 2026=7, **2027=2 (öppet, aktuellt)**. Bokslut/AR/deklaration görs för det år som är valt i sessionen. - **FK2027**: importerade Kapitas-böcker + 30 bokförda verifikat (V21–V50) för bank/skatt maj–sep 2026: V49 makulerar en dubblett (V20) och V50 är Hetzner-förskottet (nedan). 1930 stämmer mot banken utom **CDON 2 409 kr** (väntar på kvittots del 2–4; bokförs när det kommer) och Hetzner-kortköpet 2026-09-21 som ännu inte är importerat. 1630 = 40 721 (exakt enligt Skatteverket). - **Hetzner-förskott (2026-09-22)**: V50, datum 2026-09-21 (bankens bokföringsdatum), "Förskott Hetzner 100 EUR": **D 1790** 1 156,39 / **K 1930** 1 156,39, med båda PDF:erna som underlag. Matchas mot banken när nästa SEB-fil (efter 2026-09-18) importeras. Förbrukningen bokförs när Hetzner-fakturorna kommer (se reglerna nedan). - **Underlag**: 309 attachment i org 2 (alla historikdokument + insamlade underlag, inkl. de två Hetzner-PDF:erna). Bank-/SKV-utdrag ligger i `~/Makandra AB/{bank,skatteverket}` (Syncthing), källkorpus i `~/Downloads/Makandra AB-…/Bokföring/`. - **Stängning**: 2022–2026 ska stängas av ägaren via **Räkenskapsår** i TUI:n; låt FK2027 vara öppen till nästa bokslut. - **Deklaration**: FK2026 är deklarerad av revisorn. FK2027 deklareras våren 2027 (INK2/SRU via Bokslutshubben → Inkomstdeklaration). - **Årshäftet**: fylls i Bokslutshubben (händelser, stämma, utdelning + datum, medelantal, noter). OBS: `dividend_ore` för FK2027 kan vara ett testvärde (10 000) — kontrollera före AR/deklaration. - **Beslut/regler från bokföringsarbetet**: inga bokföringar utan godkännande; låsta år förblir låsta (rättelser görs i aktuellt år); SIE- importören ändras inte och importerad data "manipuleras" inte; historikårens P&L nettar noll pga källsystemets stängningar (AR hoppar över "Stäng"-verifikat i flerårsöversikten); utdelning bokförs vid stämman med mallen **Utdelning** (D 2099/K 2898); pappersoriginal finns i fysisk pärm (får refereras i efterhand, även i stängda år); leverantörsförskott och förbrukningssaldo (Hetzner) bokförs som **D 1790/K 1930** vid påfyllning och **D 6540 + D 2645 / K 2614 / K 1790** vid förbrukning; kortköp bokförs på **bankens bokföringsdatum** (lättare att härleda och matcha vid import). - **Lön (2026-09-21)**: bara ägaren är anställd, men schema och kommandon är fleranvändarklara. Lönemotorn (schema v11: anställda, skattetabeller, lönekörningar, lönebesked + mejl) och TUI-sektionen Lön är klara, men **ingen anställd är registrerad än** (personnummer, lön, skattetabell och kolumn fylls i under Register → Anställda). Konton enligt historiken: 7210/7510/2710/2731/1630. Skattetabellerna hämtas med en knapp från Skatteverket (varning när året är gammalt), personnumret är krypterat i databasen. Ingen semesteruppbokning; arbetsgivardeklarationen (AGI) görs manuellt med underlaget från `payroll.agi`.