summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-21 23:09:57 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-21 23:09:57 +0200
commit8350815a8c6987b288551848eda1c678739c8d4a (patch)
tree04ef3c56e10fa8b06768465910d10a57779e8731 /docs
parentbf9012dad7ef34bbbfb073bc0f37b0161f0496f3 (diff)
downloadbokf-0.1.62.tar.gz
bokf-0.1.62.zip
invoices: text rows, duplicate and payment link (schema v12); smtp address checksv0.1.62
Diffstat (limited to 'docs')
-rw-r--r--docs/DECISIONS.md14
-rw-r--r--docs/INVOICING.md21
-rw-r--r--docs/PROTOCOL.md56
-rw-r--r--docs/SCHEMA.md12
-rw-r--r--docs/TUI-GUIDELINES.md1
5 files changed, 80 insertions, 24 deletions
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
index b52f8ec..125d19b 100644
--- a/docs/DECISIONS.md
+++ b/docs/DECISIONS.md
@@ -213,6 +213,20 @@ kept verbatim from the STATE.md they were pruned from (2026-09-21).
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.
## Completed work formerly listed under "Pending decisions"
diff --git a/docs/INVOICING.md b/docs/INVOICING.md
index 7c6ce2e..a04c869 100644
--- a/docs/INVOICING.md
+++ b/docs/INVOICING.md
@@ -206,6 +206,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 +216,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 +236,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 +245,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/PROTOCOL.md b/docs/PROTOCOL.md
index 81eeb67..75f1990 100644
--- a/docs/PROTOCOL.md
+++ b/docs/PROTOCOL.md
@@ -396,10 +396,11 @@ characters. Verification ids are the concatenation of series and number
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
@@ -591,10 +592,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`:
@@ -617,6 +619,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
@@ -653,6 +662,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,
@@ -863,6 +881,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` |
@@ -921,16 +940,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`
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/TUI-GUIDELINES.md b/docs/TUI-GUIDELINES.md
index 433b0b2..c81b3a7 100644
--- a/docs/TUI-GUIDELINES.md
+++ b/docs/TUI-GUIDELINES.md
@@ -30,6 +30,7 @@ there.
| `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 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") |