diff options
Diffstat (limited to 'docs/PROTOCOL.md')
| -rw-r--r-- | docs/PROTOCOL.md | 66 |
1 files changed, 64 insertions, 2 deletions
diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 3f8aaff..e02d2f7 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -371,14 +371,17 @@ resolved rows in a dry run. | Command | Args | Notes | |---|---|---| | `settings.get` | — | effective org settings (defaults included) | -| `settings.set` | `key`, `value` | known keys: `default_series`, `attachment_dir`, `bank_account` | +| `settings.set` | `key`, `value` | known keys: `default_series`, `attachment_dir`, `bank_account`, `invoice_receivable_account`, `invoice_revenue_account` | `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 the folder the TUI file browser opens in when attaching underlag. `bank_account` (digits only, up to 10 characters, default `1930`) is the account `bank.import` uses -when the request carries no `account`. Verification ids are the concatenation of series and number +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 (`V-8`), and series are free-form: only an unbroken numbering per series is required. @@ -546,6 +549,65 @@ removes one link and is a `NOT_FOUND` when it does not exist. Both mutate `bank_matches` only and are audited (`bank.match`, `bank.unmatch`); `bank.import` is audited as `bank.import`. +### 7.10 Invoicing (fakturering) + +| Command | Args | Result | +|---|---|---| +| `customer.list` | `active_only?` | `items[]` ordered by name | +| `customer.get` | `id` | one customer | +| `customer.create` | `name`; `address`, `postal_code`, `city`, `country`, `vat_nr`, `email`, `your_ref`, `notes`, `payment_days?` | the customer | +| `customer.update` | `id` plus any field (merged) | the effective customer | +| `customer.archive` | `id`, `active` | `id`, `active` | +| `invoice.sequence_get` | — | `next_number` (1 when no row) | +| `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.pdf` | `id` | stored PDF as `content_base64` | + +The draft object is the argument set shared by `invoice.preview` and +`invoice.issue`: + +```json +{"customer_id":2,"invoice_date":"2026-09-20","due_date":"2026-10-20", + "delivery_date":"2026-09-20","your_ref":"Lars","our_ref":"Anders", + "notes":"", "rows":[{"article_no":"","description":"Utvecklingsarbete", + "quantity":"61","unit":"tim","unit_price_ore":120000,"note":"", + "vat_code":"25","account":""}]} +``` + +`quantity` is a decimal string with at most three decimals (`61`, `61,5`, +`0,25`); it must be greater than zero. `amount_ore = +(quantity_milli * unit_price_ore + 500) / 1000` (round half up), and +`unit_price_ore` must be a non-negative integer. `vat_code` is one of `25`, +`12`, `6`, `0`, `rc`, `eu` (default `25`). An empty `account` uses the +setting `invoice_revenue_account` (default `3001`); unknown or inactive +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`. + +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 +digit: `OCR = <number><check>`. `invoice.preview` renders the document with +the next number but consumes nothing; `invoice.issue` takes the number, +renders the PDF, stores it as an immutable `application/pdf` attachment +(named `Faktura <number> <customer>.pdf`), posts the voucher and links +invoice, document and voucher in one transaction. The voucher debits +`invoice_receivable_account` (default `1510`) with the total and credits +`2610`/`2620`/`2630` with the VAT per rate plus each row's revenue account +with its net; its source is `invoice`. The renderer's totals equal the +voucher rows exactly. `invoice.issue` ignores `client_ref` (the number series +is the idempotency key). + +`invoice.preview` is a read. `customer.create/update/archive`, +`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 +`invoice_bankgiro` is present it is printed in the document's Bankgiro field. + ## 8. The TUI is just a client `bokftui` logs in over the same socket, picks an org and issues the same |
