diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/DECISIONS.md | 70 | ||||
| -rw-r--r-- | docs/INVOICING.md | 38 | ||||
| -rw-r--r-- | docs/PAYROLL.md | 8 | ||||
| -rw-r--r-- | docs/PROTOCOL.md | 115 | ||||
| -rw-r--r-- | docs/SCHEMA.md | 12 | ||||
| -rw-r--r-- | docs/STATE.md | 160 | ||||
| -rw-r--r-- | docs/TUI-GUIDELINES.md | 131 |
7 files changed, 461 insertions, 73 deletions
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index a108f48..5499610 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -201,7 +201,65 @@ kept verbatim from the STATE.md they were pruned from (2026-09-21). employed now but everything is multi-employee; no semester accrual; tax tables from day one; manual step-buttons (Bokför, Lönebesked, AGI-underlag, Betala skatt & avgifter); bank phase 3 (`bank_rule.*`) is - dropped. + dropped. +25. **Per-feature voucher series (2026-09-21)**: the single standardserie + setting is replaced by one setting per feature that posts vouchers: + `series_voucher` (manual vouchers and new templates, default `A`, + falling back to the legacy `default_series`), `series_invoice` (`F`), + `series_payroll` (`L`), `series_bokslut` (`Å`) and `series_ib` (`IB`). + All are editable (Bolaget → Verifikationsserier), 1–8 characters with + no control characters. The manual voucher form remembers the last + series it posted as `series_voucher`. Reports and SIE treat both the + configured `series_ib` and the historical `IB` series as ingående + balans, so old books keep working. "Nästa fakturanummer" is editable + in Bolaget → Fakturauppgifter (owner only). +26. **Invoicing follow-ups (2026-09-21)**: schema v12 adds + `invoice_rows.is_text` and `invoices.paid_date`/`payment_voucher_id`. + Invoices accept **text rows** (`"text": true`): description only, no + amount, excluded from totals and the posting voucher; at least one + priced row is required. The invoice detail gets `u = duplicera` (same + customer, rows and references, dates reset to today, due = today + + payment days) and `b = kvittera betalning`, which prefills the ordinary + voucher form (D `bank_account`, K `invoice_receivable_account`, both + editable, underlag attachable) and, after posting, calls + `invoice.pay`, which requires the voucher to credit the receivable with + exactly the invoice total. Partial payments are out of scope. Lists + show `betald <datum>`. SMTP: `smtp_from`/`smtp_reply_to` are validated + as e-mail addresses (settings.set and mail config), with the sender's + display name taken from the org name. +27. **Document header (2026-09-22)**: the invoice header bar no longer + draws the `MAKANDRA AB` Comfortaa outline; it prints the organization + name (`orgs.name`) in Helvetica-Bold, scaled down and truncated with + `...` if it would reach the `FAKTURA` wordmark, which stays. The shared + setting `document_header_color` (`#rrggbb`, default `#314c59`) colours + the header bar of **every** generated document — currently the invoice + and the lönebesked; `settings.set` refuses other values and the + renderers fall back to the default if a stored value is invalid. The + lönebesked prints the employer name in its header too, so the + `MAKANDRA AB` outlines are unused at runtime (kept in the generated + header). Images remain out of scope. +28. **TUI interaction model (2026-09-22, design)**: settled in the UX + session; the spec is `TUI-GUIDELINES.md` "Interaction model" and is not + implemented yet. Two focus modes: the navigation keys (`Tab`, arrows, + `Home`/`End`, `PgUp`/`PgDn`) only move focus/selection/scroll and never + mutate data, while an active field keeps caret semantics. `Enter` + activates the focused item only when it owns an action (menu item, + action row, opening list row); a plain report has none, so `Enter` does + nothing there — it never saves a whole form, deletes or posts. Entities + have one of three lifecycles: **register** (explicit `Spara` after + validation; drafts live in memory and in + `$XDG_CACHE_HOME/bokf/drafts.json`, marked `<UTKAST>`, deletable from + the list and from the editor), **document** (the form is the draft, + explicit post, immutable) and **settings** (explicit `Spara`; the + per-field autosave is dropped). Every savable form ends with a `Spara` + (or `Posta`) action row; commit is blocked while invalid and a server + error keeps the draft. Screens declare actions in one ordered + `struct tui_action` list that drives the `F2` menu, the accelerator keys + and the footer hints; `F2` only, no `§`; `Ctrl+Enter` is dropped as a + commit key (`F9` and the save action row remain). **Kunder pilot + implemented 2026-09-22** (`clients/drafts.[ch]`, `<UTKAST>`, the `Spara` + row, the `F2` menu, delete from list and editor); the other screens + follow. ## Completed work formerly listed under "Pending decisions" @@ -275,7 +333,15 @@ kept verbatim from the STATE.md they were pruned from (2026-09-21). statement (Kapitas 2022-2026). **Decided 2026-09-19: no importer change.** Locked years stay locked and the source's closings stay in the books; the årsredovisning export flags incomplete jämförelsetal for those years and - points to the previous year's annual report. + points to the previous year's annual report. **Amended 2026-09-22**: the + income statement (and therefore INK2/SRU and the TUI resultatrapport) now + skips the same "Stäng ..." vouchers when they are SIE-imported, so a year + being declared shows its real figures; the balance sheet keeps them (the + result sits in 2099 and must not be counted twice). The source's own + #IB/#UB corrections that the vouchers do not reproduce (e.g. Makandra's FY + 2022/2023 result transfer is 17 857.63 kr short of that year's P&L) remain + a data divergence: the derived balance sheet is off by that amount until a + correction is booked in the current year. 11. ~~SIE import only into an empty fiscal year; consider broader import.~~ Chronological multi-year import works (CRLF, `#RAR 0`, zero rows, `#IB` rule handled); each year must still target an empty fiscal year. Note: diff --git a/docs/INVOICING.md b/docs/INVOICING.md index 7c6ce2e..de584cf 100644 --- a/docs/INVOICING.md +++ b/docs/INVOICING.md @@ -14,7 +14,8 @@ the generated PDF reproduces the existing document. - Customer register (name, address, momsreg.nr, e-mail, er referens, payment terms), owner-editable and audited. - One-page invoice document generated by bokfd, visually matching the - existing Google Sheets export (same grid, colours and wordmark). + existing Google Sheets export (same grid, colours and `FAKTURA` wordmark; + the header shows the organization name). - A configurable, always-increasing invoice number series per org, plus an OCR reference that Bankgiro accepts. - Issue in one action: number + PDF (stored as an immutable attachment) + @@ -49,13 +50,19 @@ Colours: | Body text | `#314c59` | | Header/table text on the bar | `#ffffff` | +The header bar's colour is the shared document setting +`document_header_color` (`#rrggbb`, default `#314c59`, also used by the +lönebesked). + Fonts: - Body: Helvetica (PDF base-14, metrically compatible with Arial); no embedding. -- Wordmark `MAKANDRA AB` and `FAKTURA`: Comfortaa Bold (SIL OFL) as - pre-generated vector outlines, drawn as filled paths. No font file or - TrueType machinery at runtime. +- Wordmark `FAKTURA`: Comfortaa Bold (SIL OFL) as pre-generated vector + outlines, drawn as filled paths; no font file or TrueType machinery at + runtime. The header's left side prints the organization `name` in + Helvetica-Bold, scaled down and truncated with `...` if it would reach + `FAKTURA`. Grid (points, origin top-left; refined against the originals in `tests/` golden comparisons): @@ -63,7 +70,7 @@ Grid (points, origin top-left; refined against the originals in | Element | x | y | |---|---|---| | Header bar (x 17.3–577.7) | 17.3 | 53.3–75.7 | -| Wordmark `MAKANDRA AB` (ink left/baseline) | 21.74 | 69.14 | +| Header name (org `name`, ink left/baseline) | 21.74 | 69.14 | | `FAKTURA` (ink right/baseline) | 576.87 | 69.14 | | Info labels (bold 7.285 pt), right-aligned | 113.98 | 101.11 + 14.71/row | | Info values (9.107 pt), left-aligned | 118.87 | same rows | @@ -206,6 +213,8 @@ CREATE TABLE invoices ( CHECK (status IN ('issued','credited')), document_id INTEGER, voucher_id INTEGER, + paid_date TEXT NOT NULL DEFAULT '', + payment_voucher_id INTEGER, last_sent_at TEXT, last_sent_to TEXT, created_at TEXT NOT NULL, @@ -214,7 +223,9 @@ CREATE TABLE invoices ( UNIQUE (org_id, number), FOREIGN KEY (org_id, customer_id) REFERENCES customers(org_id, id), FOREIGN KEY (org_id, document_id) REFERENCES attachments(org_id, id), - FOREIGN KEY (org_id, voucher_id) REFERENCES vouchers(org_id, id) + FOREIGN KEY (org_id, voucher_id) REFERENCES vouchers(org_id, id), + FOREIGN KEY (org_id, payment_voucher_id) + REFERENCES vouchers(org_id, id) ) STRICT; CREATE TABLE invoice_rows ( @@ -232,6 +243,7 @@ CREATE TABLE invoice_rows ( vat_code TEXT NOT NULL DEFAULT '25' CHECK (vat_code IN ('25','12','6','0','rc','eu')), account TEXT NOT NULL DEFAULT '', + is_text INTEGER NOT NULL DEFAULT 0, UNIQUE (org_id, id), UNIQUE (org_id, invoice_id, line_no), FOREIGN KEY (org_id, invoice_id) REFERENCES invoices(org_id, id) @@ -240,7 +252,19 @@ CREATE TABLE invoice_rows ( `vouchers.source` gains `invoice` (and later `credit`): the CHECK constraint must be widened. `invoice_rows` are written once at issue; `invoices` only -changes `status`, `last_sent_*` and (later) credit links. +changes `status`, `last_sent_*`, `paid_date`/`payment_voucher_id` and (later) +credit links. Schema v12 adds `invoice_rows.is_text` and the two payment +columns with forward `ALTER TABLE`s; the composite foreign key on +`payment_voucher_id` exists in fresh databases only (SQLite cannot add one +later), and `invoice.pay` validates the reference in code either way. + +A **text row** (`is_text`) is a free-text line in the table: only +`description` is meaningful, it has no quantity, unit, price or VAT and +contributes nothing to the totals or the posting voucher. It renders in the +description column only. Every invoice still needs at least one priced row. +When an invoice is marked **paid** (`invoice.pay`), `paid_date` is the +payment voucher's date and `payment_voucher_id` links it; partial payments +are not modelled. Customer seed: Andra bygg AB (Solna, SE559232855201, Eric Lejeby, 30), NZ Bygg AB (Bromma, SE559264837101, Valentyne Schnelle, 30), diff --git a/docs/PAYROLL.md b/docs/PAYROLL.md index b78aeba..ebf9272 100644 --- a/docs/PAYROLL.md +++ b/docs/PAYROLL.md @@ -216,9 +216,11 @@ the base is the gross. ## 6. Documents and TUI - **Lönebesked** (wave 2, done): `payroll.payslip` renders one A4 page per - employee and run with the invoice's visual language — dark `#314c59` - header bar, employer and employee blocks, gross, preliminary tax (shown - negative), net and the employer-contribution note; the personnummer is + employee and run with the invoice's visual language — the employer name in + a header bar in the shared document colour `document_header_color` + (default `#314c59`), employer and employee blocks, gross, preliminary tax + (shown negative), net and the employer-contribution note; the personnummer + is masked except the last four. `payroll.payslip_mail` stores the PDF as an `application/pdf` attachment on the run's voucher, links it with `voucher_attachments` and e-mails it through the org's `smtp_*` settings diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 76197d9..0b69d53 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -374,26 +374,36 @@ resolved rows in a dry run. | Command | Args | Notes | |---|---|---| | `settings.get` | — | effective org settings (defaults included); secret values replaced by `_set` flags | -| `settings.set` | `key`, `value?` | known keys: `default_series`, `attachment_dir`, `bank_account`, `invoice_receivable_account`, `invoice_revenue_account`, `invoice_bankgiro`, `invoice_our_ref`, `smtp_host`, `smtp_port`, `smtp_user`, `smtp_from`, `smtp_reply_to`, `smtp_security`, `smtp_password` | +| `settings.set` | `key`, `value?` | known keys: `default_series`, `series_voucher`, `series_invoice`, `series_payroll`, `series_bokslut`, `series_ib`, `attachment_dir`, `bank_account`, `invoice_receivable_account`, `invoice_revenue_account`, `invoice_bankgiro`, `invoice_our_ref`, `document_header_color`, `smtp_host`, `smtp_port`, `smtp_user`, `smtp_from`, `smtp_reply_to`, `smtp_security`, `smtp_password` | -`default_series` (1–8 characters, e.g. `A`, `V-`, `A `) is used when -`voucher.post` carries no `series` and as the default series for new -templates. `attachment_dir` (a path, up to 255 characters) is accepted for +`series_voucher`, `series_invoice`, `series_payroll`, `series_bokslut` and +`series_ib` (1–8 characters, e.g. `A`, `V-`, `Å`) are the number series each +feature posts into: manual vouchers, `invoice.issue`, payroll runs and +payments, `bokslut.post` and ingående balans. They default to `A`, `F`, `L`, +`Å` and `IB`; `series_voucher` also falls back to the legacy +`default_series` (1–8 characters) when unset, and new templates default to +it. Reports and SIE count both the configured `series_ib` and the +historical `IB` series as ingående balans. `attachment_dir` (a path, up to +255 characters) is accepted for compatibility, but the TUI file browser now remembers the directory of the last picked attachment client-side. `bank_account` (digits only, up to 10 characters, default `1930`) is the account `bank.import` uses when the request carries no `account`. `invoice_receivable_account` (default `1510`) and `invoice_revenue_account` (default `3001`) are the receivable and default revenue account of invoice postings, digits only, up to 10 -characters. Verification ids are the concatenation of series and number +characters. `document_header_color` (`#rrggbb`, default `#314c59`) is the +background colour of the generated documents' header bars (invoice and +lönebesked); the invoice prints the org name on it. Verification ids are the +concatenation of series and number (`V-8`), and series are free-form: only an unbroken numbering per series is required. `smtp_host` (up to 255 characters, no control characters), `smtp_user` (up to -255), `smtp_from` and `smtp_reply_to` (up to 254), `smtp_port` (digits, -1–65535) and `smtp_security` (`starttls`, `tls` or `plain`, default -`starttls` when unset) configure the outgoing mail used when invoices are -sent. +255), `smtp_from` and `smtp_reply_to` (up to 254) must be e-mail addresses +(one `@`, no spaces), `smtp_port` (digits, 1–65535) and `smtp_security` +(`starttls`, `tls` or `plain`, default `starttls` when unset) configure the +outgoing mail used when invoices are sent. The sender's display name is the +organization name; `smtp_from` is the address. `smtp_password` is a secret setting. `settings.set` encrypts the value with AES-256-GCM under the key in the `BOKFD_SECRET_KEY` environment variable (32 @@ -441,7 +451,18 @@ linked to that voucher and each item carries that `voucher_id`. | `sru.export` | `fiscal_year`, `adjustments?`, `submitter?`, `assisted?`, `audited?`, `ignore_unmapped?` | `INFO.SRU` + `BLANKETTER.SRU` (ISO-8859-1, base64) | All reports are pure reads, respect locks, and return JSON rows. Amounts are -öre. `report.general_ledger` (huvudbok) returns account blocks: +öre. IB (ingående balans) for a balance account (asset, liability, equity) is +all earlier history, including earlier years' `IB` vouchers, plus this +year's `IB` vouchers; a P&L account (revenue, expense) restarts at zero at +every fiscal-year start, so its IB is only, with a narrowed `from`, the +year's movements before it; `IB` rows on P&L accounts (the balancing side of +an opening-balance adjustment) are ignored. +`report.income_statement` (and therefore `sru.export` and the TUI +resultatrapport) ignores the source system's `"Stäng ..."` closing vouchers +in SIE-imported years (`source:"sie_import"`), where the P&L accounts are +closed straight to 2099 and would otherwise net to zero; the TUI +årsredovisning uses the same rule. `report.general_ledger` (huvudbok) +returns account blocks: `{"fiscal_year","from","to","last_voucher":{...},"accounts":[{"account", "name","ib_ore","debit_ore","credit_ore","ub_ore","rows":[{"series", "number","date","description","row_description","debit_ore","credit_ore", @@ -472,7 +493,10 @@ like the blankett; box 48 positive as filed) and returns `{"org_nr", bytes are ISO-8859-1, so write them verbatim to a `.xml` file. Both cover the whole fiscal year; the ledger's period can be narrowed with `from`/`to`. `report.vat` returns `{"from","to","boxes":[{"box":"05","label":"...","amount_ore":...}],"note"}`. -Rules sharing a box are summed into a single entry. `box 49` is the sum of +Rules sharing a box are summed into a single entry. The momsomföring +itself (any voucher with a row on 2650) and SIE-imported `"Stäng ..."` year +closings are left out, so a period that includes its own VAT settlement +still reports the underlying boxes. `box 49` is the sum of the moms boxes (`10`,`11`,`12`,`30`,`31`,`32`,`48`,`60`,`61`,`62`), so box 48 is signed like the blankett (ingående moms negative); underlag boxes do not change what is payable. @@ -509,7 +533,8 @@ support `dry_run`, which validates without writing. | `sie.import` | `content_base64` or `path`, `dry_run?` | one file per call; creates missing accounts and posts #VER as `source:"sie_import"`; only into an empty org fiscal year; `#IB` becomes an `IB` voucher when the year has no earlier history, otherwise the earlier vouchers carry the balances | SIE 4 files are written in CP437 with PC8 format, `#SIETYP 4`, `#FNR`, `#ORGNR`, -`#KONTO`, `#IB`, `#UB`, `#RES`, `#VER`, `#TRANS`. Import is the migration path +`#KONTO`, `#IB`, `#UB`, `#RES`, `#VER`, `#TRANS`. `#IB`/`#UB` are written for +balance accounts and `#RES` for P&L accounts, with IB as in §7.6. Import is the migration path from Fortnox/Visma/BL and must be dry-run first; it reports exactly what would be created. @@ -585,10 +610,11 @@ removes one link and is a `NOT_FOUND` when it does not exist. Both mutate | `invoice.sequence_set` | `next_number` (owner) | `next_number` | | `invoice.preview` | draft (below) | `content_base64`, `number`, `ocr`, `net_ore`, `vat_ore`, `total_ore` | | `invoice.issue` | draft, `dry_run?` | `id`, `number`, `ocr`, `document_id`, `voucher_id`, totals | -| `invoice.get` | `id` | header, `rows[]`, `document_id`, `voucher_id`, `last_sent_at`, `last_sent_to` | -| `invoice.list` | `customer_id?`, `status?` (`issued`/`credited`), `limit?` | `items[]`, newest first | +| `invoice.get` | `id` | header, `rows[]`, `document_id`, `voucher_id`, `paid_date`, `payment_voucher_id`, `last_sent_at`, `last_sent_to` | +| `invoice.list` | `customer_id?`, `status?` (`issued`/`credited`), `limit?` | `items[]`, newest first, with `paid_date` | | `invoice.pdf` | `id` | stored PDF as `content_base64` | | `invoice.send` | `id`, `to?` | `id`, `sent_to`, `at`; `dry_run` returns `to`, `subject` | +| `invoice.pay` | `id`, `voucher_id`, `dry_run?` | `id`, `number`, `paid_date`, `payment_voucher_id`, `voucher_series`, `voucher_number` | The draft object is the argument set shared by `invoice.preview` and `invoice.issue`: @@ -611,6 +637,13 @@ accounts are `ACCOUNT_NOT_FOUND`/`ACCOUNT_INACTIVE`. The customer must exist and be active (`NOT_FOUND`). A draft whose rows do not fit the single page is rejected with `TOO_LARGE`. +A row with `"text": true` is a free-text line: only `description` is used, +it has no quantity, unit, price or VAT, contributes nothing to the totals +and only prints its description in the document. Every draft still needs at +least one priced row (`INVALID_ARGS` otherwise, "invoice total must be +greater than zero"). `invoice.get` and `invoice.list` return rows with +`is_text`. + Numbering is a per-org, global series: `invoice_sequence.next_number` starts at 1, is set by the owner and is incremented by exactly one per issued invoice. The OCR reference is the number followed by its MOD10 (Luhn) check @@ -629,7 +662,9 @@ is the idempotency key). `invoice.sequence_set` and `invoice.issue` are audited; `invoice.issue` supports `dry_run`, which validates and renders but takes no number and writes nothing. `invoice.pdf` returns the stored document as base64 -(`JVBERi0` after decoding is the PDF magic). When the setting +(`JVBERi0` after decoding is the PDF magic). The document's header bar +prints the organization `name` and is coloured by `document_header_color` +(default `#314c59`; invalid stored values fall back to it). When the setting `invoice_bankgiro` is present it is printed in the document's Bankgiro field; `invoice_our_ref` (up to 64 characters) prefills the invoice form's "Vår referens". @@ -647,6 +682,15 @@ updated and the `invoice.send` audit entry stores `{id,to,subject}` only. `dry_run` validates configuration, recipient and stored document and returns the recipient and subject without sending or updating anything. +`invoice.pay` links a payment voucher (created by the client, normally from +the TUI's **Kvittera betalning** action, which prefills debit `bank_account` +and credit `invoice_receivable_account` in the ordinary voucher form) and +stamps `paid_date` with the voucher's date. The voucher must credit the +`invoice_receivable_account` (default `1510`) with exactly the invoice +total, else `INVALID_ARGS`; an already paid invoice and a `credited` one are +rejected (`CONFLICT` and `INVALID_ARGS`). `dry_run` validates without +writing, and the command is audited. + ### 7.11 Anställda (employees) The employee register. `personal_no` is checked for shape (10 or 12 digits, @@ -732,7 +776,8 @@ given date (default: today) and sets the run's `status` to `paid` with `payment_voucher_id`; paying twice is a `CONFLICT`. `payroll.payslip` renders one A4 lönebesked for an employee line of a posted -run: the employer header and footer, the employee name, the masked +run: the employer name in the header bar (coloured by +`document_header_color`), the employer footer, the employee name, the masked personnummer (`********-1234`, or `********` when the key is unavailable), period, pay date, tax table and column, then Bruttolön, the negative Preliminärskatt, a rule and Nettolön, and the note `Arbetsgivaravgifter @@ -857,6 +902,7 @@ Args: `name:type(values)[!][=default]`, `!` = required. | `invoice.list` | viewer | yes | no | no | `customer_id:int`, `status:enum(issued\|credited)`, `limit:int=200` | | `invoice.pdf` | viewer | yes | no | no | `id:int!` | | `invoice.send` | bookkeeper | yes | yes | yes | `id:int!`, `to:string` | +| `invoice.pay` | bookkeeper | yes | yes | yes | `id:int!`, `voucher_id:int!` | | `employee.list` | viewer | yes | no | no | `active_only:bool` | | `employee.get` | viewer | yes | no | no | `id:int!` | | `employee.create` | bookkeeper | yes | yes | yes | `name:string!`, `personal_no:string!`, `address:string`, `postal_code:string`, `city:string`, `bank_account:string`, `email:string`, `salary_account:string`, `monthly_salary_ore:int=0`, `tax_table:int=30`, `tax_column:int=1` | @@ -902,7 +948,10 @@ commands. Implemented screens (0.1.0-dev): - **Mallar** — list, create and edit templates in the same form style as vouchers (Tab, dynamic rows, F7 clear row, F5 validate, F9 save); archive keeps the template but hides it from the list. -- **Underlag** — inbox of unlinked attachments; `a` uploads a file. +- **Underlag** — inbox of unlinked attachments; `a` uploads a file and + Enter opens **Granska** (text in a pager, other files in the desktop + viewer) or **Ladda ned…**; the voucher detail's underlag list (`f`) works + the same way. - **Bankavstämning** — imported bank transactions (`bank.import`) matched against vouchers on the bank account, with suggestions; Enter matches the suggested voucher (or picks another), `u` unmatches, `a` imports a SEB CSV. @@ -912,16 +961,21 @@ commands. Implemented screens (0.1.0-dev): returns to the list. A failed auto-match keeps the posted voucher and shows the server error. - **Fakturor** — invoice list (`invoice.list`, newest first) with number, - date, customer, total and status (`utfärdad`/`krediterad`). Ctrl+N opens - the form, Enter the detail. The form has the customer picker, invoice/due - (due defaults from the customer's payment days) and delivery dates, er/var - referens and rows (beskrivning, antal, enhet, à-pris, moms, anm); `F5` - previews the real PDF (`invoice.preview`, nothing stored, no number - consumed), `Ctrl+Enter` issues (`invoice.issue`) and then asks - "Skicka faktura <nr> till <e-post>?". The detail shows header and rows; - `p` fetches the stored PDF (`invoice.pdf`) and `s` sends it - (`invoice.send`). In the list, `n` sets the next invoice number - (`invoice.sequence_get`/`sequence_set`, owner-only). + date, customer, total and status (`utfärdad`/`krediterad`/`betald + <datum>`). Ctrl+N opens the form, Enter the detail. The form has the + customer picker, invoice/due (due defaults from the customer's payment + days) and delivery dates, er/var referens and rows (beskrivning, antal, + enhet, à-pris, moms, anm); a row with only beskrivning is a free-text line + (`text` rows, no amount). `F5` previews the real PDF (`invoice.preview`, + nothing stored, no number consumed), `F9` issues (`invoice.issue`) and + then asks "Skicka faktura <nr> till <e-post>?". The detail shows header + and rows and offers `p = visa PDF` (`invoice.pdf`), `s = skicka` + (`invoice.send`), `u = duplicera` (a new draft with the same rows and + today's dates) and, on unpaid invoices, `b = kvittera betalning`: a + prefilled payment voucher (debit `bank_account`, credit + `invoice_receivable_account`) is opened in the ordinary voucher form and, + once posted, linked with `invoice.pay`. In the list, `n` sets the next + invoice number (`invoice.sequence_get`/`sequence_set`, owner-only). - **Kunder** — the customer register (name, address, postal code, city, VAT number, e-mail, your reference, payment days, notes). Ctrl+N creates, Enter edits (F5 validates with a dry run, Ctrl+Enter saves), `d` @@ -963,10 +1017,11 @@ commands. Implemented screens (0.1.0-dev): - **Bolaget** — the dashboard's hub for the org's master data: **Företagsuppgifter** (name, org number, VAT number, address, e-mail, phone, moms period, framework, fiscal-year start month), editable in - place by owners, others see it read-only; **Fakturauppgifter** - (standardserie, fordringskonto, intäktskonto, bankgiro, vår referens) and + place by owners, others see it read-only; **Fakturauppgifter** (nästa + fakturanummer (owner), fordringskonto, intäktskonto, bankgiro, vår + referens); **Verifikationsserier** (the per-feature series); and **E-post (SMTP)** (host, port, user, sender, reply-to, security, - password), both `settings.set` and open to bookkeepers; + password), the last three `settings.set` and open to bookkeepers; **Styrelseledamöter**; and the registers **Anställda**, **Kunder** and **Momsregler**. - **System** — the hub with **Skattetabeller** and **Revision**. The file diff --git a/docs/SCHEMA.md b/docs/SCHEMA.md index 9818e1c..9199691 100644 --- a/docs/SCHEMA.md +++ b/docs/SCHEMA.md @@ -560,12 +560,12 @@ another voucher is posted in between) — clients must not persist it. ## 12. Migrations and versioning - `meta(key TEXT PRIMARY KEY, value TEXT)` holds `schema_version` (integer) - and `created_at`. Current version: **11** (v11 adds the employee e-mail, - v10 adds the payroll tables and the `payroll`/`payroll_tax` voucher - sources, v9 adds the invoicing tables and `invoice`, v8 the two bank - reconciliation tables, v7 makes attachments append-only, v3 replaces the - seeded moms rules with the corrected mapping; v2 adds the two template - tables). + and `created_at`. Current version: **12** (v12 adds invoice text rows and + the invoice payment link, v11 the employee e-mail, v10 the payroll tables + and the `payroll`/`payroll_tax` voucher sources, v9 the invoicing tables + and `invoice`, v8 the two bank reconciliation tables, v7 makes attachments + append-only, v3 replaces the seeded moms rules with the corrected mapping; + v2 adds the two template tables). - Migrations are forward-only, applied automatically at daemon start, each in one transaction. Before the first migration statement a consistent `VACUUM INTO` snapshot is written to diff --git a/docs/STATE.md b/docs/STATE.md index cf2efc8..f481524 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -12,9 +12,22 @@ filing/year-end work remains. TUI is usable and exercised by `make test-pty` lönebesked); `make test` covers the server/protocol/ledger, the TUI widget unit tests and the docs consistency check. -## Resume here (2026-09-21) +## Resume here (2026-09-22) -- **Deployed**: `v0.1.58`, healthy on `nas` (Alpine runtime, static aarch64 +- **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 @@ -22,9 +35,87 @@ unit tests and the docs consistency check. 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). The live database migrated - v9 → v11 on the v0.1.55 startup with the automatic pre-migration snapshot - in `var/db/backup/`. `main` and the tags are pushed to `nas`. + 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 + `<UTKAST>` 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()`, `<UTKAST>`, `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. +- **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 @@ -34,15 +125,16 @@ unit tests and the docs consistency check. - **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 Företag and the Skattetabeller + 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, E-post (SMTP), Styrelseledamöter - and the registers Anställda, Kunder, Momsregler); **System** holds + (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` @@ -76,20 +168,28 @@ None open. Completed items that used to be listed here are archived in 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 follow-ups when needed: credit notes (`invoice.credit`), per-row - account in the invoice form, kundreskontra view. +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_<domain>.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/`<UTKAST>`/`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.58`, healthy on nas). +- **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 @@ -132,6 +232,10 @@ Original entries for the struck items are in `docs/DECISIONS.md`. ## 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_<domain>[]` 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 @@ -145,24 +249,34 @@ Original entries for the struck items are in `docs/DECISIONS.md`. - 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 11 (v3 moms rules; v4/v6 year info; v5 org +- 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`); forward migrations are in `db.c`. + 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 + 28 bokförda verifikat (V21–V48) - för bank/skatt maj–sep 2026, samt V49 som makulerar en dubblett (V20). - 1930 stämmer mot banken utom **CDON 2 409 kr** (väntar på kvittots del - 2–4; bokförs när det kommer). 1630 = 40 721 (exakt enligt Skatteverket). -- **Underlag**: 279 attachment i org 2 (alla historikdokument + insamlade - underlag). Bank-/SKV-utdrag ligger i `~/Makandra AB/{bank,skatteverket}` - (Syncthing), källkorpus i `~/Downloads/Makandra AB-…/Bokföring/`. +- **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 @@ -176,7 +290,11 @@ Original entries for the struck items are in `docs/DECISIONS.md`. 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). + 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 diff --git a/docs/TUI-GUIDELINES.md b/docs/TUI-GUIDELINES.md index 190a5c5..1c201aa 100644 --- a/docs/TUI-GUIDELINES.md +++ b/docs/TUI-GUIDELINES.md @@ -4,6 +4,124 @@ Rules for the ncurses client so every view behaves the same. When in doubt, copy the behaviour of the voucher list / voucher form; they are the reference implementations. Inspired by Midnight Commander, htop, mutt and calcurse. +The **Interaction model** section is the agreed target (settled 2026-09-22, +`DECISIONS.md` #28). The **Kunder** pilot is implemented: drafts in +`$XDG_CACHE_HOME/bokf/drafts.json`, `<UTKAST>` marking, the explicit `Spara` +row, the `F2` action menu and draft deletion work there. Every other screen +still follows the sections below; the interaction model wins where they +conflict as each screen moves over. + +## Interaction model (target, 2026-09-22) + +Every entity has **one lifecycle**, every form **one commit gesture**, and +every action **one declaration**. Nothing is written to the backend +implicitly. + +### Two focus modes + +The cursor is always in one of two modes: + +- **Navigation** — `Tab`/`Shift-Tab`, arrows, `Home`/`End`, `PgUp`/`PgDn` + only move focus, selection or the viewport. They never mutate data, never + save and never run an action. +- **Editing** — the focused field is reverse video with a caret. Inside a + field, `←`/`→`/`Home`/`End` move the caret, `Backspace`/`Del`/`Ctrl+U` + edit the text and `Up`/`Down` leave the field. `Enter` commits the field + and advances; `Esc` restores it (the scratch-copy semantics stay). + +`Enter` activates the focused item **when that item owns an action** (a menu +item, an action row, a list row that opens a detail). A report, a pager or a +blank area has no such item, so `Enter` does nothing there. `Enter` never +saves a whole form, never deletes and never posts. + +### Entity lifecycle + +| Class | Draft | Commit event | `Esc`/`q` | Examples | +|---|---|---|---|---| +| **Register** | in memory + local draft file | explicit `Spara` (the save action row, `F9`) after validation | back; the draft stays | customers, employees, templates, momsregler, org data | +| **Document** | the form is the draft | explicit `Posta` (the save action row, `F9`); immutable once written | back; confirm only when dirty | verifikat, fakturor, löneruns, bokslut | +| **Settings** | an edit buffer until `Spara` | explicit `Spara` | back | faktura/SMTP/serie-inställningar | + +- Every savable form ends with a visible commit action row (`Spara`, or + `Posta` for documents); `Enter` on the focused row commits (it is an item + with an action), `F9` is the accelerator. +- `Ctrl+Enter` is **dropped** as a commit key: gnome-terminal/VTE cannot + send it. The save action row and `F9` are the only commit gestures, which + makes every terminal behave the same. +- Commit is blocked while the data is invalid: the first invalid field is + focused and its error shown. The draft is untouched. A server error at + commit keeps the draft as well; only a successful command removes it. +- `Ctrl+R` (reload) and `Ctrl+C` never flush drafts to the backend; the + client-side draft file makes them survive both, so no work is lost. + +### Drafts + +- Every non-committed edit is a **draft**, held in memory and mirrored to + `$XDG_CACHE_HOME/bokf/drafts.json` (mode 0600, atomic replace) on every + change. This is the "nothing is ever lost" guarantee — `Ctrl+R`, a crash + and `Ctrl+C` included. +- A draft is keyed by `(org, entity, id)`; a new entity gets a temporary id + and shows up in its list immediately. +- Drafts are marked `<UTKAST>` directly after the row number in lists + (`3. <UTKAST> Namn`) and in the editor's frame title (`Kund <UTKAST>`). +- **Delete draft** is an action both on the list row (via `F2`) and inside + the editor, with a confirmation. It removes the memory and file draft and + never touches the backend. A successful commit removes the draft too. +- Drafts are client-local and never synced; another client sees the last + committed value. +- When the backend entity is gone at commit time (`NOT_FOUND`), the editor + offers "spara som ny" or "radera utkast". +- Review point: a draft of an encrypted field (an employee's personnummer) + puts plaintext in the cache file. 0600 is the same protection as the + Bitwarden session file; decide whether such fields are excluded from + drafts. + +### Actions and the `F2` menu + +Screens declare actions, never keys: + +```c +struct tui_action { + const char *id; /* stable, e.g. "customer.archive" */ + const char *label; /* Swedish UI text */ + int key; /* accelerator; 0 = menu only */ + int enabled; /* 1 runnable, 0 dimmed with a reason, -1 heading */ + const char *reason; /* why a disabled action is dim */ +}; +``` + +- One ordered action list per context drives everything: `F2` opens the + `Åtgärder` menu, the same list dispatches the accelerator keys and builds + the footer hint. A key can no longer exist outside the registry. +- The menu is sectioned: **Aktuell rad** (item actions), **Skärmen** (save, + delete draft, attach, …), **Globalt** (`F5` uppdatera, `Ctrl+R` ladda om, + …). Destructive actions are last and still ask for confirmation. +- `Enter` in the menu runs the highlighted action; disabled actions are dim + with their reason (as in `tui_form_action` today); `Esc` closes. The + actions that complex forms hide behind hotkeys today live here unchanged. +- The footer shows at most the two or three most important contextual + actions plus `F2 = fler`. Universal navigation keys (`Tab`, arrows, + `PgUp`/`PgDn`, `Home`/`End`) are not repeated there. +- The session's key decisions: letter accelerators stay, `F9` is the only + commit key (`Ctrl+Enter` is dropped) and `F2` only — no `§` binding (it is + not reliably encodable across terminals). + +### Implementation status + +1. Done: `struct tui_action`, `tui_action_menu()` and `tui_action_hint()` in + `clients/tui.[ch]`, unit-tested in `tests/test_tui.c`. +2. Partly done: the Kunder list builds its `F2` actions in a key hook and + appends `F2 = åtgärder` to the footer via `tui_list_hint_extra()`; other + lists have no actions yet, and `tui_rt` still uses its own key branches. +3. Done for Kunder: `clients/drafts.[ch]` (JSON store, atomic 0600 write, + temporary ids, `<UTKAST>` marking, delete action), unit-tested. +4. Done for Kunder: draft/`Spara` model with the `Spara` action row, + `F2` menu and draft deletion from both the list and the editor. The + other register screens and the settings forms (explicit `Spara`, no + per-field autosave) are next. +5. Done for Kunder: pty scenarios `customer-draft` and + `customer-draft-save` cover create, edit, `<UTKAST>`, delete and save. + ## Session start After login the org picker ("Välj organisation att representera") is always @@ -29,11 +147,12 @@ there. | `a` | Add/upload (Underlag) | | `c` | Correct (voucher detail) | | `d` | Delete/arkivera the selected row (only where the action exists; asks for confirmation) | -| `f` | Voucher detail: list the voucher's underlag — Enter fetches, `d` removes the link (asks first). Underlag: Enter fetches | +| `f` | Voucher detail: list the voucher's underlag — Enter opens Granska (text in a pager, PDFs/images in the desktop viewer) or Ladda ned…, `d` removes the link (asks first). Underlag: Enter does the same | +| `u` / `b` | Faktura detail: `u` duplicates the invoice into a new draft (same rows, dates reset to today), `b` (unpaid invoices) prefills and posts the payment voucher, then marks the invoice paid | | `Ctrl+F` | Attach a file via the file browser (voucher form and voucher detail) | | `k` | Underlag: link the highlighted attachment to a voucher picked from a list | | `Ctrl+X` | Clear the current row — only inside row editors (never "new") | -| `Ctrl+Enter` | Save/post the current form. Enabled via xterm `modifyOtherKeys` level 2 or the Kitty keyboard protocol; terminals that send neither keep `F9` working, and the hints show `^Enter/F9` | +| `Ctrl+Enter` | Save/post the current form. Needs xterm `modifyOtherKeys` level 2 or the Kitty keyboard protocol (xterm, kitty, foot, WezTerm); gnome-terminal/VTE sends neither, so the hints advertise `F9`, which works everywhere. The interaction model drops `Ctrl+Enter` entirely — don't add it to new views | Every screen prints its keys in the footer via `hints()`. If a key exists, the footer shows it; if the footer shows it, the key works. Control keys are @@ -43,7 +162,7 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width. - Rows are numbered `NN. text`, right-aligned so 2- and 3-digit numbers line up. - Verifikation ids are shown concatenated as `series+number` (`V-8`, `A8`), - using the org's `default_series` (Bolaget → Fakturauppgifter) for new + using the org's `series_voucher` (Bolaget → Verifikationsserier) for new vouchers. - The last row may be an action (e.g. `+ Nytt verifikat (Ctrl+N)`); selecting it runs the action instead of opening a detail view. @@ -235,10 +354,14 @@ only place that touches ncurses. Rules: ## Adding a view — checklist 1. Data comes from public protocol commands only. -2. Wrap the screen in `frame()`/`hints()`; return `Esc`/`q` to the parent. +2. Wrap the screen in `tui_frame()` and let the widgets carry the footer + hints; return `Esc`/`q` to the parent. 3. Use `tui_menu`/`tui_select_list` instead of writing a new loop; pass `allow_new`/`allow_refresh` so the universal keys apply. 4. Forms use the shared editor (`tui_edit_field`, `tui_prompt_into`, `tui_date_prompt_into`, `tui_amount_prompt_into`) and the row helpers. 5. Support `F5` if the data can change elsewhere. 6. Update `PROTOCOL.md` §8 and this file if you add a new key or interaction. +7. Declare the screen's actions in one `tui_action` list (once the + interaction model is implemented); dispatch, the `F2` menu and the footer + hint all read that list, so a key cannot exist without a visible action. |
