15

The Run my Accounts fork: thirteen years of a Swiss SQL-Ledger, feature by feature

Run my Accounts AG, a Zurich bookkeeping firm, has run its clients on a fork of SQL-Ledger since before 2013 and publishes that fork on GitHub under the GPL. Its history — 2,130 commits to August 2026 — is the most detailed public record of what a Swiss accounting practice needs from this ledger, and, because the firm keeps part of its platform closed, the schema also records features whose code was never published. This chapter reads the whole history, sorts the features into what is in the open code and what is only implied by tables, says how each was built, and sets out what we should build so that a firm leaving Run my Accounts, or an accountant who learned SQL-Ledger there, finds what they expect on Ledger123.

2026-08-26 · read from a clone of github.com/ledger123/runmyaccounts at commit of 20 August 2026; six agents covered the five periods of the history and the repository’s present state
Executive summary

Run my Accounts froze SQL-Ledger at 2.8.33 in 2013 and spent thirteen years turning it into the posting engine of a Swiss outsourced-bookkeeping service: ESR and then QR-bill payment parts, a line-level VAT model with its own MWST report, a dunning workflow with audit and batch mailing, a clearing-account rebooking screen, a full change log, Swiss and German charts, and DATEV, GoBD and UStVA exports for a German arm. From 2022 the ledger was embedded in a closed portal, and from 2024 in a closed .NET platform that imports camt.053 statements, pulls accounts through SIX bLink, exports pain.001 payments, matches bank lines to names and files the VAT return with the ESTV — all visible only as tables. Eleven of the open features are worth porting under the GPL; the closed ones we build ourselves on the same schema shape; and a 2.8-to-3.2 migration path is the piece that actually captures the users.

15.1What the fork is

The first commit, in September 2013, imports an already-customised SQL-Ledger 2.8.33 with Swiss payment-slip plumbing in it. The application version never changed again; the database version walked from 2.8.10 to 2.8.52 through forty-odd upgrade scripts. Two people wrote most of it: Armaghan Saqib as the contractor, on and off for the whole period, and a succession of in-house developers at Run my Accounts — Nils Samuelsson to 2019, then a team from 2022 working from an internal tracker whose ticket numbers (IBP-nnnn) mark the later commits. Releases were weekly, named by calendar week, then numbered 5.1.8 to 5.2.25. The whole tree is still ISO-8859-1.

Plate 15.1 · Thirteen years in five periods
PeriodCommitsWhat was builtDirection
2013–2015399Per-line VAT on AR, AP and GL; FX residual columns (fxamount, fxpaid); reminder levels audited, queued and printed with ESR slips; GL report ported from 3.x; NRLR-compliant Swiss chart; modular LaTeX templates with the orange payment slip; encpassword and deep-link hooks.A fiduciary’s production line on a frozen 2.8: Swiss compliance and operational hardening, with the first hooks for an external portal.
2016–2017438The All Taxes line-tax report on a rebuilt invoicetax; extended transaction log with row snapshots and a history view; Ledger Doctor; an SQL-injection and CSRF sweep; dispatch methods for the mailing house; ZUGFeRD 1.0 embedded from LaTeX; separate Swiss and German charts and template sets; a Mojolicious GoBD export written by an outside developer.Making a multi-client hosted service auditable, and expanding into Germany.
2018–2020430Clearing-account rebooking screen (cl.pl); Swiss QR-bill variables for the templates in time for the June 2020 deadline; DATEV export; deleted-transaction log; generic CSV importer; per-user closing date; GL “on hold”; departmental year-end; Sendinblue API mailer; pg_dump backups; the Mojolicious layer removed and re-imported.Tools that let the firm’s accountants process client bank movements faster.
2021–2023389QR-bill made standards-exact and extended to dunning letters; reverse-charge VAT; UStVA groundwork (tax.vatkey, formdigit); structured addresses; searchable dropdowns everywhere; batch e-mail of reminders with attachments; last-used report columns; and the hooks — postMessage events, CSRF tokens, an IP whitelist — that show the ledger running inside a closed portal.An in-house team takes over; the ledger becomes the engine behind my.runmyaccounts.com.
2024–2026474A camt.053 bank-import data model; api.pl payment endpoints; SIX bLink consent and process tables; pain.001 export tables; fuzzy name matching; VAT-settlement storage for the ESTV e-filing; financial years; controlling checklists; a SQLite members store; Factur-X post-processing in Python; customer default accounts and Skonto.Two-thirds of substantive commits are schema with no Perl consumer: SQL-Ledger is now the data store of a closed .NET platform.

Two things follow for us. Run my Accounts is a service, not a software vendor: a client who leaves it does not take a product, they take a database dump in a 2.8 schema and a habit of certain screens. And the fork is GPL, with only the trademark and logo reserved, so anything in the open code may be ported with attribution.

15.2The features in the open code

Everything below has its Perl in the repository. The last column marks where we stand: have already in 3.2.12 or this book, partial the substance exists, port worth taking from the fork.

FeatureHow it was builtUsNote
Swiss and German compliance
QR-bill on invoices, statements and remindersbank.qriban, strdbkginf, invdescriptionqr (upgrade 2.8.18); IS::invoice_details and RP::reminder build length-capped, character-stripped *qr variables including the Swico S1 tax string; RMA-content_qrinvoice.tex draws the payment part with the LaTeX qrcode package, gated on CHF and a QR-IBAN.portChapter 1 item 2a asked for exactly this; the open code is the Swiss block.
ESR payment slipar/ap.dcn, bank.dcn/rvc/membernumber, Form::format_dcn modulo-10 check digit, orange-slip template with a PostFinance branch.haveOur bank table carries the columns; ESR is retired in Switzerland since 2022.
Line-item VAT and the All Taxes reportacc_trans.tax, taxamount, tax_chart_id, lineamount; invoicetax.amount; ar/ap.linetax; a Defaults toggle; reports.pl alltaxes reconciles taxable base and tax per account across AR, AP and GL with a period picker; a rebuild tool in custom_am.pl.partialOur invoices carry tax per line; AR/AP/GL transactions do not. The report is the Swiss MWST return’s basis and the period-return engine of chapter 1 item 11.
Reverse-charge VATtax.reversecharge_id; posting a GL line with such a tax writes two offsetting tax lines; the report shows both sides.portNeeded by any EU or Swiss firm buying services abroad.
Swiss and German charts, Swiss-German localeRMACH_{DEU,ENG,FRA} and RMA_CHE KMU charts on the 2014 accounting law, a German SKR-style chart, locale/rma_ch_de; chart.allow_gl and chart.symbol_link role tags.portOur Swiss-German chart is older; the fork’s carry zero-rate VAT accounts and the NRLR structure. Credited to Brändle, Kienast and Samuelsson.
DATEV, GoBD and UStVA for GermanyDATEV: im.pl prepare_datev pairs debits with credits into debitscredits and exports. GoBD: a Mojolicious app writes Firma, Kontenplan, Journal and SummenSalden with the GDPdU index. UStVA: the same app fills an ODS form from a v_inline_tax view.portGerman-market items; the Mojolicious code is the most portable in the tree.
ZUGFeRD and Factur-X2016: XML embedded by LaTeX (pdfx, embedfile) from xml_* variables. 2026: a Python post-processor (factur-x, Ghostscript to PDF/A-3b) reading the database directly, gated per client.partialBacklog item 44 wants EN 16931 output; the Python script is a working reference, though its Perl hook is not in the public tree.
Bookkeeping operations
Clearing-account rebooking (cl.pl)From the account ledger of the configured clearing account, open a GL line, see open AR and AP invoices, tick them, and the line is rewritten to the AR/AP account with payment rows split across invoices oldest first, FX gain or loss posted, or re-pointed to any GL account. CGI::FormBuilder and DBIx::Simple, no menu entry.partialThis is the manual half of chapter 1 item 1’s matching grid; its posting logic is the part to keep.
Extended transaction logOn every re-post or delete, the old header and lines are copied into ar_log, ap_log, gl_log, acc_trans_log and *_log_deleted with a timestamp; a View action shows any prior version; the GL journal can include the log.portThe immutable history chapter 1 item 7 wants; row snapshots are simpler than our audittrail rows.
Ledger Doctorcustom_am.pl: unbalanced journals, paid-versus-booked mismatches, orphans, blank rows, missing dates, bad line-tax rows, each with a fix.portThe anomaly review of chapter 9 item 4, already written for this schema.
Per-user closing date, GL on hold, departmental year-endemployee.closedto with Form::closedto_user; gl.onhold flag and filter; year-end per department.portSmall, useful to a practice with juniors.
Financial yearsfinancial_year rows OPEN, CURRENT, CLOSED, maintained by AM::post_yearend.portThe table the closed platform reads; harmless and clarifying for us.
Last-used report columns, searchable dropdownslastused table via Form::get_lastused; select2 on every select with a numeric-prefix matcher for account numbers.portCheap usability; our usability pass stopped short of both.
Chart hierarchy, customer default accounts, Skonto, reminder stopchart.parent_id back-filled from headings; customer.income_accno_id, vendor.expense_accno_id, early_payment_discount, payment_clearing_accno_id, reminderstop; last_modified triggers.portMaster-data fields an automated poster needs; chapter 9’s categoriser wants the first two.
Dunning
The reminder workflowLevel changes written to audittrail and reported; reminders queued to the spool and batch-printed or concatenated into one PDF; batch e-mail with the invoice attached; Mark as sent writing a structured note; due-date and level filters; QR payment part on the letter.partialOur Reminder report is the 2.8 original; chapter 1 item 3 scheduled it. The fork’s queue, audit and mark-as-sent are the missing operational half.
Import, export, documents
Generic CSV importerIM::prepare_import_data parses any delimited file into report/reportvars, then reuses the per-type importers; sixteen import subs; sample files; closed-period check on import.partialOur importer handles fixed layouts; the mapping step is the addition.
Dispatch methodsdispatch table on customer and vendor; the batch-print queue filters by it, so post, e-mail and portal documents separate.portAn afternoon.
Modular LaTeX templatesSeventy RMA-*.tex parts: packages, settings, header, address, articles, sums, terms, payment table, page breaks, country lookup, QR part, ZUGFeRD part; three languages.partialOur templates were redesigned in HTML; the fork’s LaTeX set is the reference for the QR and e-invoice parts.
Backupspg_dump | gzip mailed or downloaded; templates tarred separately; Mojolicious restore.haveOurs does the same; chapter 4 item 6 goes further.
Warehouse transfers with FIFO repostingtrf.pl, SL/Trf.pm, fifo and build tables, a COGS repost tool.have3.2.12 has warehouse transfers natively.
Integration hooks
api.plMojolicious::Lite; POST /post_payment builds a $form per invoice and calls CP::post_payment, stamping acc_trans.imported_transaction_id; /delete_payment reverses by that id; the dataset is chosen by clientName. No authentication.partialThe first two endpoints of chapter 6 item 1, and proof the approach works on this codebase.
Portal hooksencpassword bearer login, menuids deep links, $ip_whitelist password bypass, FRONTEND_COOKIE CSRF, postMessage events for upload-and-sign and URL-to-PDF, Sendinblue mailer.skipEach is a shortcut for one deployment; the API and roles of chapters 6 and 4 replace them properly.
WLprinterA Java Web Start bridge that spools print jobs for a client on the user’s PC to pull and print locally.skipJava Web Start is dead; browsers print.
SQLite members storeusers/members.db replacing the INI and per-user .conf files, with migration scripts.haveWe did the same in the registry database (SL/Members.pm).

15.3The features whose code stayed closed

From 2024 most substantive commits are upgrade scripts creating tables that no Perl in the repository reads or writes. Their columns, enums and ticket names (every branch is an IBP- ticket, the platform’s own tracker) describe the system that does. Read again in August 2026 against the firm’s support articles, the closed half is eleven modules, not eight: the table below is what the schema says was built, what the website says it does, and how it was evidently put together.

ModuleEvidence in the schemaWhat the site says it doesHow it was evidently built
Bank accounts and statement import (camt.053/054, CSV)bank_account (2.8.32: iban, bic, account, bank, address fields, chart_id, currency moved off chart in 2.8.37, inactive); banking_import_event (status, source_type, is_manual, imported_iban, totals, file_content, delegated_to from 2.8.44); imported_balance (OPBD/CLBD by balance_type); imported_transaction with the ISO 20022 vocabulary — bank_domain_code, bank_family_code, bank_sub_family_code, end_to_end_id, remittance_ref, remittance_type, related_party_name, related_party_address JSONB, origin_amount, exchange_rate, hashcode, suppression NONE/EXCLUDED, a parent id for batch splits; imported_transaction_to_booking (ledger_type AR/AP/GL, 2.8.38); acc_trans.imported_transaction_id written by CP::post_payment.“Daily account synchronisation”; paid receivables and payables reconciled automatically; sixteen named banks.A .NET service parses camt.053 and 054 into rows, one event per file or feed, deduplicates by hash, and links each posted payment back to its bank line through api.pl. The link design was reworked three times in autumn 2024 (IBP-1256, 1290, 1312: partial and multiple invoice linking, splitting, full-text search on invoices).
Open banking through SIX bLinkbank_account.blink_permission_id, blink_account_id (2.8.43); blink_import_process and _log, blink_export_process and _log with JSONB payloads, processed_target_id and, from 2.8.51, a correlation_id (“bLink traceability”, IBP-2965); banking_import_event.delegated_to; the branch release-candidate_BLink-API-v5 merged June 2026.“Direct connections to Swiss and German banks for automated payment uploads and transaction synchronisation.”A consent flow per account, a poller that pulls statements into the same import tables, a delegation flag when the platform takes the import over, and a log row per API call so a failed pull can be traced.
The payment module (pain.001 out, review mode)banking_export_event (status, source_type, is_manual, export_message_id, export_submission_id); exported_payment (rma_payment_id, payload JSONB, export_instruction_id); vendor.early_payment_discount (2.8.47); vendor.payment_clearing_accno_id (2.8.48); vendor.iban, bic on the open vendor screen.Payments are prepared from processed documents and either uploaded straight to e-banking or routed to a review mode where the user may change the valuation date, the bank account and the amount, re-send a payment the bank deleted, generate a file for manual upload, and drill into the document. Every payment carries an ISO 20022 End-to-End ID so the returning statement closes it automatically. The module is “an alternative to the workflow module” for firms that only want one approver.The platform builds pain.001 from its own payment proposals (rma_payment_id, not SQL-Ledger’s), stores the message and instruction ids, submits through bLink or hands the file over, and matches the camt.054 debit back by End-to-End ID.
Auto-matching of bank linessearch_irrelevant_words (search_target RELATED_PARTY or COMMON) seeded with legal forms (sàrl, sa, gmbh, ag, sagl, gbr, ohg, partg, ug, ggmbh, llc) and DE/FR/EN stopwords; a plpgsql to_filtered_tsvector(input, filter_type, ts_config); CREATE EXTENSION fuzzystrmatch (2.8.42).— (invisible to the client; it is what makes “automatic reconciliation” true)Full-text and Levenshtein matching of related_party_name against customers and vendors, done in PostgreSQL and called from the platform.
The clearing account (Abklärungskonto)Chart account 1099 Unklare Beträge with symbol_link = 'abklärung' and 1090 Transferkonto with 'transfer'; customer.payment_clearing_accno_id and vendor.payment_clearing_accno_id (2.8.48); chat (2.8.21: trans_id, message, employee_id, creation_date); the open cl.pl rebooking screen.A to-do list of bank lines the bookkeeper could not place: the bookkeeper assigns each to the client with a question; the client answers in the module, uploads the missing receipt or e-mails it, and chats instead of writing e-mails; the amount stays visible on 1099 in the trial balance until resolved, then is rebooked; notification e-mails immediate, daily or weekly.The portal shows the client the 1099 lines with the bookkeeper’s question and a chat thread per line; the bookkeeper resolves through cl.pl; the platform finds the clearing account by its symbol_link role so one screen serves every chart.
Swiss VAT return filed with the ESTVvat_settlement (2.8.27: vat_form_id, period, data JSON, xml BYTEA; type NORMAL_SETTLEMENT / CORRECTION_SETTLEMENT / ANNUAL_RECONCILIATION from 2.8.28; meldecenter_data JSON from 2.8.39); booking_to_settlement (booking_id, settlement_id); tax.vatkey, tax.formdigit, tax.validfrom (2.8.25), tax.reversecharge_id, tax.inactive; ticket IBP-2504 “VAT App Overview” (2026).Documents in by the 10th after the period (quarterly for the effective method, half-yearly for the net-tax-rate method); the firm reviews, prepares the statement, fills the form in the client’s ESTV online account under a power of attorney; the client logs in and releases it; a PDF of the workings is filed in the software and the payment is prepared for e-banking.The platform computes the form from line-tax data, maps rates to boxes through formdigit, stores the XML, keeps what the Meldecenter returned, and locks every booking to the settlement it was reported in.
Controlling checklistxcontrolling_log (2.8.49: trans_id, controlling_key, checked, checked_timestamp, checked_hash, checked_by_ibp_user_id).“The algorithm continuously monitors completeness and consistency … plausibilises VAT and supports our teams with interactive checklists.”A review workflow per transaction and per check key, the hash of the reviewed figures detecting a later edit — the mechanism chapter 13 section 6 wants for a US preparer’s review.
The closing app (Abschluss-App)xabschluss and xabschluss_history: xml made nullable, data jsonb and status TEXT DEFAULT 'NEW' added in 2.8.52 (June 2026); financial_year (2.8.34: OPEN / CURRENT_FINANCIAL_YEAR / CLOSED, yearend_id); error code for closing a period with no bookings (IBP-2208).“In 18 steps to the close”: questions in business language rather than accountant’s German; the answers “flow directly into the closing entries, prepared in the background as you answer”; documentation of the closing entries generated for the auditor; then a closing adviser reviews. Sold with the closing service at CHF 990 / 1,590 / 2,750 including 4 / 8 / 16 adviser hours.A questionnaire whose state lives in xabschluss.data, with a history table, moving from an XML document to JSON in 2026; the answers generate proposed adjusting entries.
Chat and the change feedchat (trans_id, message, employee_id, creation_date); customer.last_modified and vendor.last_modified with the trigger update_last_modified_column() (2.8.46); chart.symbol_link roles (kassa, transfer, forderungenschweiz…).“Chat on bookings with your online accountant”, for unclear entries and documents.A comment thread per transaction surfaced in the portal and on the clearing lines; a CRM sync polling by timestamp; the platform finding accounts by role, not number.
Workflow stationsNone in the ledger — the module is entirely in the platform.An optional approval process inserted between document processing and booking, “sequential and by different criteria”, for management, department heads and project owners; at a station a user may change the coding, add cost or profit centres, split amounts and assign projects; every change and release is logged at posting; stations are drawn as a graph with arrows for the moves allowed (delete, to payment, forward) and a number flag per station counting documents waiting; paid documents drop into an archive.Stations, members and rules in the platform; the ledger receives the finished coding as a posting, with the audit rows.
Document intake, recognition and archivepostMessage events uploadLinkAndSignFile and urlToPdf; Alfresco WebDAV links rewritten to service.runmyaccounts.com/…/dms; help pages dms.html, volltextsuche.html.Scan at 300 dpi black-and-white PDF or forward by e-mail; “automated document recognition using machine-learning algorithms” at “96% correct recognition”, then “specialists manually verify each extraction”; every document signed with a GeBüV signature, filed, and found by full-text search; annotations written on the paper in black ballpoint are read as instructions.An Alfresco-backed DMS in the portal, a recognition service with a human validation queue in front of posting, a headless browser rendering report URLs to PDF, and search across documents.
The expense app (Spesen-App)None in the ledger beyond employee and AP.Photograph (Kamera), import, or Ohne Beleg for mileage; crop with a blue frame; choose Spesenbeleg or Lieferantenrechnung; mandatory Beschreibung, Zahlungsmethode (company or private card) and Spesenart; an Erweiterte Ansicht for partial amounts; the service centre validates date, amount, currency, rate, account and VAT and returns the booking to the employee, who confirms and sends it to the Spesenverantwortlicher; approval books it and triggers reimbursement.A native app on iOS and Android feeding the same recognition and validation queue as scanned documents, then an AP posting against the employee.
The cockpit (dashboard)None in the ledger; graphs.pl is a one-chart stub.Fourteen widgets on the support page and in the app — gross revenue, cash and bank, top 10 customers, balance of one account, expenses, income, profit or loss as a figure and as a trend, expenses vs income, supplier invoices, customer invoices, open customer vs supplier invoices, net working capital, and the net working capital trend the users voted for in 2012 (four curves over 6 to 36 months) — added and removed from a side menu, arranged by drag and drop, most with a comparison period and red or green flags against it, saved per user, addable more than once; the app pushes a notification when the bookkeeper has a question and a daily 17:00 notice of processed expenses.A widget layout per user in the portal, each widget one report query over the ledger; the same widgets in the RmA Cockpit app.
Finding

Read together, the two tables are one design. The open half is what a Swiss bookkeeper needs the ledger to do: QR-bills, line VAT, dunning, clearing, history. The closed half is what the service does around it: bring the bank in, send payments out, match, ask the client, file, review, close. Every item in the closed half is already an enhancement in this book — chapter 1 item 1 is their import model, chapter 6 item 3 is bLink, chapter 2 item 6 is pain.001, chapter 9 item 1 is the matcher, chapter 13 module 4 is the clearing questions — and their schema is a good shape to borrow, because it was co-designed with a real bank-feed consumer over two years. Section 15.7 designs each of the eleven as a set of screens.

15.4What the website sells, checked against the code

The repository shows what was built; runmyaccounts.ch shows what is sold. The two were read against each other. Every line below is from the firm’s own pages, with the page it came from, and a note saying where in the code it lives, or that it does not.

Sold asOn the siteIn the codeUs
Kunden-Modul — the customer module (online-buchhaltungs-software)
Invoices, credit notes, quotations, orders, delivery notes, customer and item masterListed verbatimStock SQL-Ledgerhave
Recurring invoices “for subscription management (weekly, monthly, yearly)”; bulk dispatchListedStock recurring transactions; batch queue and e-mailhave
Payment slips and QR invoicesListed; QR “from mid-2020” (iso-20022)Open: the QR block in IS.pm/RP.pmbuild
“Live e-mail notifications on incoming payments” and “for overdue invoices”ListedClosed: fired by the platform on bank importbuild
Three-level dunning; customer statementsListedOpen: the reminder workflowpartial
Lieferanten-Modul — the supplier module
“Automated document recognition using machine-learning algorithms”, “validation by specialists”, automated reading of payment slips, QR invoices, IBANsListedClosed: the scanning and booking service (the “PaperBuddy” of the 2014 chart accounts)build
“Payment processing in e-banking”: “we load the payments we have captured directly into your e-banking, where you release them”; “continuous synchronisation of all outgoing payments with your bank account”Listed; pain.001 since 2018, camt.053 from 2017 “at the first banks”Closed: the export and import tables of 15.3build
“All your documents are digitally signed with a GeBüV signature”; full-text searchListed; “meets the Federal Tax Administration’s requirements for an electronic archive”Closed: the Alfresco DMSbuild
Workflow-Modul (wie-funktioniert-der-workflow, workflow-einstellungen)
Organisation structure as “stations”; users assigned per station; sequential multi-level approval; rule sets by amount and supplier; during approval the coding can be changed, amounts split, cost centres and projects assigned, notes added; every approval loggedListed; “works with all document classes”; a counter of documents per stationClosed entirelybuild
Payment workflow: an editable grid of payments with statuses new, exported, loaded to the bank, in processing, deleted; amount, date and debit account editable in place; export CSV, XLS, PDF; an exported payment can be put back to newzahlungsworkflow-modulClosed: exported_payment, banking_export_eventbuild
Finanzbuchhaltungs-Modul — the ledger
Dashboard “with daily current figures”; charts of liquidity, receivables and payables; top customers; period comparisonsListedClosed: the portal (graphs.pl is a one-chart stub)partial
Profit centres and cost centres; project accounting; journal, trial balance, account sheets; any foreign currencyListedStock SQL-Ledger plus reports.plhave
“Chat on bookings with your online accountant”ListedClosed: the chat tablebuild
“Interactive closing app: to the annual close in 18 steps”ListedClosed: xabschluss, xcontrolling_logbuild
Spesen-App (spesen-app)
iOS and Android; photograph a receipt or forward an e-mail; categorise; “the supervisor can check the employee’s expenses and generate a payment at the press of a button”; works only with an RMA clientListedClosedbuild
Schnittstellen — integrations (run-my-accounts-restful-api, api-authentifizierung, marktplatz)
REST API: https://service.runmyaccounts.com/api/latest/clients/<client>/…; JSON or XML by Accept; dates yyyy-MM-dd; Authorization: Bearer pat_… (URL keys blocked from 1 April 2026); GET, POST, and PUT only on customer; 200/204/400/403/404/500; sandbox on request. Resources: customers (list, create or deactivate, get by number, update), invoices (list, create, get, PDF or HTML), invoice payments (list, add), parts, projects (list, create, edit), vendors, vendor invoices (list, create, get, PDF), vendor payments, charts, trial balance, cost and profit centres, GL bookings (create), account transactions, year-end bookings, document upload. Eighteen worked use cases (discounts, deposits, credit notes, reversals, Skonto, transfer accounts). Rule: “bank accounts are normally only booked by Run my Accounts … post through a current account”.Documented in 30 resource articles and 11 entity articlesClosed — served by the platform, not by api.plpartial
Zapier (create customer, AR invoice, AR transaction); WooCommerce; Harvest; PayPal; Dropbox; Pingen (postal mailing); Revolut expenses; smallinvoice; Tresio cash-flow; MOCO; DAPAY; “over 40 partners”Marketplace and support articlesClosed, over the APIpartial
Sixteen named Swiss banks from UBS and PostFinance to the cantonal and Raiffeisen banks; “we never access your e-banking directly”Listed with logosClosed: camt in, pain out, bLinkbuild
MWST (wie-wird-die-mwst-abrechnung-erstellt)
Effective method quarterly, net-tax-rate method half-yearly; the client opens an MWST online account and grants power of attorney; the accountant prepares the return; the client logs in and releases it; a PDF of all supporting documents is uploaded; the payment is loaded to e-banking for approvalStep by stepClosed: vat_settlement; open: the line-tax report beneath itpartial
Security and hosting
Two data centres in Bern; daily backup snapshot; 2FA by mTAN or Google Authenticator; login log; unlimited users and roles; database export at any time; an “open-source licence option”; ISO 9001ListedRoles and export open; 2FA and the login log closedpartial
Prices (preis-buchhaltung)
Bookkeeping service: S CHF 0 + 1.80 a booking; M CHF 35 + 1.40; L CHF 290 + 0.95; scanning CHF 2 a document. Software alone: Startup free (debtors, creditors, orders, REST API, Zapier, PDF archive, workflows, apps); KMU CHF 25 a month plus CHF 10 a user; free for a year to Zürcher Kantonalbank customersListed, excluding VAT, packages changeable monthlyThe Practice plan of chapter 10 sits between M and L
Finding

The website and the repository agree exactly. Everything under Kunden and Finanzbuchhaltung that is a ledger function is open code; everything that is a service around it — recognition, banking, workflow, chat, closing app, expense app, the REST API the partners use, 2FA — is the closed platform whose tables section 15.3 read. The free Startup software plan is the freemium of chapter 10 in Swiss form, and it names the six things a Swiss firm expects free: debtors, creditors, orders, an API, Zapier and a PDF archive.

15.5Who comes from Run my Accounts, and what they expect

Three kinds of user. A client firm that stops outsourcing and wants its own books: it arrives with a database dump in the 2.8 schema, years of history, and a habit of the RMA screens. An accountant who worked in the fork and now serves clients independently: they know the All Taxes report, the reminder queue and the clearing screen by name. And a German client of the firm’s German arm, who needs DATEV and GoBD more than anything Swiss.

Plate 15.2 · Day one for a Run my Accounts refugee
  • Their dump restores — fxamount, dcn, the QR bank columns, invoicetax, the log tables, financial_year, symbol_link all mapped or preserved — and the trial balance agrees to the cent with their last RMA statement.
  • Invoices and reminders print with a QR-bill on the firm’s own QR-IBAN.
  • The MWST return comes out of one report for the period, in the ESTV’s box numbers.
  • Reminders queue, batch, mail and mark themselves sent.
  • A bank statement imports and the clearing screen, or better the matching grid, clears it.
  • The chart is the KMU chart they know, in German, French or English, and the interface is in Swiss German.
  • For the German client: DATEV export and a GoBD archive on demand.

15.6What to build

Ordered so that the first three make the migration credible, the next four make the daily work familiar, and the rest match the platform. Items marked port take GPL code from the fork with attribution; the rest we build.

Tier 1Credible migration: 1–3
Tier 2Familiar daily work: 4–7
Tier 3The closed half, built open: 8–10
Tier 4Germany: 11

1A 2.8-to-3.2 migration importer for RMA datasets

The one item nothing else substitutes for. A script that takes a fork dump, restores it into a scratch database, and produces a Ledger123 dataset: the 3.2 upgrade chain applied, then a mapping layer for the fork’s own columns — ar/ap.fxamount and fxpaid folded into our FX handling, dcn and the bank QR columns carried across, the line-tax columns on acc_trans preserved for enhancement 3, the *_log tables archived as history, financial_year and chart.parent_id kept, symbol_link retained as a tag, the ISO-8859-1 text recoded to UTF-8, and a reconciliation report proving the trial balance, AR and AP ageing and VAT totals match before and after. Run for the client by us, as chapter 10’s first-customer stage already assumes.

Touches: a new bin/migrate-rma.pl, SL/User.pm dbupdate, one schema upgrade for the retained columns. Effort: medium-large; the reconciliation is the work.

2QR-bill, ported

Take IS::invoice_details’s QR block and its twin in RP::reminder into one shared sub, the three bank columns, the country lookup, the abbreviation helpers, and the qrinvoice LaTeX parts as the LaTeX half of chapter 1 item 2a; add the HTML-template equivalent with a generated QR image. Test against the SIX validator. This is the single most-asked question a Swiss prospect has.

Touches: SL/IS.pm, SL/RP.pm, SL/AM.pm bank accounts, templates. Effort: small; the code exists.

3Line-item VAT on transactions and the All Taxes report

Extend our per-line tax from invoices to AR, AP and GL transactions using the fork’s acc_trans.tax, taxamount, tax_chart_id and lineamount columns, with reverse charge, and port the All Taxes report as the period engine of chapter 1 item 11. Then the Swiss MWST form: box numbers on the tax record (the fork’s formdigit), the effective and net-tax-rate methods, and a printable return.

Touches: SL/AA.pm, SL/GL.pm, SL/RP.pm, bin/mozilla/aa.pl, gl.pl, one schema upgrade. Effort: medium.

4The reminder workflow, ported

Level-change audit, the spool queue with batch print and single-PDF concatenation, batch e-mail with the invoice attached, Mark as sent, reminderstop on the customer, due-date and level filters — onto the scheduled reminders of chapter 1 item 3.

Touches: SL/RP.pm, SL/BP.pm, bin/mozilla/rp.pl, bp.pl. Effort: small-medium.

5Clearing-account posting logic

Keep cl.pl’s posting core — rewrite a GL line to the AR/AP account, split across open invoices oldest first, post FX difference, or re-point to any account — as the accept action of the matching grid in chapter 1 item 1, so a statement line becomes a payment in one click. Drop its CGI::FormBuilder screen.

Touches: SL/BK.pm, SL/CP.pm. Effort: small.

6Extended transaction log and Ledger Doctor, ported

The row-snapshot log tables and the View history as the append-only audit of chapter 1 item 7; the Doctor’s checks as the first tests of chapter 9 item 4’s anomaly review.

Touches: SL/AA.pm, GL.pm, IS.pm, IR.pm, a new bin/mozilla/doctor.pl. Effort: small-medium.

7The small habits

Per-user closing date; GL on hold; departmental year-end; financial years; last-used columns; searchable dropdowns; dispatch methods; chart hierarchy; customer income and vendor expense accounts, Skonto and clearing account; the KMU charts in three languages and the Swiss-German locale. Each is a day; together they are what makes the fork’s accountant feel at home.

Touches: across SL/ and bin/mozilla/; three chart files; one locale. Effort: medium in aggregate.

8The bank-import model, in their shape

Adopt the fork’s imported_transaction vocabulary for the bankline table of chapter 1 item 1 — the ISO 20022 transaction codes, end-to-end id, remittance reference, related party as JSON, the hash, suppression and parent id — and write the camt.053 and 054 parser it never published, in Perl, alongside CSV and OFX. Then the bLink adapter of chapter 6 item 3 on the consent columns they defined, and the pain.001 writer of chapter 2 item 6 with the message, submission and instruction ids they store.

Touches: SL/BK.pm, a camt parser, provider adapters. Effort: medium-large, shared with the earlier chapters.

9Matching in the database

Their stopword table, filtered tsvector function and fuzzystrmatch as the memory-and-rules layer of chapter 9 item 1, before any model is consulted. The legal-form token list is the useful part and is theirs to copy.

Touches: one schema upgrade, SL/BK.pm. Effort: small.

10VAT settlement and the Meldecenter

Their vat_settlement and booking_to_settlement tables as the filed-return store of chapter 1 item 11: a settlement locks the bookings it contains, corrections and the annual reconciliation are typed, and the eCH-0217 XML is kept. E-filing through the ESTV Meldecenter API is the last step and needs a registered account; the XML and the printable form come first.

Touches: SL/RP.pm, one schema upgrade, a filing client. Effort: medium.

11DATEV, GoBD and UStVA, ported

The Mojolicious GoBD and UStVA code moves almost unchanged onto our application server; the DATEV pairing routine ports into SL/IM.pm with its staging tables made per-session. Together with a German SKR chart these are what the fork’s German clients need, and what any German prospect asks first.

Touches: the appserver, SL/IM.pm. Effort: small-medium.

15.7Architecture: our own implementation of the closed half

Section 15.3 read the closed features from their tables and their brochure. This section designs ours, one module at a time: the data it keeps, the screens the user sees — every form in the flow, not just the first — how it joins the screens SQL-Ledger already has, the API and outside services it touches, and a prompt a Claude Code session in this repository can be given to build it. Where the fork defined a table, the design keeps their column names so a migrated dataset carries its history; where SQL-Ledger already has a field, the design uses that name and no other.

Conventions in the figures: each is a working HTML form — type in it, open the selects; nothing submits anywhere. The grey tag after a label, transdate, is the name the input posts as, in SQL-Ledger’s own vocabulary (accno, transdate, vc, checked_1…) or the fork’s where the table is theirs. Buttons carry the label the dispatcher will receive as the action, which is why they are not renamed lightly (chapter 1). Report tables use the classes this repository’s CLAUDE.md prescribes; layout tables carry none.

ABank accounts and statements in: camt.053, camt.054, CSV, OFX, bLink

Data

SQL-Ledger 3.2 already has System → Bank Accounts: a chart account whose link carries AR_paid/AP_paid, plus a bank row keyed by the chart id with name, iban, bic, clearingnumber, membernumber, rvc, dcn. The fork made a separate bank_account table with chart_id and currency; we keep ours and extend it: bank.curr, bank.feed (none, blink, plaid), bank.blink_permission_id, bank.blink_account_id, bank.csvmap (jsonb column map), bank.format (default statement format). Then three tables in the fork’s shape, reduced to what Perl will read: bankimport (id, bank_id → chart id, source_type FILE/FEED, format camt053/camt054/csv/ofx, filename, imported_when, login, imported_iban, curr, opening_balance, closing_balance, status NEW/MATCHING/RECONCILED, file_content, delegated_to); bankline (id, bankimport_id, parent_id for batch splits, booking_date, value_date, amount, curr, credit_debit, related_party_name, related_party_iban, remittance_ref, remittance_type QRR/SCOR/NONE, transaction_text, end_to_end_id, bank_domain_code, bank_family_code, bank_sub_family_code, origin_amount, origin_currency, exchange_rate, hashcode unique, suppression NONE/EXCLUDED, suggest_type, suggest_id, suggest_score, status OPEN/MATCHED/SUPPRESSED, matched_at, matched_by); and bankrule (id, bank_id, pattern, field name/text/reference, chart_id, department_id, project_id, vc, vc_id, priority). acc_trans gains bankline_id — the fork’s imported_transaction_id under our name, with a view of that name for migrated datasets.

Screens

System → Bank Accounts → Bank Account Detailsmock-up · nothing submits
1020 · UBS CHF the existing edit_bank screen with a feed section added
Account accno
Bank name
IBAN iban BIC bic
Currency curr   QR-IBAN dcn
Statement format format
CSV column map csvmapdate amount name reference learned from the first upload, editable here
Feed feed   connected permission blink_permission_id account blink_account_id
Pull schedule feed_schedule   notify on incoming payment notify_login

Figure A1. The bank account is the anchor: Connect Feed launches the bLink consent flow and stores what comes back on bank, as the fork stores it on bank_account.

Cash → Bank Importmock-up · nothing submits
Bank accno bLink last pull 26.08.2026 06:00
Statement file file
Format format        
Imports
Imported imported_whenSource filenameLinesMatchedOpenOpening opening_balanceClosing closing_balanceStatus status
26.08.2026 06:00bLink pull4137412'480.1519'302.60matching
25.08.2026 06:00bLink pull1212011'900.0012'480.15reconciled
31.07.2026 17:12camt053_20260731.xml969608'204.3011'900.00reconciled

Figure A2. Upload or pull; one bankimport row per file or pull. A file whose lines all hash to existing rows imports as zero new lines rather than failing.

Cash → Bank Import → Open lines · 1020 UBS CHF · import of 26.08.2026mock-up · nothing submits
Show status from fromdate to todate
Date booking_dateAmount amountCounterparty related_party_nameReference remittance_refSuggestion suggest_type and action
25.08.2026+2'150.00MUELLER AG, Zug21 00000 00003 13947 14300 09017reference Invoice 2026-0143 Mueller AG 2'150.00 · 1.00
25.08.2026−389.40SWISSCOM AGE2E 0d9f2c…name 0.92 Vendor invoice 88123 Swisscom 389.40
24.08.2026−45.00KARTENGEBUEHRrule 6900 Bank charges · rule “KARTENGEB*”
24.08.2026+1'000.00UNKNOWN PAYERnone
4 open · suppressed lines are kept and reported, never deleted

Figure A3. The screen that replaces the fork’s closed importer and its open cl.pl for the common case. Accept posts through CP::post_payment (invoices) or GL::post_transaction (rules) with bankline_id on every acc_trans row; Ask Client is design J; Post As is Figure A5.

Cash → Bank Import → Choose · line 90322 · +1'000.00 · UNKNOWN PAYERmock-up · nothing submits
Match against vc  
Name name Invoice invnumber Amount amountfrom amountto
Invoice invnumberCustomerDate transdateDue duedateOpenApply paid_NDiscount discount_N
2026-0151Bergwerk GmbH02.08.202601.09.20261'000.00
2026-0139Bergwerk GmbH12.07.202611.08.20261'020.50
Remainder accno   Memo memo
Remember remember

Figure A4. The invoice picker is Cash → Receipt’s grid with the bank line as the source: id_N, paid_N, discount_N are that screen’s own names, and Match calls CP::post_payment with datepaid = the line’s booking date and source = its reference.

Cash → Bank Import → Post As · line 90318 · −45.00 · KARTENGEBUEHRmock-up · nothing submits
Reference reference Date transdate
Description description Department department
Account accno_NDebit debit_NCredit credit_NTax tax_NProject projectnumber_NMemo memo_N
Save as rule rule

Figure A5. A two-line general ledger entry with the bank side fixed: the same accno_N/debit_N/credit_N vocabulary as General Ledger → Add Transaction, posted by GL::post_transaction, and a rule saved in the same click.

Cash → Reconciliation · prefilled from import of 26.08.2026mock-up · nothing submits
Account accno from fromdate to todate
Statement balance statementbalance = bankimport.closing_balance, not typed   Difference difference
Date transdate_NSource source_NDescriptionDebitCreditCleared cleared_NBank line
25.08.20262026-0143Mueller AG2'150.0090311
25.08.202688123Swisscom AG389.4090312
22.08.2026CHK 1041Hotel Adler640.00not on statement

Figure A6. The existing rc.pl screen, entered from the import with cleared_N pre-ticked for every posting that carries a bankline_id; Done locks the import. Only the unmatched cheque needs a person.

Workflow

A file arrives by upload, by the inbox mail address, or by the feed poller. The parser (SL/BK.pm: parse_camt053, parse_camt054, parse_csv by the bank’s saved csvmap, parse_ofx) writes bankimport and bankline rows, skipping any whose hash exists. The matcher runs in order: QR or SCOR reference against ar.dcn/ap.dcn and invoice numbers; end_to_end_id against our own payment runs (design B); amount and counterparty name against open invoices with the fuzzy matcher of design C, then bankmemory, then bankrule. Each line carries its suggestion and a score. Accepting posts; the receipt or payment appears in the normal reports with a link back to the statement line; a line nobody can place goes to the client as a question (design J) and, if the firm chooses, is posted meanwhile to the clearing account. Reconciliation is against the import’s closing balance and locks the import.

Integration points

API: POST /bank/{accno}/imports (multipart), GET /bank/{accno}/lines?status=open, POST /bank/lines/{id}/accept, POST /bank/lines/{id}/match {vc, id, paid}, POST /bank/lines/{id}/suppress; webhook bankline.matched, which is also what mails the “live notification on incoming payments” to bank.notify_login. Outside services: bLink (SIX) behind a BK::Feed adapter with connect, pull, disconnect; the same interface takes Plaid or GoCardless. A nightly job pulls every connected account.

Claude Code prompt
Read CLAUDE.md, bin/mozilla/am.pl (bank_accounts, edit_bank, bank_header), bin/mozilla/rc.pl, bin/mozilla/cp.pl and SL/CP.pm. Build bank-statement import for SQL-Ledger in this repository, matching Figures A1–A6 of doc/enhancement/bank-import.md (write that spec first from this prompt, reproducing the six screens and their field names).

Schema: sql/Pg-upgrade-3.2.12-3.2.13.sql adding to bank: curr char(3), feed text, blink_permission_id bigint, blink_account_id text, csvmap jsonb, format text, feed_schedule text, notify_login text; creating bankimport(id serial, bank_id int, source_type text, format text, filename text, imported_when timestamptz default now(), login text, imported_iban text, curr char(3), opening_balance numeric, closing_balance numeric, status text default 'NEW', file_content bytea, delegated_to text), bankline(id serial, bankimport_id int references bankimport, parent_id int references bankline, booking_date date, value_date date, amount numeric, curr char(3), credit_debit char(4), related_party_name text, related_party_iban text, remittance_ref text, remittance_type text, transaction_text text, end_to_end_id text, bank_domain_code text, bank_family_code text, bank_sub_family_code text, origin_amount numeric, origin_currency char(3), exchange_rate numeric, hashcode text unique, suppression text default 'NONE', suggest_type text, suggest_id int, suggest_score numeric, status text default 'OPEN', matched_at timestamptz, matched_by text), bankrule(id serial, bank_id int, pattern text, field text, chart_id int, department_id int, project_id int, vc text, vc_id int, priority int default 100); acc_trans.bankline_id int; a view imported_transaction_id_compat is not needed — instead write the 2.8→3.2 migration importer (item 1 of section .6) to map imported_transaction rows into bankline and acc_trans.imported_transaction_id into bankline_id. Bump dbversion in Form::new; mirror the DDL in sql/Pg-tables.sql.

Module: SL/BK.pm with parse_camt053 and parse_camt054 (XML::LibXML; BkToCstmrStmt and BkToCstmrDbtCdtNtfctn; Ntry/NtryDtls/TxDtls; RmtInf/Strd/CdtrRefInf/Ref with Tp/CdOrPrtry to set remittance_type QRR or SCOR; RltdPties Dbtr/Cdtr Nm and Acct IBAN; BkTxCd Domn/Fmly/SubFmlyCd; Bal with Tp CdOrPrtry OPBD/CLBD into opening/closing balance; batch entries into child lines with parent_id), parse_csv (bank.csvmap; learn the map from a header row on first upload and save it), parse_ofx, import_file (hashcode = sha256 of bank_id|booking_date|amount|remittance_ref|related_party_name|end_to_end_id; skip existing), suggest (order: reference → ar.dcn/ap.dcn and ar/ap.invnumber; end_to_end_id → paymentrunline; amount ± 0.05 and name via BK::match_party; bankmemory; bankrule; store suggest_type/suggest_id/suggest_score), accept_line and accept_selected (invoices: build the $form CP::post_payment expects — datepaid = booking_date, source = remittance_ref or end_to_end_id, memo = transaction_text, id_N/paid_N/discount_N — then set bankline_id on every acc_trans row it created, in one transaction; rules: GL::post_transaction with reference/transdate/description/accno_N/debit_N/credit_N), match_line (Figure A4 fields), post_as (Figure A5 fields, plus bankrule when rule=1), suppress_line, reconcile_import (open rc.pl with accno/fromdate/todate/statementbalance and cleared_N for postings carrying bankline_id of this import; on Done set bankimport.status RECONCILED).

UI: bin/mozilla/bk.pl (root symlink bk.pl → dispatch.pl) with actions import, list, open, continue, accept, accept_selected, choose, search, match, post_as, post, suppress, ask_client (stub calling design J when present), reconcile_import, pull_now; extend am.pl edit_bank with the Figure A1 rows (curr, dcn, format, csvmap_*, feed, blink ids readonly, feed_schedule, notify_login) and actions Connect Feed / Disconnect Feed calling a BK::Feed adapter (stub for bLink returning a fixed permission id in dev); menu.ini sections [Cash--Bank Import] and [Cash--Reports--Bank Imports]; locale entries for every new submit label; Bootstrap 5 classes exactly as CLAUDE.md prescribes. Make Cash → Reconciliation accept bankimport_id and pre-tick.

Tests: t/bank_import.t with synthetic fixtures t/fixtures/camt053-ubs.xml, camt054-postfinance.xml (with one batch entry) and ubs.csv, asserting line counts, batch splitting, dedupe on re-import, QRR reference matching to an AR invoice created in the test, name matching to an AP invoice, a rule posting a GL entry, bankline_id set on every acc_trans row, and that reconcile_import pre-ticks and locks. perl -c on every touched file; run the test; do not add bLink beyond the adapter stub.

BThe payment module: payment run, review, pain.001

Data

paymentrun (id, bank_id → chart id, rundate, datepaid as the execution date, status NEW / APPROVED / EXPORTED / AT_BANK / DONE / CANCELLED, approved_by, approved_at, export_message_id, export_submission_id, exported_when, is_manual, file_content, login) — the fork’s banking_export_event with a status machine; and paymentrunline (id, paymentrun_id, vc, vc_id, trans_id, invnumber, amount, discount, paid, curr, iban, bic, remittance_type QRR/SCOR/NONE, remittance_ref, transaction_text, export_instruction_id, end_to_end_id, payload jsonb, status NEW / EXPORTED / PAID / REJECTED / DELETED, acc_trans_id once paid) — their exported_payment. Vendors gain early_payment_discount, payment_clearing_accno_id and remittance_type, next to the iban, bic, discountterms, cashdiscount and payment_accno_id the vendor screen already has.

Screens

AP → Vendors → Edit Vendor · payment sectionmock-up · nothing submits
IBAN iban BIC bic
Reference type remittance_type   Remittance voucher remittancevoucher
Early payment discount early_payment_discount % within discountterms days   Terms terms days
Pay from payment_accno   Clearing account payment_clearing_accno

Figure B1. Four rows added to ct.pl’s vendor form. The fork already edits iban, bic, early_payment_discount and payment_clearing_accno there; remittance_type is ours.

Cash → Payment Runmock-up · nothing submits
Pay from accno Due up to duedateto Execution date datepaid Vendor vendor Department department
Run 2026-031 new paymentrun_id
VendorInvoice invnumberDue duedateAmountDiscount discount_NPay paid_NReference remittance_refIBAN
Swisscom AG8812329.08.2026389.40QRR 21 00000 00003 13947 14300 09017CH44 3199 9123 0008 8901 2
Mueller Treuhand GmbH2026-1731.08.20261'200.00SCOR RF18 5390 0754 7034CH93 0076 2011 6238 5295 7
Hotel Adler447102.09.2026640.00no IBAN on vendor
Selected 21'565.40CHF
approver: A. Ciccone (rule “payment release”)

Figure B2. Propose is Cash → Payments’ selection with the fork’s five statuses on top. Discount is computed from early_payment_discount when datepaid is within discountterms; the grid’s names are cp.pl’s.

Cash → Payment Run → Review · run 2026-031 · 2 payments · 1'565.40 CHFmock-up · nothing submits

The approver’s screen: the fork’s review mode. Each line may be unticked, re-dated, re-routed or reduced; the document opens beside it.

Pay checked_NVendor / invoiceDocumentValuation date datepaid_NFrom account accno_NAmount paid_NEnd-to-End ID end_to_end_id
Swisscom AG · 88123L123-DEMO-31-1
Mueller Treuhand GmbH · 2026-17L123-DEMO-31-2
Note notes

Figure B3. Approval through the rule of chapter 4 item 3. The End-to-End ID is minted here, not at export, so the approver signs off the identifier the bank will echo back.

Cash → Payment Run → Export · run 2026-031 approvedmock-up · nothing submits
Transport source_type  
Message id export_message_id   pain.001.001.09, Swiss Payment Standard, one PmtInf, two CdtTrfTxInf
Lines
Instruction export_instruction_idVendor / invoiceAmountStatus statusBank linePosted
L123-DEMO-31-1Swisscom AG · 88123389.40paid90441 · camt.054 30.08.202630.08.2026 · CP
L123-DEMO-31-2Mueller Treuhand GmbH · 2026-171'176.00exported

Figure B4. The file is the default transport — “never our system touching their banking”, as the fork’s site puts it. When the camt.054 import of design A brings a debit whose End-to-End ID is ours, the line is marked paid and the payment posted with the invoice’s trans_id; Re-export covers the payment the bank deleted.

Workflow and integration

Propose selects open AP rows due by duedateto, skipping any still inside a station (design H). The run passes through the approval rule; the approver’s edits are kept on the lines. Export writes pain.001.001.09 in the ch.03 flavour with CdtrRefInf for QRR and SCOR and Ustrd otherwise, storing message and instruction ids. API: POST /paymentruns {accno, duedateto, datepaid}, POST /paymentruns/{id}/approve, GET /paymentruns/{id}/pain001, POST /paymentruns/{id}/loaded; webhooks paymentrun.exported, paymentrun.paid.

Claude Code prompt
Read CLAUDE.md, bin/mozilla/cp.pl, SL/CP.pm, bin/mozilla/ct.pl (vendor form) and SL/BK.pm. Build a payment run with review and pain.001 export, matching Figures B1–B4 of doc/enhancement/payment-run.md (write it first from this prompt).

Schema: upgrade script adding vendor.early_payment_discount numeric(5,4), vendor.payment_clearing_accno_id int, vendor.remittance_type text default 'NONE'; tables paymentrun and paymentrunline with the columns listed in the Data paragraph of design B; mirror in Pg-tables.sql; bump dbversion.

Module: SL/PR.pm with propose(bank accno, duedateto, datepaid, vendor, department, approved_only) selecting open ap rows (amount - paid > 0) and computing discount_N from vendor.early_payment_discount when datepaid <= transdate + discountterms; save_run (Figure B2 fields: checked_N, id_N, discount_N, paid_N); submit_for_approval (approvalrule mechanism if present, else status APPROVED with login and timestamp); review_save (Figure B3: checked_N, datepaid_N, accno_N, paid_N, notes); approve; send_back; reject; mint end_to_end_id "L123---" at approval; export_pain001 producing pain.001.001.09 valid against the Swiss Payment Standard (GrpHdr MsgId = export_message_id, InitgPty from defaults.company, one PmtInf per debit account with ReqdExctnDt = datepaid, Dbtr, DbtrAcct IBAN from bank.iban, ChrgBr SLEV; per line CdtTrfTxInf with InstrId = export_instruction_id, EndToEndId, Amt, Cdtr with structured address where the vendor record has one, CdtrAcct IBAN, RmtInf Strd CdtrRefInf Tp Cd SCOR or Prtry QRR with remittance_ref, else Ustrd transaction_text), storing file_content and setting status EXPORTED; mark_loaded; revert_to_new; reexport; mark_paid_from_bankline(end_to_end_id, bankline) called from BK::suggest, posting through CP::post_payment (datepaid = the bank line's booking_date, source = end_to_end_id) and writing acc_trans.bankline_id and paymentrunline.acc_trans_id.

UI: bin/mozilla/pr.pl (symlink pr.pl → dispatch.pl) with actions propose, save, submit_for_approval, review, approve, send_back, reject, export, mark_loaded_at_bank, revert_to_new, reexport, delete_run, list, open_document; a Payment Runs report; ct.pl vendor form gains the Figure B1 rows; menu.ini [Cash--Payment Run] and [Cash--Reports--Payment Runs]; locale entries.

Tests: t/payment_run.t creating two vendor invoices (one with a QRR dcn, one with SCOR), proposing, saving, approving, exporting, validating the XML with XML::LibXML against a bundled pain.001.001.09.ch.03.xsd in t/xsd, then simulating a camt.054 bankline with the first EndToEndId and asserting that invoice is paid and linked while the second stays EXPORTED; and that revert_to_new after export clears the ids. perl -c everything; run the test.

CMatching in the database

Data and method

The fork’s search_irrelevant_words (search_target RELATED_PARTY or COMMON, word) seeded with legal forms and stopwords in German, French, Italian and English, and its plpgsql to_filtered_tsvector(input, filter_type, ts_config); we add generated columns customer.name_tsv and vendor.name_tsv with GIN indexes, fuzzystrmatch for a Levenshtein fallback, and bankmemory (id, bank_id, pattern, vc, vc_id, chart_id, hits, last_used), written on every accepted line. BK::match_party returns candidates ranked memory → ts_rank → Levenshtein → none, and the reason and score land on bankline.suggest_type and suggest_score, which Figure A3 shows.

Screens

System → Bank Accounts → Matching Wordsmock-up · nothing submits
Add word word ignore when matching search_target
TargetWordOrigin
RELATED_PARTYagseed
RELATED_PARTYgmbhseed
RELATED_PARTYllcseed
RELATED_PARTYc/othis dataset
COMMONundseed
Memory what accepting lines has taught this bank
Pattern patternResolves toHits hitsLast used
UNKNOWN PAYERcustomer Bergwerk GmbH325.08.2026
KARTENGEB*6900 Bankspesen1424.08.2026

Figure C1. The only screen the matcher has: a firm’s own noise words, and the memory it can inspect and un-learn. Everything else shows up as the reason column of Figure A3.

Claude Code prompt
Read CLAUDE.md and SL/BK.pm. Add database-side party matching for bank lines, matching Figure C1 of doc/enhancement/bank-match.md (write it first).

Schema upgrade: CREATE EXTENSION IF NOT EXISTS fuzzystrmatch; table search_irrelevant_words(id serial, search_target text, word text, UNIQUE(search_target, word)) seeded exactly as the runmyaccounts fork's 2.8.42 upgrade seeds it plus 'inc', 'corp', 'co', 'ltd', 'plc', 'lp', 'kg', 'eg', 'e.v.'; plpgsql function to_filtered_tsvector(input text, filter_type text default 'COMMON', ts_config regconfig default 'simple') removing the words and returning a tsvector; generated columns customer.name_tsv and vendor.name_tsv = to_filtered_tsvector(name, 'RELATED_PARTY') with GIN indexes; table bankmemory(id serial, bank_id int, pattern text, vc text, vc_id int, chart_id int, hits int default 0, last_used timestamptz).

Module: in SL/BK.pm add match_party($dbh, $name, $amount, $credit_debit, $bank_id) returning [{vc, id, name, score, reason}] using in order: exact bankmemory pattern (reason 'memory') → ts_rank of to_filtered_tsvector(name) against name_tsv (reason 'name', score = rank normalised 0..1) → levenshtein_less_equal(lower(filtered name), lower(candidate), 3) (reason 'fuzzy') → none; remember_match(bank_id, name, vc, vc_id, chart_id) called from accept_line, match_line and post_as, incrementing hits. Wire into suggest so suggest_type and suggest_score are stored.

UI: in am.pl a Matching Words sub-screen under System → Bank Accounts with the Figure C1 forms: add/delete words (name=word, search_target) and forget memory rows; menu.ini [System--Bank Accounts--Matching Words]; locale entries.

Tests: t/bank_match.t with customers "Müller AG", "Mueller Treuhand GmbH", vendor "Swisscom (Schweiz) AG" and bank names "MUELLER AG ZUG", "SWISSCOM AG", "Muller Treuhand" asserting the expected top candidate and reason, and that after remember_match the same name returns reason 'memory' first. perl -c; run the test.

DThe MWST return and the ESTV

Data

The fork’s tables kept as they are: vat_settlement (id, period_from, period_to, method EFFECTIVE / NET_TAX_RATE, type NORMAL_SETTLEMENT / CORRECTION_SETTLEMENT / ANNUAL_RECONCILIATION, status DRAFT / RELEASED / FILED / PAID, data jsonb holding every form box, xml bytea for the eCH-0217 document, meldecenter_data jsonb for what the portal returned, filed_at, filed_ref, login) and booking_to_settlement (booking_id = trans_id, settlement_id) locking each booking to the return it was reported in. tax keeps the fork’s vatkey, formdigit (the form box), validfrom, reversecharge_id and inactive. Defaults gain vat_method and vat_period.

Screens

System → Taxesmock-up · nothing submits
Account accnoDescriptionRate % taxrate_NTax number taxnumber_NVAT key vatkey_NForm box formdigit_NValid from validfrom_NValid to validto_NReverse charge reversecharge_N
2200MWST Umsatz Normalsatz
2200MWST Umsatz Normalsatz (bis 2023)
1170Vorsteuer Material, Waren, Dienstleistungen

Figure D1. System → Taxes with the fork’s four columns. A rate change is a new row with validfrom, not an edit — which is how the 7.7% and 8.1% rows coexist and old bookings keep their box.

Reports → MWST Returnmock-up · nothing submits
Period fromdate to todate   or period
Type type    
Method method from Defaults; change there
Settlement 2026-Q2 draft
BoxLineTurnoverRateTax CHFBookings
200Total consideration agreed or received412'380
220Exempt supplies (export)−38'000 7
289Total deductions−38'000
299Taxable turnover374'380
302Standard rate371'2008.1%30'067.20 214
312Reduced rate3'1802.6%82.68 9
399Total tax due30'149.88
400Input tax on goods and services9'812.40 388
405Input tax on investments1'104.00 6
500Amount payable19'233.48

Checks: 3 bookings with tax but no box 1 booking dated in a released period

locks 617 bookings to 2026-Q2

Figure D2. The All Taxes report arranged as the federal form. Release freezes the bookings; Workings PDF is the pack the fork uploads to its software after filing; Payment Run is design B for box 500; Mark Filed records what the ESTV portal returned into meldecenter_data.

Reports → MWST Return → Bookings · box 400 · 2026-Q2mock-up · nothing submits
Date transdateReference referenceDescriptionAccount accnoNet lineamountTax taxamountKey vatkeySettlement
03.04.2026AP 88012Swisscom AG6510 Telefon360.2229.18VM81
11.04.2026AP 88031Brack.ch1500 Mobiliar1'850.00149.85VM81405?
30.06.2026GL 2026-0411Spesen Juni6640 Reisespesen412.9033.44VM81
388 bookings121'140.749'812.40

Figure D3. Every box expands to its bookings, each a link to the transaction; the last column shows which settlement already owns a line, which is how a correction knows its delta.

Workflow

The period engine of chapter 1 item 11 computes the boxes from acc_trans.tax, taxamount and lineamount (design of item 3 in section 15.6), applying defaults.vat_method. Release writes booking_to_settlement and every posting path refuses to touch those bookings unless a correction settlement is open; the correction reports only the delta — the fork’s type enum. The annual reconciliation reconciles the periods to the income statement’s turnover, which is the ESTV’s Umsatzabstimmung. Filing follows the fork’s own procedure: documents in by the 10th after the period, the preparer fills the form in the client’s ESTV online account under a power of attorney, the client logs in and releases it; the eCH-0217 XML is what that portal accepts, and a direct upload is a later step behind the same button.

Integration points

API: GET /vat/settlements, POST /vat/settlements {fromdate, todate, type, parent_id}, POST /vat/settlements/{id}/release, GET /vat/settlements/{id}.xml, GET /vat/settlements/{id}/bookings?box=400; webhook vat.released. The accountant seat of chapter 1 item 8 computes and releases; the client’s login files.

Claude Code prompt
Read CLAUDE.md, SL/RP.pm, bin/mozilla/rp.pl and bin/mozilla/am.pl (taxes). Build the Swiss VAT (MWST) settlement on per-line tax data, matching Figures D1–D3 of doc/enhancement/mwst.md (write it first, with the box list for the current ESTV form: 200, 205, 220, 221, 225, 230, 235, 280, 289, 299, 302, 303, 312, 313, 342, 343, 382, 383, 399, 400, 405, 410, 415, 420, 479, 500, 510, 900, 910 and the net-tax-rate variant 322/332).

Prerequisite: acc_trans rows carry tax (accno text), taxamount and lineamount as in the runmyaccounts fork; if absent, add them by upgrade and populate them in AA::post_transaction, GL::post_transaction, IS::post_invoice and IR::post_invoice from the existing tax calculation.

Schema: tax.vatkey varchar(5), tax.formdigit int default 0, tax.validfrom date, tax.reversecharge_id int, tax.inactive bool default false; defaults rows vat_method and vat_period; tables vat_settlement and booking_to_settlement with the columns of design D's Data paragraph; mirror in Pg-tables.sql; bump dbversion.

Module: SL/VAT.pm with compute(fromdate, todate, method) returning box totals and per-box booking lists from acc_trans joined to tax by accno and validfrom/validto; checks() listing bookings with tax but no formdigit and bookings inside released periods; save_draft (data jsonb); release (inserts booking_to_settlement in one transaction, status RELEASED); open_correction(parent_id) computing deltas against bookings already owned; annual_reconciliation(year); to_ech0217(id); workings_pdf(id) rendering Figure D2 and the box lists through the existing print path; mark_filed(id, filed_ref, meldecenter_data); a guard in AA/GL/IS/IR post and delete paths that dies with a locale message when a trans_id is in booking_to_settlement and no correction settlement is open.

UI: bin/mozilla/vat.pl (symlink vat.pl → dispatch.pl) with actions compute, save_draft, release, bookings, show_checks, print_form, export_ech0217, workings_pdf, payment_run (hands box 500 to design B if present), mark_filed, list; am.pl taxes screen gains the Figure D1 columns (vatkey_N, formdigit_N as a select of the box list, validfrom_N, reversecharge_N); Defaults gains vat_method and vat_period; menu.ini [Reports--MWST Return]; locale entries.

Tests: t/vat.t posting a standard-rate sale, a reduced-rate sale, an export and two purchases (one to an investment account), asserting boxes 302, 312, 220, 400, 405 and 500; release; assert an edit attempt dies with the locked message; open a correction with one more purchase and assert only the delta is reported; validate the eCH-0217 XML against a bundled schema in t/xsd. perl -c all files; run tests.

EThe closing app: questions, checklist, package

Data

The fork’s financial_year (id, start_date, end_date, status OPEN / CURRENT_FINANCIAL_YEAR / CLOSED, yearend_id) and its xabschluss (id, financial_year_id, data jsonb, status NEW / IN_PROGRESS / IN_REVIEW / CLOSED, xml nullable) with xabschluss_history; closingstep (id, xabschluss_id, seq, key, label, kind CHECK / QUESTION / TASK / REPORT, state TODO / DONE / SKIPPED, done_by, done_at, note, answer jsonb, checked_hash, trans_id of the entry it produced) — the fork’s xcontrolling_log generalised into an ordered list; and closingtemplate (seq, key, label, kind, question, module, action). The eighteen steps of the fork’s app are not published; what a Swiss year-end contains is, and it is the template.

Screens

System → Year-end → Closing 2025mock-up · nothing submits
Financial year financial_year_id in progress 11 of 18 done  
#Step keyKindState stateBy / onResult or note
1Bank balances agree to statementscheckdoneA.S. 12.02.20263 accounts, 0 difference
2Open receivables reviewedtaskdoneA.S. 12.02.20262 written off → GL 2026-0007
5Depreciation postedcheckdoneA.S. 14.02.2026register run 2025 → GL 2025-0421
6Stock counted and valuedquestiondoneM.K. 15.02.202688'410.00 at 31.12 → GL 2025-0425
8VAT reconciled to turnovercheckin reviewannual reconciliation 2025: 1'240.00 unexplained
9Payroll agreed to AHV statementtasktodo
10Private share of vehicle and phonequestiontodo
11Invoices received after year end for 2025questiontodo
17Post year-endtasktodoSystem → Yearend
18Seal archivereporttodo

Figure E1. A checklist whose check steps compute their own result and refuse to be ticked while it fails, whose task steps a person ticks, and whose question steps open Figure E2. The chat of design F hangs off every row.

System → Year-end → Closing 2025 → Step 10 · Private share of vehicle and phonemock-up · nothing submits

A question in business language; the answer becomes a proposed entry. This is the fork’s closing app in one screen.

Did you use the company car privately in 2025? answer_private_car  
Purchase price of the car answer_car_cost from the fixed asset register; 0.9% a month = 5'184.00 a year
Phone used privately? answer_private_phone % of 6510 Telefon (2'184.60)
Proposed entry trans_id
Account accno_NDebit debit_NCredit credit_NTax tax_NMemo memo_N
Date transdate Reference reference Note for the auditor note

Figure E2. The answer is stored in closingstep.answer; the entry it generates is an ordinary GL posting whose trans_id is kept on the step, with the note as the auditor’s documentation. Other question steps: stock at year end, invoices received after the close, unpaid bonuses, loans and their interest, provisions, prepaid and accrued items.

System → Year-end → Closing Stepsmock-up · nothing submits
Seq seq_NKey key_NLabel label_NKind kind_NOpens module_N

Figure E3. A practice edits the template once; every client’s closing run is copied from it.

System → Year-end → Closing 2025 → Packagemock-up · nothing submits
DocumentSourceHash checked_hash
Balance sheet 31.12.2025Reports → Balance Sheet9f2c41…
Income statement 2025Reports → Income Statementb71ae0…
Trial balance, with adjustments columnReports → Trial Balance
AR and AP ageing at 31.12.2025AR/AP → Reports
VAT settlements 2025-Q1..Q4 and annual reconciliationdesign D
Bank reconciliations, 3 accountsdesign A
Closing entries with the answers that produced themFigure E2, all steps
Archive log 2025design G
Format format     Manifest manifest manifest.json of hashes

Figure E4. The year-end package: every report rendered and hashed, so a later edit is detectable — the fork’s checked_hash, applied to documents instead of bookings.

Workflow and integration

Opening a closing copies the template into closingstep rows. Check steps compute (bank differences from design A’s reconciled imports, unposted depreciation, unreleased VAT periods, an FX adjustment dated year end); question steps generate entries; task steps are ticked; report steps render into the package. Submit for Review hands the run to the accountant seat, which is the fork’s closing adviser; Close Year calls AM::post_yearend and moves financial_year to CLOSED. API: GET /closing/{year}, POST /closing/{year}/steps/{key} with an answer or a note, GET /closing/{year}/package; webhook closing.step_done.

Claude Code prompt
Read CLAUDE.md, SL/AM.pm (yearend), SL/RP.pm and SL/GL.pm. Build the closing app: a year-end checklist with question steps that generate entries, and a closing package, matching Figures E1–E4 of doc/enhancement/closing.md (write it first, listing the 18 default steps: bank_balances, open_receivables, open_payables, accruals_deferrals, depreciation, stock, fx_revaluation, vat_reconciled, payroll_ahv, private_share, late_invoices, bonuses_unpaid, loans_interest, provisions, trial_balance_reviewed, statements_rendered, post_yearend, seal_archive, each with kind and, for questions, the fields of the question form and the entry it proposes).

Schema: financial_year, xabschluss, xabschluss_history, closingstep and closingtemplate with the columns of design E's Data paragraph (copy the fork's financial_year definition and its enum); seed closingtemplate with the 18 steps; upgrade script; Pg-tables.sql; bump dbversion.

Module: SL/CL.pm with open_closing(financial_year_id) copying the template; step_check(key) implementing bank_balances (each bank chart balance at end_date against the last RECONCILED bankimport.closing_balance), depreciation (assets without a depreciation row for the year if a fixedasset table exists, else skip), vat_reconciled (four RELEASED settlements and an ANNUAL_RECONCILIATION whose difference is 0), fx_revaluation (an FX Adjustment dated end_date); answer_question(key, fields) storing closingstep.answer and returning a proposed entry as the accno_N/debit_N/credit_N/tax_N/memo_N structure GL::post_transaction takes; post_and_mark_done posting it and storing trans_id and a sha256 of the answer plus entry as checked_hash; mark_done(key, note); skip(key, reason); build_package(id, docs, format) rendering the chosen reports through the existing print paths into a zip with manifest.json of sha256 hashes; submit_for_review; close_year calling AM::post_yearend and setting financial_year CLOSED and xabschluss CLOSED with a history row.

UI: bin/mozilla/cl.pl (symlink cl.pl → dispatch.pl; note the fork's cl.pl is the clearing screen — ours is closing; name the clearing screen of design J cq.pl) with actions open_closing, show, answer, save_answer, post_and_mark_done, not_applicable, mark_done, skip, ask_accountant (design F), build_package, download, send_to_accountant, submit_for_review, close_year; a System → Year-end → Closing Steps editor (Figure E3, rows seq_N/key_N/label_N/kind_N/module_N with Save and Restore Defaults); menu.ini [System--Year-end--Closing] and [System--Year-end--Closing Steps]; locale entries.

Tests: t/closing.t opening a closing on a test dataset, asserting 18 steps; that bank_balances fails with no reconciled import and passes after one; that answering private_share with a 48'000 car and 30% of a 2'184.60 phone proposes a balanced three-line entry, posts it, and stores trans_id and checked_hash; that build_package yields a zip whose manifest hashes match its files; and that close_year sets both statuses. perl -c everything; run tests.

FChat on bookings, and the change feed

Data

The fork’s chat (id, trans_id, message, employee_id, creation_date) kept and widened: parent_id, login, mentions text[], resolved_at, resolved_by, and closingstep_id so a thread can hang off a closing step as well as a transaction. Their customer.last_modified and vendor.last_modified with the trigger update_last_modified_column() ported as they are, plus a view changefeed(kind, id, modified_at) over audittrail, customer, vendor and chat.

Screens

AR → Invoice 2026-0143 · Mueller AG · 2'150.00 · posted 12.08.2026 → Chatmock-up · nothing submits

A. Ciccone14.08.2026 09:12
Is the 2% discount on line 3 intentional? Customer terms say 0.

M. Keller14.08.2026 10:40
Yes — agreed by phone, see note on the order.

Message message
Notify mentions mails through SL::Mailer and lists on their dashboard

Figure F1. A collapsible section at the foot of every AR, AP and GL transaction screen and every closing step, rendered by one shared sub in arap.pl. This is the fork’s “chat on bookings with your online accountant”.

Dashboard → Open threadsmock-up · nothing submits
Show filter
OnLast messageFromSince
AP 88201 Hotel Adler 640.00Which project was this for?A. Ciccone2 days
Closing 2025 · step 8 VAT reconciled1'240.00 unexplained — any cash sales in December?A. Ciccone5 days
Bank line 90322 +1'000.00 UNKNOWN PAYERsee clearing questionsystem1 day

Figure F2. The dashboard list, with a badge count in the menu. The MCP server of chapter 9 item 3 reads the same list so an assistant can summarise what is waiting on whom.

Integration

API GET /transactions/{id}/chat, POST the same with {message, mentions}, POST /chat/{id}/resolve; webhook chat.posted. The feed: GET /changes?since= returning customers, vendors, transactions and chat changed after a timestamp — the fork’s last_modified made general, which is what their CRM sync polls.

Claude Code prompt
Read CLAUDE.md, bin/mozilla/arap.pl and SL/Mailer.pm. Add a per-transaction chat and a change feed, matching Figures F1–F2 of doc/enhancement/chat.md (write it first).

Schema: table chat(id serial, trans_id int, closingstep_id int, parent_id int, login text, message text, mentions text[], creation_date timestamptz default now(), resolved_at timestamptz, resolved_by text); customer.last_modified and vendor.last_modified timestamptz with the trigger function update_last_modified_column() copied from the runmyaccounts fork's 2.8.46 upgrade; a view changefeed(kind, id, modified_at) unioning audittrail (transdate), customer, vendor and chat; upgrade script; Pg-tables.sql; bump dbversion.

Module: SL/Chat.pm with list(trans_id | closingstep_id), post(fields: trans_id, closingstep_id, parent_id, login, message, mentions[]) parsing @login mentions out of the text as well as the select, mailing each mentioned employee through SL::Mailer from defaults.noreplyemail or the sender's e-mail with a link to the transaction, resolve(id, login), reopen(id), open_for(login, filter) for Figure F2, and changes_since(timestamp).

UI: a shared sub chat_section in bin/mozilla/arap.pl rendering Figure F1 (thread, textarea name=message, select name=mentions multiple, hidden trans_id/parent_id, buttons Send / Resolve / Reopen) called at the foot of the display subs in aa.pl, gl.pl, is.pl, ir.pl and of cl.pl steps; a dashboard section and menu badge with the unresolved count for the login; menu.ini [Dashboard--Open Threads] if a dashboard menu exists, else under [Reports]; locale entries for the new labels.

Tests: t/chat.t posting a message with a mention on a test transaction, asserting it lists, that the mentioned login sees it in open_for, that resolve hides it and reopen restores it; and that updating a customer bumps last_modified and appears in changes_since. perl -c everything; run tests.

GThe document inbox: capture, recognition, validation, archive

Data

The receipt inbox of chapter 1 item 4 stores files in Reference Documents (archive, archivedata). Add to archive: hash sha256, signed_at, signature, text_tsv from OCR text, formname as the document class (vendor_invoice, receipt, customer_invoice, bank_statement, mail, other), status NEW / RECOGNISED / VALIDATED / POSTED / FILED, extracted jsonb with the recognised fields and their confidences, trans_id once posted; and an append-only archivelog (day, root, count, signature). Signing the hash with a server key and rolling a daily Merkle root is what the fork’s brochure calls a GeBüV signature: proof a document has not changed since capture.

Screens

Reference Documents → Inbox → Add Documentmock-up · nothing submits
File file or mail it to [email protected], or photograph it in the app (design I)
Class formname Folder folder
Description description Link to trans_id
hashed and signed on arrival; recognition runs in the background

Figure G1. rd.pl’s Add Document with a class and a folder; the same form serves upload, the mail address and the API.

Reference Documents → Inbox → Validate · swisscom-2026-08.pdfmock-up · nothing submits
PDF preview
page 1 of 2
rendered by the existing viewer

What recognition read, with its confidence; a person confirms or corrects before anything posts. The fork calls this “validation by specialists”.

Vendor vendor 0.98
Invoice invnumber 0.97 Date transdate 0.99 Due duedate 0.71
Amount amount curr 0.99   Tax tax_1 0.95
QR reference dcn 0.99 IBAN matches the vendor’s
Expense account accno_1 Department department Project projectnumber_1
Description description Handwritten note read notes

Figure G2. The validation queue: the fields are ir.pl’s own (vendor, invnumber, transdate, duedate, dcn, accno_1, tax_1), so Confirm opens the AP invoice screen prefilled — or, with stations (design H), routes the draft there; the document is attached and its status moves to VALIDATED then POSTED.

Reference Documents → Inbox → Verify · swisscom-2026-08.pdf → AP draft 88201mock-up · nothing submits

The transaction exactly as it will post, beside the page it was read from. Each numbered flag on the left is the same number on the document; click one to jump to it. Nothing posts until Post.

Vendor Invoice draft id
Vendor vendor1 0.98
Invoice invnumber2   Order ordnumber
Date transdate3 Due duedate 4 0.71 — computed from “30 Tage netto”
Currency currency Rate exchangerate  
Description description Department department
Account accno_NDescription description_NAmount amount_NTax tax_N
56
78 check
Subtotal360.22
VM81 8.1% tax_2200929.18matches
Total10389.40matches
QR reference dcn11 IBAN 12 = vendor’s
Notes notes Document archive_id
page of 2
Swisscom (Schweiz) AG1
Alte Tiefenaustrasse 6, 3050 Bern
CHE-101.654.423 MWST
Muster Consulting GmbH
Bahnhofstrasse 12
8001 Zürich
Rechnung Nr. 882012 · Datum 04.08.20263
Zahlbar innert 30 Tagen netto4 · Kunden-Nr. 7710455
Projekt Zürich (handwritten)
LeistungPeriodeCHF
Abo Mobile Business M501.08.–31.08.2026360.226
Auslandgespräche701.08.–31.08.20260.008
MWST 8.1%29.189
Total inkl. MWST389.4010
QR
Konto CH44 3199 9123 0008 8901 212
Referenz 21 00000 00003 13947 14300 0901711
Betrag CHF 389.40

Rendered page image; flags are positioned from the OCR word boxes. Unflagged text was not used.

Figure G3. The verification screen: ir.pl’s vendor-invoice form on the left, populated by recognition, and the document on the right with every read value flagged by the same number. The two totals are checked against the page; a line the reader found doubtful is marked. Post posts through IR::post_invoice with the document attached; Send to Station hands the draft to design H.

Reference Documents → Searchmock-up · nothing submits
Search q Year year Class formname Status status
DateDocument filenameClassLinked to trans_idStatusSigned signed_atHash hash
04.08.2026swisscom-2026-08.pdfvendor invoiceAP 88201 Swisscom 389.40posted04.08.2026 17:029f2c…
02.07.2026swisscom-2026-07.pdfvendor invoiceAP 87455 Swisscom 389.40posted02.07.2026 09:41b71a…
26.08.2026IMG_4471.jpgreceiptnew26.08.2026 12:10e04d…
2026: 1'204 files, 0 altered, last root sealed 26.08.2026 00:05

Figure G4. Full-text search over the OCR text of every stored document, with signature state; Verify Archive recomputes every hash against the daily log.

Integration

Every file entering by upload, mail or POST /documents is hashed and signed on arrival; recognition (the extraction of chapter 9 item 2, or a stub that reads only the QR code) writes extracted; GET /documents?q= searches; GET /documents/inbox lists what waits for validation. The closing package of design E includes the year’s archivelog.

Claude Code prompt
Read CLAUDE.md, SL/RD.pm, bin/mozilla/rd.pl and bin/mozilla/ir.pl. Turn Reference Documents into a document inbox with recognition, a validation queue and a verifiable, searchable archive, matching Figures G1–G4 of doc/enhancement/inbox.md (write it first).

Schema: archive.hash text, archive.signed_at timestamptz, archive.signature text, archive.text_tsv tsvector with a GIN index, archive.formname text, archive.folder text, archive.status text default 'NEW', archive.extracted jsonb, archive.trans_id int, archive.login text, archive.created_at timestamptz default now(); table archivelog(day date primary key, root text, count int, signature text); upgrade script; Pg-tables.sql; bump dbversion.

Module: in SL/RD.pm, on save compute sha256 of the bytes, sign it with a server Ed25519 key at $userspath/archive.key (generate on first use; document in doc/), store both; extract text (pdftotext, tesseract if installed, else empty) into text_tsv; recognise(id) filling extracted with vendor (matched through BK::match_party if present, else exact name), invnumber, transdate, duedate, amount, curr, tax, dcn and iban from the Swiss QR code when present (decode with the existing QR tooling if any, else parse the SPC text block from pdftotext output), and a confidence per field; validate(id, fields of Figure G2) returning the $form ir.pl expects so Confirm as Vendor Invoice opens ir.pl prefilled with the document attached, or route(id) to design H when stations exist; reject(id, note); search(q, year, formname, status) using websearch_to_tsquery; nightly bin/archive-seal.pl computing a Merkle root over the day's hashes into archivelog and signing it; verify(year) recomputing every hash and signature and reporting alterations. Inbound mail: a bin/inbox-mail.pl that reads a message from STDIN (procmail/sieve), saves each attachment through the same save path with formname from the subject or 'other', and creates a captured expense (design I) when the sender is an employee.

UI: rd.pl gains Figure G1 fields (formname, folder, trans_id, confidential) on Add Document, an Inbox list (status NEW/RECOGNISED) with Validate opening Figure G2 (vendor, invnumber, transdate, duedate, amount, curr, tax_1, dcn, iban, accno_1, department, projectnumber_1, description, notes; buttons Confirm as Vendor Invoice / Send to Station / Split / Ask Client / Reject), a Verify screen (Figure G3: ir.pl's own vendor-invoice fields — vendor, invnumber, ordnumber, transdate, duedate, currency, exchangerate, taxincluded, description, department, accno_N, description_N, amount_N, tax_N, projectnumber_N, dcn, iban, notes, archive_id — beside a document pane rendered from the stored PDF/image with numbered flags positioned from the OCR word boxes, and totals checked against the page; buttons Post / Save Draft / Send to Station / Re-run Recognition / Reject), and Figure G4 search with signed/hash columns and Verify Archive; menu.ini [Reference Documents--Inbox] and [Reference Documents--Search]; locale entries.

Tests: t/archive.t storing a PDF with a synthetic Swiss QR text block, asserting hash and a valid signature, that recognise extracts vendor, amount, dcn and iban, that validate returns an ir.pl-shaped form, that altering the stored bytes makes verify report it, and that search finds a word from the text. perl -c everything; run tests.

HWorkflow stations

Data

The approval rules of chapter 4 item 3 extended with station (id, seq, name, description, next_station_id, can_delete, can_to_payment), stationmember (station_id, login, can_approve, can_code, substitute_login, substitute_after_days), stationrule (id, seq, amount_over, vendor_new, department_id, formname, first_station_id) and approval gaining station_id, archive_id and waiting_since. A document is routed to a first station by a rule; each station passes it on, sends it back or rejects it; every move is an audittrail row.

Screens

System → Stationsmock-up · nothing submits
Seq seq_NStation name_NMembers login_NMay code can_code_NSubstitute after substitute_after_days_NThen next_station_id_NMay
days →
days →
Routing rules
SeqAmount over amount_over_NNew vendor vendor_new_NDepartment department_NClass formname_NFirst station first_station_id_N
1
2
no rule matched: post directly

Figure H1. Stations, members, substitutes and the moves each station may make — the arrows of the fork’s graph, as columns.

AP → Approvals · my stations: Projects 4 · Management 1mock-up · nothing submits
Station station_id
DocumentVendorAmountArrivedWaiting waiting_sinceFrom
AP draft 88201 approval_idHotel Adler640.00inbox, 22.08.20262 daysM. Keller
AP draft 88203Muster Consulting4'800.00inbox, 23.08.20261 dayvalidation
AP draft 88204Brack.ch1'850.00inbox, 25.08.2026< 1 dayvalidation

Figure H2. The queue with the fork’s number flags. After substitute_after_days the substitute sees the same list.

AP → Approvals → AP draft 88201 · Hotel Adler · 640.00 · station Projectsmock-up · nothing submits
hotel-adler-4471.pdf
from the inbox, signed 22.08
Vendor vendorHotel Adler   Invoice invnumber 4471   Date transdate 20.08.2026   Due duedate 02.09.2026   Total amount 640.00 CHF
Coding editable at this station (can_code)
Account accno_NAmount amount_NTax tax_NDepartment department_NProject projectnumber_NDescription description_N
Note notes
→ Management

History: inbox (M. Keller 22.08) → Projects (you) → Management → posted

Figure H3. The station edits the draft’s coding in ir.pl’s own line vocabulary; Approve at the last station calls the module’s post sub. Paid documents drop into the archive of design G, which is where the fork’s graph ends too.

System → Stations → Overviewmock-up · nothing submits

The fork’s workflow visualisation: stations as boxes, the allowed moves as arrows, a number flag for what is waiting. Click a box to edit it (Figure H5) or open its queue (Figure H2).

Inbox7validation queue (G2)
rule: > 500 or new vendor
1 · Projects4mkeller, jroos · may code
forward
2 · Management1aciccone · may delete, to payment
post
Posted38this month
paid (design B)
ArchiveGsigned, searchable

Back arrows: Projects → Inbox (send back) · Management → Projects (send back) · Management → deleted. Rule 2 routes new vendors straight to Management.

Figure H4. The overview page a partner opens on Monday: where the documents are, who is sitting on them, and the shape of the process in one glance.

System → Stations → Edit Station · 1 Projectsmock-up · nothing submits
Sequence seq Name name
Description description
Forward to next_station_id   Send back to back_station_id
This station may can_*      
Notify members notify   Escalate after escalate_after_days days to
Members stationmember
Login login_NNameMay approve can_approve_NMay code can_code_NSubstitute substitute_login_NAfter substitute_after_days_NOnly documents where
mkellerM. Keller days
jroosJ. Roos days
Add member login_3
delete is refused while documents wait here

Figure H5. One station in full: its moves, its notifications and escalation, and each member’s rights, substitute and scope. The grid of Figure H1 is this form’s summary.

AP → Approvals → AP draft 88201 → Send Backmock-up · nothing submits
Send to to_station_id    
Reason reason
Note notes
Notify notify_login
reject ends the workflow; the document stays in the archive marked rejected

Figure H6. The one dialog both Send Back and Reject open: a target, a reason from a list (so the report of Figure H7 can count them), a mandatory note, and a mail.

AP → Approvals → AP 88201 → Historymock-up · nothing submits
When transdateWho employee_idStationAction actionChangeNote
22.08.2026 09:14M. KellerInboxcapturedexpense app, photo
22.08.2026 09:31recognitionInboxrecognisedvendor 0.96, amount 640.00, date 20.08
22.08.2026 11:02A. CicconeInboxvalidated6640 Reisespesen 640.00 → station Projects (rule 1)
24.08.2026 08:40J. RoosProjectssent back→ Inboxproject missing
24.08.2026 10:05A. CicconeInboxvalidatedproject ZH-FAIR-26 → Projects
26.08.2026 14:20M. KellerProjectsapprovedsplit 580.00 / 60.00, 6640 / 6641, dept Sales → ManagementTrade fair, approved budget
27.08.2026 09:02A. CicconeManagementapproved→ posted AP 88201
29.08.2026 06:00payment run 2026-031paidbank line 90441, E2E L123-DEMO-31-3
Across all documents fromdate to todate Station station_id documents per station, average days waiting, send-backs by reason, approvals per member

Figure H7. The document’s life as audittrail rows (formname = approval), from capture to payment, and the same rows summarised into the workflow report a partner asks for at month end.

HR → Employees → Edit Employee · M. Keller → Stationsmock-up · nothing submits

The same memberships seen from the person: the tab a manager uses when someone joins, leaves or goes on holiday.

StationMember member_NMay approve can_approve_NMay code can_code_NMy substitute substitute_login_NWaiting for me
1 · Projects4
2 · Management
Expense approver for employee.managerid of each, design I
Out of office absent_from to absent_to substitutes take over immediately during these dates, not after N days
moves everything waiting for this person to their substitutes now — the leaving-employee button

Figure H8. Memberships, substitution, expense-approver relationships and absence on the employee record, so that one screen answers “what happens to the queue when M. Keller is away or leaves”.

Integration

The validation queue of design G feeds station one; e-mail and the dashboard tell a station’s members what waits. API GET /approvals?station=, POST /approvals/{id}/approve {lines, notes}, POST /approvals/{id}/send_back; webhook approval.moved. The payment run of design B proposes only invoices that have left their last station.

Claude Code prompt
Read CLAUDE.md and doc/enhancement/approvals.md (write it first if absent from chapter 4 item 3 of the enhancement book: approvalrule and approval tables). Add workflow stations, matching Figures H1–H8 of doc/enhancement/stations.md (write it first).

Schema: station(id serial, seq int, name text, description text, next_station_id int, back_station_id int, can_delete bool, can_to_payment bool, can_edit_header bool, notify text default 'DAILY', escalate_after_days int, escalate_station_id int), stationmember(station_id int, login text, can_approve bool default true, can_code bool default false, substitute_login text, substitute_after_days int, scope text), employee.absent_from date, employee.absent_to date, stationreason(id serial, seq int, label text) seeded with the six reasons of Figure H6, stationrule(id serial, seq int, amount_over numeric, vendor_new bool, department_id int, formname text, first_station_id int), approval.station_id int, approval.archive_id int, approval.waiting_since timestamptz; upgrade script; Pg-tables.sql; bump dbversion.

Module: SL/WF.pm with route(document hashref, archive_id) applying stationrule rows in seq order to pick the first station or returning undef (post directly); queue(login, station_id) listing documents at the caller's stations including those inherited by substitution after substitute_after_days; open(approval_id) returning the draft in ir.pl's field vocabulary (vendor, invnumber, transdate, duedate, amount, accno_N, amount_N, tax_N, department_N, projectnumber_N, description_N, notes); approve(approval_id, login, fields) applying the coding edits when the member can_code, writing an audittrail row (formname 'approval', action "approved at "), and moving to next_station_id or, when none, posting through IR::post_invoice / AA::post_transaction; send_back(approval_id, note) to the inbox; reject(approval_id, note); history(approval_id); counts(login) for the badges.

UI: bin/mozilla/wf.pl (symlink wf.pl → dispatch.pl) with actions queue, continue, open, approve, add_line, send_back, reject, history; System → Stations: the grid editor (Figure H1: seq_N, name_N, login_N multiple, can_code_N, substitute_after_days_N, substitute_login_N, next_station_id_N, can_delete_N, can_to_payment_N, and the rules grid amount_over_N, vendor_new_N, department_N, formname_N, first_station_id_N), an Overview drawn as a station graph with waiting counts (Figure H4; plain HTML flex boxes and arrows, Open Queue / Edit / Add Station / Edit Rules / Print), and an Edit Station form (Figure H5: seq, name, description, next_station_id, back_station_id, can_code, can_delete, can_to_payment, can_edit_header, notify, escalate_after_days, escalate_station_id, and a members grid login_N, can_approve_N, can_code_N, substitute_login_N, substitute_after_days_N, scope_N, with Add Member / Remove; Delete Station refused while documents wait); a Send Back / Reject dialog (Figure H6: to_station_id, reason from a fixed list, mandatory notes, notify_login) used by both actions; a History screen per document plus a period report per station (Figure H7: audittrail rows with formname approval; report of documents per station, average days waiting, send-backs by reason, approvals per member; Export CSV); a Stations tab on HR → Employees → Edit Employee (Figure H8: member_N, can_approve_N, can_code_N, substitute_login_N per station, approves_for multiple writing employee.managerid, absent_from/absent_to during which substitutes take over immediately, and Hand Over All moving everything waiting for the person to their substitutes); a dashboard badge with the caller's waiting count; menu.ini [AP--Approvals] and [System--Stations]; locale entries. Vendor invoices confirmed in the validation queue (design G) call WF::route instead of opening ir.pl when a rule matches; PR::propose skips invoices with an open approval.

Tests: t/workflow.t with two stations, a rule for amounts over 500, a 640.00 draft asserting it lands at station 1, is recoded into two lines and approved to station 2, and posts after station 2 with the two-line coding and two audittrail rows; a 200.00 draft that bypasses stations; and substitution after the configured days. perl -c everything; run tests.

IThe expense app

Data and shape

Not a native app: the phone layout of chapter 1 item 10 as an installable web app with one capture screen. expense (id, employee_id, transdate, amount, curr, exchangerate, accno for the expense category, description, paymentmethod PRIVATE / COMPANY_CARD, projectnumber, department_id, tax, formname receipt / vendor_invoice, archive_id, status CAPTURED / VALIDATED / SUBMITTED / APPROVED / REJECTED / PAID, validated_by, approved_by, note, trans_id); employee.managerid for the approver (the fork’s Spesenverantwortlicher) and employee.vendor_id for reimbursement. The fork’s six-step flow is kept exactly: capture, crop, categorise, validate, submit, approve.

Screens

Expenses → Newphone
receipt photo
crop: drag the four corners, then Apply
Type formname
Category accno
Date transdate
Amount amount
Description description
Paid with paymentmethod
Project projectnumber
Advanced view
Business share business_pct %
Tax tax
Rate exchangerate

Figure I1. The fork’s capture screen: Kamera / Import / Ohne Beleg, the crop, Spesenbeleg or Lieferantenrechnung, the three mandatory fields (description, payment method, category), and the Erweiterte Ansicht for a private share. Mailing a receipt to the inbox address from the employee’s own e-mail creates the same row.

Expenses → Minephone
ExpenseCHFStatus status
26.08 Lunch, Bergwerk86.50validated
23.08 Train ZH–BE58.00validated
26.08 Parking9.00captured
18.08 Hotel Adler640.00submitted
July, 6 expenses412.90paid 05.08

Validated by the bookkeeper: check the figures, then submit to your approver.

Figure I2. The fork’s step five: the validated booking comes back to the employee, who confirms and forwards it to the approver. Submitted stays visible until the approver acts.

HR → Expenses → Validate · M. Keller · 26.08.2026 · 86.50mock-up · nothing submits
IMG_4471.jpg
cropped, signed 26.08
Date transdate Amount amount Rate exchangerate
Account accno Tax tax Department department_id Project projectnumber
Paid with paymentmethod   Question to employee note

Figure I3. The fork’s step four, the service centre’s validation, done by the bookkeeper or the accountant seat: the standardised data — date, amount, currency, rate, account, VAT — confirmed before the employee ever submits.

HR → Expenses → Approvemock-up · nothing submits
Employee employee Status status
EmployeeDateDescriptionAmountAccountPaid withReceipt
M. Keller26.08.2026Lunch with client Bergwerk86.506641privateyes
M. Keller23.08.2026Train ZH–BE58.006640privateyes
J. Roos23.08.2026Parking9.006640company cardno
Note notes
→ one AP transaction per employee (vendor = employee), receipts attached, into the next payment run

Figure I4. The approver: “generate a payment at the press of a button” is Approve Selected posting an AP transaction against the employee as vendor and adding it to design B’s next run; company-card expenses skip the run and match to the card statement through design A.

Claude Code prompt
Read CLAUDE.md, bin/mozilla/hr.pl, SL/HR.pm, SL/AA.pm and the inbox design (doc/enhancement/inbox.md). Build an expense claim flow with a phone-first capture screen, matching Figures I1–I4 of doc/enhancement/expenses.md (write it first).

Schema: expense table with the columns of design I's Data paragraph; employee.managerid int and employee.vendor_id int (a vendor record created on the first claim, name = employee name, iban/bic copied from the employee); upgrade script; Pg-tables.sql; bump dbversion.

Module: SL/EX.pm with capture(employee_id, file or archive_id, fields of Figure I1: formname, accno, transdate, amount, curr, description, paymentmethod, projectnumber, business_pct, tax, exchangerate) storing the file through SL/RD.pm (design G) and an expense row with status CAPTURED (a vendor_invoice type hands the file to the inbox instead); validate(id, fields of Figure I3, login) setting VALIDATED; ask_employee(id, note) posting a chat message (design F) to the employee; submit(ids, employee_id) requiring VALIDATED; approve(ids, login) grouping by employee and creating one AP transaction per employee through AA::post_transaction (vendor = employee.vendor_id, transdate = today, one line per expense with its accno, amount × business_pct, tax and department, projectnumber on the line, receipts linked by archive_id) with status APPROVED and trans_id, returning trans ids for PR::propose; company-card expenses instead post to the card's chart account and are marked for matching in design A; reject(ids, note); mine(employee_id); pending(login) for supervisors using employee.managerid.

UI: bin/mozilla/ex.pl (symlink ex.pl → dispatch.pl) with a mobile-first capture screen (Figure I1: single column, file input accept="image/*" capture="environment", crop via a small canvas script, the radio, selects and inputs named as shown, a details block for the advanced view), Mine (Figure I2, checked_N, Submit Selected, Edit), Validate (Figure I3) and Approve (Figure I4, checked_N, expense_id_N, notes) under HR → Expenses; a web app manifest and service worker so the capture screen installs to a home screen; menu.ini [HR--Expenses], [HR--Expenses--Validate], [HR--Expenses--Approve] and a home-screen link; locale entries. Inbound mail: bin/inbox-mail.pl creates a CAPTURED expense when the sender matches employee.email.

Tests: t/expense.t capturing two expenses for a test employee (one with business_pct 50), validating them, submitting, approving as the manager, and asserting one AP transaction with two lines at the right amounts and the vendor created; rejecting one with a note; and that a company-card expense posts to the card account instead. perl -c everything; run tests.

JThe clearing account: questions to the client

Data

The fork’s clearing account is chart 1099 Unklare Beträge, found by chart.symbol_link = 'abklärung', with customer.payment_clearing_accno_id and vendor.payment_clearing_accno_id as per-counterparty overrides, the chat table as the conversation, and the open cl.pl screen as the bookkeeper’s side. Ours: defaults.clearing_accno_id; clearingquestion (id, trans_id of the 1099 posting or bankline_id of an unposted line, accno, amount, transdate, question, asked_by, asked_at, status OPEN / ANSWERED / RESOLVED, answer, answer_vc, answer_vc_id, answer_trans_id the invoice the client named, answer_accno, archive_id of an uploaded receipt, answered_at, resolved_at, resolved_trans_id of the rebooking); employee.notify_frequency IMMEDIATE / DAILY / WEEKLY. The chat of design F hangs off each question. For a US preparer this is chapter 13 module 4 — the questions queue.

Screens

Cash → Clearing Account · 1099 Unklare Beträgemock-up · nothing submits
Account accno from fromdate to todate Show status
Date transdateReferenceDescriptionAmountQuestion status
24.08.2026UBS 24.08 UNKNOWN PAYERbank line 903221'000.00not asked
19.08.2026UBS 19.08 TWINT *4471bank line 90277−120.00asked 20.08
12.08.2026UBS 12.08 SUMUP *AB12bank line 90201340.00answered 25.08 “cash sales at the fair, no invoice”
Balance on 10991'220.003 lines · oldest 14 days
Question question
Send to login their notification setting: daily digest

Figure J1. The bookkeeper’s side, on the fork’s own cl.pl (accno, fromdate, todate, trans_id): every line on the clearing account, its question state, and a question composed once for many lines.

Home → Open questions · M. Kellermock-up · nothing submits

The client’s side: a to-do list, one card per question. Answer with words, an invoice, an account, or a receipt.

A. Ciccone asks24.08.2026 · +1'000.00 on 24.08 from “UNKNOWN PAYER”
Who paid this 1'000.00 on 24 August, and for which invoice?

It was answer_kind    
Customer answer_vc_id Invoice answer_trans_id
or account answer_accno plain names; the bookkeeper sees the numbers
Receipt file or reply to the e-mail with it attached
Message answer

Notify me notify_frequency

Figure J2. The client answers in their own words or by choosing; the answer lands on the question and in the chat, and the notification setting is the fork’s immediate / daily / weekly.

Cash → Clearing Account → Rebook · 24.08.2026 · 1'000.00 · answeredmock-up · nothing submits

M. Keller answered26.08.2026
Bergwerk GmbH, invoice 2026-0151.

Move from accno
To target  
Date datepaid Memo memo

Figure J3. The resolution is the fork’s cl.pl transition (transitionaccount, trans_id) or a receipt through CP::post_payment with id_1; either way the 1099 line is reversed in the same transaction and the question closes with resolved_trans_id.

Integration

Figure A3’s Ask Client creates a question on an unposted bank line (and, if Defaults say so, posts it to 1099 meanwhile); the answer flows back as the suggestion on that line. The client side lives in the portal or the dataset’s own home screen; notifications go out by employee.notify_frequency through a nightly digest job. API GET /clearing/questions?status=open, POST /clearing/questions/{id}/answer; webhooks clearing.asked, clearing.answered. Chapter 13’s console counts open questions per client from this table.

Claude Code prompt
Read CLAUDE.md, the runmyaccounts fork's bin/mozilla/cl.pl (clearing-account rebooking), SL/CP.pm and SL/Chat.pm. Build the clearing-account questions module, matching Figures J1–J3 of doc/enhancement/clearing.md (write it first).

Schema: defaults row clearing_accno_id; customer.payment_clearing_accno_id and vendor.payment_clearing_accno_id int; table clearingquestion with the columns of design J's Data paragraph; employee.notify_frequency text default 'DAILY'; upgrade script; Pg-tables.sql; bump dbversion.

Module: SL/CQ.pm with list(accno, fromdate, todate, status) joining acc_trans on the clearing account with clearingquestion; ask(trans_ids or bankline_ids, question, login, asked_by) creating rows, a chat message on each, and a notification per employee.notify_frequency (IMMEDIATE mails now through SL::Mailer; DAILY and WEEKLY are picked up by bin/notify-digest.pl); remind(id); open_for(login) for Figure J2; answer(id, fields: answer_kind, answer_vc_id, answer_trans_id, answer_accno, answer, file) storing the answer, the uploaded receipt through SL/RD.pm, a chat message, status ANSWERED, and — when the question is on a bank line — writing the answer back as that line's suggestion; rebook(id, fields of Figure J3: target, id_1, transitionaccount, datepaid, memo) reversing the 1099 line and posting either CP::post_payment against the invoice or a GL transfer to transitionaccount in one transaction, storing resolved_trans_id and status RESOLVED; a port of the fork's cl.pl transition logic for the account case.

UI: bin/mozilla/cq.pl (symlink cq.pl → dispatch.pl) with actions continue, ask_client, ask_selected, remind, rebook, ask_again, print_list, open_questions (Figure J2), answer, later, save_setting; a home-screen and dashboard count of open questions for the login; menu.ini [Cash--Clearing Account] and, for the client, [Home--Open Questions]; Figure A3's Ask Client calls CQ::ask; Defaults gets the clearing account and a "post unmatched lines to clearing" checkbox; locale entries.

Tests: t/clearing.t posting a bank line to 1099, asking a question, answering it as the client with an invoice, asserting the chat rows and the status, rebooking, and asserting the 1099 balance returns to zero and the invoice is paid with the bank line's bankline_id; and that notify_frequency DAILY produces no immediate mail but a digest row. perl -c everything; run tests.

KThe cockpit

Data

dashboardwidget (id, login, seq, kind, accno, period, compare_period, months for the trend widgets, settings jsonb) — one row per widget instance, saved per user as the fork does, addable more than once. Each kind is one report query the ledger already answers.

The widget catalogue

Everything the fork’s site and app offer, with the English name we use, and four of our own. An asterisk in the fork’s list marks a widget with a comparison period; red and green flags show the ranking against it.

OursTheirsShowsCompareSource in the ledger
Gross revenueUmsatz brutto*Sales including VAT for the period, as a figureyesAR invoices and transactions by transdate
Cash and bankFlüssige Mittel*The sum of cash and bank accounts, with the count of accountsyeschart accounts with link containing paid, category A
Top 10 customersTop 10 Kunden*Ten customers by revenue, each with its rank change against the comparison periodyesAR by customer_id
Account balanceSaldo eines Kontos*The balance of one chosen account, with its reconciled-to date for a bankyesacc_trans by chart_id; design A for the reconciled date
ExpensesAufwand*Total expenses for the periodyescategory E
IncomeErtrag*Total income for the periodyescategory I
Profit or lossErfolg (Gewinn/Verlust)*Income less expenses for the period, as a figure with the two componentsyesthe income statement of RP.pm
Profit trendErfolg*The same, month by month as a small chartyesthe same, grouped by month
Expenses vs incomeAufwand/ErtragThe two series side by side by monthcategories E and I by month
Supplier invoicesLieferantenrechnungs-SpiegelOpen, due this week, overdue, with the next payment run’s dateAP outstanding; design B
Customer invoicesKundenrechnungs-SpiegelOpen, overdue, over 30 days, with the reminder levelAR outstanding and ageing
Open customer vs supplier invoicesOffene Kunden- vs. Lieferanten-RechnungenReceivables against payables as two bars and a net figureAR and AP outstanding
Net working capitalNettoumlaufvermögenCash and bank + trade receivables − short-term liabilities (creditors and amounts owed to staff): “the money available to you in the short term”against 31.12categories by accno range from the chart, configurable
Net working capital trendEntwicklung Nettoumlaufvermögen (the widget the fork’s users voted for in 2012)Four curves over 6 to 36 months: cash and bank, receivables, short-term liabilities, and the net figurethe same, month-end balances
Open questions— oursClearing questions waiting on the client, with the oldestdesign J
Unreconciled imports— oursBank imports not yet reconciled, and open linesdesign A
Closing progress— oursSteps done of the current closing, and its statusdesign E
Open threads— oursChat threads mentioning the userdesign F

The fork’s app adds two things that are not widgets but belong on the same screen: a push notification when the bookkeeper has a question about a document, and a daily 17:00 notice of the expenses processed — designs J and I through the notification setting of Figure J2.

Screens

Home → Cockpit · M. Kellermock-up · nothing submits
Cash and bank31'702.60
3 accounts · +2'480 vs July
Gross revenue Q3212'410
vs Q3 2025 +8%
Profit or loss YTD38'920
income 611'004 · expenses 572'084
Customer invoices46'380
open 21 · overdue 6 · 9'120 > 30 d
Supplier invoices18'204
due this week 4'812 · next run 29.08
Top 10 customersMueller AG
84'200 YTD · ↑ 1 · Bergwerk GmbH ↓ 3
Net working capital59'878
vs 31.12 −4'100
Net working capital trend12 months
cash · receivables · liabilities · net — chart
Account balance 102019'302.60
reconciled to 26.08
Expenses vs incomeAug
68'112 vs 74'590 — chart
Open questions3
oldest 14 days · design J
Closing 202511 / 18
in review · design E
drag to reorder; saved per login; the same widget may be added twice with different settings

Figure K1. Twelve of the eighteen widgets on one user’s cockpit. Every figure is a link into the report that produced it.

Home → Cockpit → Add Widgetmock-up · nothing submits
Widget kind
Account accno for Account balance only
Period period compare with compare_period
Trend length months months for the two trend widgets and Expenses vs income
Show settings    

Figure K2. The fork’s side menu as a form: kind, account, period, comparison, trend length, flags. Adding the same kind twice with different accounts or periods is allowed, as theirs is.

Claude Code prompt
Read CLAUDE.md, SL/RP.pm, SL/AR.pm, SL/AP.pm and bin/mozilla/menu.pl (the home screen). Build the cockpit, matching the widget catalogue and Figures K1–K2 of doc/enhancement/cockpit.md (write it first, defining each widget kind as one SQL query with its period and comparison semantics; use the English names and the keys of Figure K2's select).

Schema: table dashboardwidget(id serial, login text, seq int, kind text, accno text, period text, compare_period text, months int, settings jsonb); defaults rows nwc_receivable_accnos, nwc_liability_accnos (account ranges for net working capital, seeded from the chart's categories); upgrade script; Pg-tables.sql; bump dbversion.

Module: SL/DB.pm (dashboard) with widgets(login) returning the rows with their computed figures; compute(kind, accno, period, compare_period, months) implementing: gross_revenue (AR amount incl. tax by transdate), cash_and_bank (sum of chart accounts with link ~ 'paid' and category A, with the count), top10_customers (revenue by customer_id with rank change vs the comparison), account_balance(accno) (plus the last RECONCILED bankimport date when the account is a bank), expenses (category E), income (category I), profit_loss and profit_trend (RP's income statement logic, the trend grouped by month over months), expenses_vs_income (both series by month), supplier_invoices and customer_invoices (open, due this week, overdue, over 30 days, next payment run date, reminder level from AR/AP outstanding), open_customer_vs_supplier, net_working_capital and nwc_trend (cash_and_bank + receivables − short-term liabilities using the defaults account ranges; the trend as month-end balances over months), open_questions (design J), unreconciled_imports (design A), closing_progress (design E), open_threads (design F); every compute returns {value, compare_value, delta, flag up/down/none, series[] for trends, drill_url}; add(login, fields of Figure K2), remove(id), reorder(login, ids).

UI: a Cockpit section on the home screen rendered by bin/mozilla/menu.pl (or a new db.pl symlink) as a responsive grid of cards each linking to its report, with Add Widget (Figure K2: kind, accno, period, compare_period, months, show_flag, show_chart, show_drill), Remove, and Arrange (drag with a small inline script that posts the new order); trend widgets and sparklines as inline SVG; menu.ini [Home--Cockpit]; locale entries for the eighteen English widget names.

Tests: t/cockpit.t adding account_balance for a bank account, posting a receipt, asserting the figure moves; customer_invoices with an overdue invoice asserting the overdue count; nwc_trend over 6 months asserting six points and the formula; and that reorder persists. perl -c everything; run tests.
Plate 15.3 · Build order and dependencies
DesignScreensDepends onDelivers toEffort
A Bank accounts and statements inA1–A6API (ch. 6 1)B, C, D’s payment, E’s bank check, I’s card matching, J’s lines, Kmedium-large
C MatchingC1AA’s suggestions, G’s vendor matchsmall
F Chat and feedF1–F2E, J, dashboard, MCPsmall
J Clearing questionsJ1–J3A, Fthe client’s to-do list; chapter 13 module 4small-medium
B Payment moduleB1–B4A, approval rules (ch. 4 3)D, H, Imedium
G Document inbox and archiveG1–G4inbox (ch. 1 4), OCR (ch. 9 2)E, H, Imedium
H StationsH1–H8approval rules, GB, Imedium
D MWST returnD1–D3line tax (15.6 item 3)Emedium
E Closing appE1–E4A, D, fixed assets (ch. 4 1)the year-end packagemedium-large
I Expense appI1–I4G, F, B, phone layoutmedium
K CockpitK1–K2A, E, F, J for four of the widgetsthe home screensmall-medium

Each prompt is written to be run on its own against this repository, in a fresh session, after the item it depends on exists; each asks for a spec file under doc/enhancement/ that reproduces its figures and their field names, a schema upgrade with the mirror in Pg-tables.sql, a module, a UI script that is a symlink to the dispatcher, locale entries, and a test under t/. Forty-one screens in all; the figures above are the acceptance test for each.

15.8What not to take

The Latin-1 encoding and the frozen 2.8 base: we are on 3.2 and UTF-8, which is the reason to migrate rather than the reverse. The portal shortcuts (encpassword in query strings, the IP whitelist that skips passwords, a committed mail API key): each is a hole. The scratch tables shared between users (filtered, debitscredits) and the LIMIT-1 rounding patches: symptoms, not features. WLprinter and the hard-coded exchange-rate cron. And the trademark: the charts and code are GPL, the name and logo are not.

In one sentence

Run my Accounts spent thirteen years making SQL-Ledger a Swiss bookkeeper’s engine and then hid the bank, the filing and the review behind a closed platform; port the open half under the GPL, build the closed half on the schema they left in the open, and write the migration that turns their dump into our dataset — and their clients and accountants have somewhere to go.

15.9Glossary

TermWhat it is
AbklärungskontoThe clearing account (1099 Unklare Beträge) where a bookkeeper parks a bank line nobody can place, and the fork’s module that turns each such line into a question the client answers. Design J.
bLinkSIX Group’s Swiss open-banking API: account access and payment submission by consent, used by accounting platforms to pull statements and push payments.
camt.053 / camt.054ISO 20022 XML bank-statement and debit/credit-notification messages; the Swiss standard for account statements since 2018.
DATEVThe German accountants’ cooperative whose data format every German tax adviser expects; an export is a list of debit–credit pairs.
DMSDocument management system; in the fork, Alfresco behind the portal.
eCH-0217The Swiss e-government XML standard for the VAT return.
ESR / VESRThe orange payment slip with a reference number and modulo-10 check digit, replaced by the QR-bill in 2022.
ESTV, MeldecenterThe Swiss Federal Tax Administration and its online filing portal for VAT returns.
GoBD, GDPdUGerman rules on the keeping of electronic books and the audit-export format the tax office can demand.
IBPThe internal name and ticket prefix of Run my Accounts’ closed .NET platform from 2024.
KMU chart, NRLRThe Swiss small-business chart of accounts, restructured for the 2014 accounting law (Neues Rechnungslegungsrecht).
MWSTMehrwertsteuer: Swiss VAT, returned quarterly to the ESTV.
pain.001The ISO 20022 credit-transfer instruction message; a bulk payment file.
QR-bill, QR-IBANThe Swiss payment part with a QR code, mandatory since September 2022; a QR-IBAN is the special IBAN that carries a structured reference.
SkontoAn early-payment discount on a vendor invoice.
Swico S1The tax-breakdown string inside a Swiss QR-bill’s billing information.
UStVAUmsatzsteuer-Voranmeldung, the German monthly or quarterly VAT return.
ZUGFeRD, Factur-XThe German–French hybrid e-invoice: a PDF/A-3 with an EN 16931 XML inside.
CockpitThe fork’s name for its dashboard of user-arranged widgets. Design K.
End-to-End IDThe ISO 20022 identifier a payer assigns to each credit transfer in pain.001; the bank echoes it in camt.053/054, which is how an outgoing payment closes its own invoice without matching. Design B.
SpesenverantwortlicherThe expense approver: the employee’s manager in the expense app, our employee.managerid. Design I.

15.10Sources

The repository itself: github.com/ledger123/runmyaccounts, cloned at its state of 20 August 2026 (2,130 commits), read commit by commit in five periods, and its present tree diffed against this repository’s schema, menu and modules. Commit hashes cited by the agents are recorded in the session transcript; the repository’s RELEASE file carries the firm’s own changelog from 2013. Licence: GPL, per LICENSE and the first commit “not including Run my Accounts trademark and logo”.