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.
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.
| Period | Commits | What was built | Direction |
|---|---|---|---|
| 2013–2015 | 399 | Per-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–2017 | 438 | The 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–2020 | 430 | Clearing-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–2023 | 389 | QR-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–2026 | 474 | A 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.
| Feature | How it was built | Us | Note |
|---|---|---|---|
| Swiss and German compliance | |||
| QR-bill on invoices, statements and reminders | bank.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. | port | Chapter 1 item 2a asked for exactly this; the open code is the Swiss block. |
| ESR payment slip | ar/ap.dcn, bank.dcn/rvc/membernumber, Form::format_dcn modulo-10 check digit, orange-slip template with a PostFinance branch. | have | Our bank table carries the columns; ESR is retired in Switzerland since 2022. |
| Line-item VAT and the All Taxes report | acc_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. | partial | Our 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 VAT | tax.reversecharge_id; posting a GL line with such a tax writes two offsetting tax lines; the report shows both sides. | port | Needed by any EU or Swiss firm buying services abroad. |
| Swiss and German charts, Swiss-German locale | RMACH_{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. | port | Our 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 Germany | DATEV: 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. | port | German-market items; the Mojolicious code is the most portable in the tree. |
| ZUGFeRD and Factur-X | 2016: 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. | partial | Backlog 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. | partial | This is the manual half of chapter 1 item 1’s matching grid; its posting logic is the part to keep. |
| Extended transaction log | On 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. | port | The immutable history chapter 1 item 7 wants; row snapshots are simpler than our audittrail rows. |
| Ledger Doctor | custom_am.pl: unbalanced journals, paid-versus-booked mismatches, orphans, blank rows, missing dates, bad line-tax rows, each with a fix. | port | The anomaly review of chapter 9 item 4, already written for this schema. |
| Per-user closing date, GL on hold, departmental year-end | employee.closedto with Form::closedto_user; gl.onhold flag and filter; year-end per department. | port | Small, useful to a practice with juniors. |
| Financial years | financial_year rows OPEN, CURRENT, CLOSED, maintained by AM::post_yearend. | port | The table the closed platform reads; harmless and clarifying for us. |
| Last-used report columns, searchable dropdowns | lastused table via Form::get_lastused; select2 on every select with a numeric-prefix matcher for account numbers. | port | Cheap usability; our usability pass stopped short of both. |
| Chart hierarchy, customer default accounts, Skonto, reminder stop | chart.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. | port | Master-data fields an automated poster needs; chapter 9’s categoriser wants the first two. |
| Dunning | |||
| The reminder workflow | Level 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. | partial | Our 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 importer | IM::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. | partial | Our importer handles fixed layouts; the mapping step is the addition. |
| Dispatch methods | dispatch table on customer and vendor; the batch-print queue filters by it, so post, e-mail and portal documents separate. | port | An afternoon. |
| Modular LaTeX templates | Seventy RMA-*.tex parts: packages, settings, header, address, articles, sums, terms, payment table, page breaks, country lookup, QR part, ZUGFeRD part; three languages. | partial | Our templates were redesigned in HTML; the fork’s LaTeX set is the reference for the QR and e-invoice parts. |
| Backups | pg_dump | gzip mailed or downloaded; templates tarred separately; Mojolicious restore. | have | Ours does the same; chapter 4 item 6 goes further. |
| Warehouse transfers with FIFO reposting | trf.pl, SL/Trf.pm, fifo and build tables, a COGS repost tool. | have | 3.2.12 has warehouse transfers natively. |
| Integration hooks | |||
api.pl | Mojolicious::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. | partial | The first two endpoints of chapter 6 item 1, and proof the approach works on this codebase. |
| Portal hooks | encpassword bearer login, menuids deep links, $ip_whitelist password bypass, FRONTEND_COOKIE CSRF, postMessage events for upload-and-sign and URL-to-PDF, Sendinblue mailer. | skip | Each is a shortcut for one deployment; the API and roles of chapters 6 and 4 replace them properly. |
| WLprinter | A Java Web Start bridge that spools print jobs for a client on the user’s PC to pull and print locally. | skip | Java Web Start is dead; browsers print. |
| SQLite members store | users/members.db replacing the INI and per-user .conf files, with migration scripts. | have | We 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.
| Module | Evidence in the schema | What the site says it does | How 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 bLink | bank_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 lines | search_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 ESTV | vat_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 checklist | xcontrolling_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 feed | chat (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 stations | None 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 archive | postMessage 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. |
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 as | On the site | In the code | Us |
|---|---|---|---|
| Kunden-Modul — the customer module | |||
| Invoices, credit notes, quotations, orders, delivery notes, customer and item master | Listed verbatim | Stock SQL-Ledger | have |
| Recurring invoices “for subscription management (weekly, monthly, yearly)”; bulk dispatch | Listed | Stock recurring transactions; batch queue and e-mail | have |
| Payment slips and QR invoices | Listed; QR “from mid-2020” (iso-20022) | Open: the QR block in IS.pm/RP.pm | build |
| “Live e-mail notifications on incoming payments” and “for overdue invoices” | Listed | Closed: fired by the platform on bank import | build |
| Three-level dunning; customer statements | Listed | Open: the reminder workflow | partial |
| Lieferanten-Modul — the supplier module | |||
| “Automated document recognition using machine-learning algorithms”, “validation by specialists”, automated reading of payment slips, QR invoices, IBANs | Listed | Closed: 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.3 | build |
| “All your documents are digitally signed with a GeBüV signature”; full-text search | Listed; “meets the Federal Tax Administration’s requirements for an electronic archive” | Closed: the Alfresco DMS | build |
| Workflow-Modul | |||
| 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 logged | Listed; “works with all document classes”; a counter of documents per station | Closed entirely | build |
| 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 new | zahlungsworkflow-modul | Closed: exported_payment, banking_export_event | build |
| Finanzbuchhaltungs-Modul — the ledger | |||
| Dashboard “with daily current figures”; charts of liquidity, receivables and payables; top customers; period comparisons | Listed | Closed: the portal (graphs.pl is a one-chart stub) | partial |
| Profit centres and cost centres; project accounting; journal, trial balance, account sheets; any foreign currency | Listed | Stock SQL-Ledger plus reports.pl | have |
| “Chat on bookings with your online accountant” | Listed | Closed: the chat table | build |
| “Interactive closing app: to the annual close in 18 steps” | Listed | Closed: xabschluss, xcontrolling_log | build |
| 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 client | Listed | Closed | build |
| Schnittstellen — integrations | |||
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 articles | Closed — served by the platform, not by api.pl | partial |
| 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 articles | Closed, over the API | partial |
| Sixteen named Swiss banks from UBS and PostFinance to the cantonal and Raiffeisen banks; “we never access your e-banking directly” | Listed with logos | Closed: camt in, pain out, bLink | build |
| MWST | |||
| 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 approval | Step by step | Closed: vat_settlement; open: the line-tax report beneath it | partial |
| 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 9001 | Listed | Roles and export open; 2FA and the login log closed | partial |
| Prices | |||
| 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 customers | Listed, excluding VAT, packages changeable monthly | — | The Practice plan of chapter 10 sits between M and L |
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.
- Their dump restores —
fxamount,dcn, the QR bank columns,invoicetax, the log tables,financial_year,symbol_linkall 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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
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 promptRead 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
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.
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.
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.
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
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.
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.
Figure E3. A practice edits the template once; every client’s closing run is copied from it.
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.
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
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”.
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.
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
Figure G1. rd.pl’s Add Document with a class and a folder; the same form serves upload, the mail address and the API.
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.
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.
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.
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
Figure H1. Stations, members, substitutes and the moves each station may make — the arrows of the fork’s graph, as columns.
Figure H2. The queue with the fork’s number flags. After substitute_after_days the substitute sees the same list.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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 promptRead 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
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.
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.
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.
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.
| Ours | Theirs | Shows | Compare | Source in the ledger |
|---|---|---|---|---|
| Gross revenue | Umsatz brutto* | Sales including VAT for the period, as a figure | yes | AR invoices and transactions by transdate |
| Cash and bank | Flüssige Mittel* | The sum of cash and bank accounts, with the count of accounts | yes | chart accounts with link containing paid, category A |
| Top 10 customers | Top 10 Kunden* | Ten customers by revenue, each with its rank change against the comparison period | yes | AR by customer_id |
| Account balance | Saldo eines Kontos* | The balance of one chosen account, with its reconciled-to date for a bank | yes | acc_trans by chart_id; design A for the reconciled date |
| Expenses | Aufwand* | Total expenses for the period | yes | category E |
| Income | Ertrag* | Total income for the period | yes | category I |
| Profit or loss | Erfolg (Gewinn/Verlust)* | Income less expenses for the period, as a figure with the two components | yes | the income statement of RP.pm |
| Profit trend | Erfolg* | The same, month by month as a small chart | yes | the same, grouped by month |
| Expenses vs income | Aufwand/Ertrag | The two series side by side by month | — | categories E and I by month |
| Supplier invoices | Lieferantenrechnungs-Spiegel | Open, due this week, overdue, with the next payment run’s date | — | AP outstanding; design B |
| Customer invoices | Kundenrechnungs-Spiegel | Open, overdue, over 30 days, with the reminder level | — | AR outstanding and ageing |
| Open customer vs supplier invoices | Offene Kunden- vs. Lieferanten-Rechnungen | Receivables against payables as two bars and a net figure | — | AR and AP outstanding |
| Net working capital | Nettoumlaufvermögen | Cash and bank + trade receivables − short-term liabilities (creditors and amounts owed to staff): “the money available to you in the short term” | against 31.12 | categories by accno range from the chart, configurable |
| Net working capital trend | Entwicklung 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 figure | — | the same, month-end balances |
| Open questions | — ours | Clearing questions waiting on the client, with the oldest | — | design J |
| Unreconciled imports | — ours | Bank imports not yet reconciled, and open lines | — | design A |
| Closing progress | — ours | Steps done of the current closing, and its status | — | design E |
| Open threads | — ours | Chat threads mentioning the user | — | design F |
Screens
Figure K1. Twelve of the eighteen widgets on one user’s cockpit. Every figure is a link into the report that produced it.
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 promptRead 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.
| Design | Screens | Depends on | Delivers to | Effort |
|---|---|---|---|---|
| A Bank accounts and statements in | A1–A6 | API (ch. 6 1) | B, C, D’s payment, E’s bank check, I’s card matching, J’s lines, K | medium-large |
| C Matching | C1 | A | A’s suggestions, G’s vendor match | small |
| F Chat and feed | F1–F2 | — | E, J, dashboard, MCP | small |
| J Clearing questions | J1–J3 | A, F | the client’s to-do list; chapter 13 module 4 | small-medium |
| B Payment module | B1–B4 | A, approval rules (ch. 4 3) | D, H, I | medium |
| G Document inbox and archive | G1–G4 | inbox (ch. 1 4), OCR (ch. 9 2) | E, H, I | medium |
| H Stations | H1–H8 | approval rules, G | B, I | medium |
| D MWST return | D1–D3 | line tax (15.6 item 3) | E | medium |
| E Closing app | E1–E4 | A, D, fixed assets (ch. 4 1) | the year-end package | medium-large |
| I Expense app | I1–I4 | G, F, B, phone layout | — | medium |
| K Cockpit | K1–K2 | A, E, F, J for four of the widgets | the home screen | small-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.
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
| Term | What it is |
|---|---|
| Abklärungskonto | The 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. |
| bLink | SIX 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.054 | ISO 20022 XML bank-statement and debit/credit-notification messages; the Swiss standard for account statements since 2018. |
| DATEV | The German accountants’ cooperative whose data format every German tax adviser expects; an export is a list of debit–credit pairs. |
| DMS | Document management system; in the fork, Alfresco behind the portal. |
| eCH-0217 | The Swiss e-government XML standard for the VAT return. |
| ESR / VESR | The orange payment slip with a reference number and modulo-10 check digit, replaced by the QR-bill in 2022. |
| ESTV, Meldecenter | The Swiss Federal Tax Administration and its online filing portal for VAT returns. |
| GoBD, GDPdU | German rules on the keeping of electronic books and the audit-export format the tax office can demand. |
| IBP | The internal name and ticket prefix of Run my Accounts’ closed .NET platform from 2024. |
| KMU chart, NRLR | The Swiss small-business chart of accounts, restructured for the 2014 accounting law (Neues Rechnungslegungsrecht). |
| MWST | Mehrwertsteuer: Swiss VAT, returned quarterly to the ESTV. |
| pain.001 | The ISO 20022 credit-transfer instruction message; a bulk payment file. |
| QR-bill, QR-IBAN | The Swiss payment part with a QR code, mandatory since September 2022; a QR-IBAN is the special IBAN that carries a structured reference. |
| Skonto | An early-payment discount on a vendor invoice. |
| Swico S1 | The tax-breakdown string inside a Swiss QR-bill’s billing information. |
| UStVA | Umsatzsteuer-Voranmeldung, the German monthly or quarterly VAT return. |
| ZUGFeRD, Factur-X | The German–French hybrid e-invoice: a PDF/A-3 with an EN 16931 XML inside. |
| Cockpit | The fork’s name for its dashboard of user-arranged widgets. Design K. |
| End-to-End ID | The 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. |
| Spesenverantwortlicher | The 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”.