Chapter 12 Contents Last chapter
13

The Tekki fork: eighteen years of tracking upstream, and the Swiss layer on top

Rolf Stöckli has kept a public SQL-Ledger since 2008 — first as a tracking fork that imported every DWS release within weeks and layered a Swiss branch on top, then, from December 2025, as a version 4 that requires Perl 5.40 and PostgreSQL 17 and carries the only test suite SQL-Ledger has ever had. Where the Run my Accounts fork of chapter 12 hides half its platform, this one is entirely open under the GPL version 3. This chapter reads its 702 commits the same way: what was built and how, what its users expect, what to port, and — because Rolf is the person chapter 10 said to talk to first — how to build with it rather than beside it.

2026-08-26 · read from a clone of github.com/Tekki/sql-ledger at version 4.1.3 (17 August 2026); four agents covered three periods of the history and the repository’s present state
Executive summary

The Tekki fork is SQL-Ledger kept alive by discipline: every upstream release merged within weeks for fifteen years, Swiss additions isolated on their own branch and schema track, and new code put in new files. It brought UTF-8 to the whole application seven years before anyone else, a small JSON API with session tokens, native XLSX export on every report, QR-bill variables out and QR-bill decoding in, pain.001 payment files and camt.054 statement import, TOTP two-factor login, database snapshots and GPG-encrypted backups, structured addresses with a live lookup of the Swiss UID register, and a recently-used screen in place of the company logo. Version 4.0 then rewrote the runtime — Perl 5.40 signatures, YAML configuration compiled to Storable, no lynx, and 146 test files. Eleven items are worth porting outright; the 4.0 plumbing we should not follow; and the right relationship is a shared upstream, not a competitor.

13.1What the fork is

The repository was seeded from the ledger123 git import of 2.6.0 — the first fifty-eight commits are Armaghan Saqib’s upstream imports — and Rolf Stöckli begins committing in November 2010. From then the model is fixed: pristine DWS tarballs on master with a tag per version, topic branches for Switzerland, printing and security, and full as the default checkout carrying everything. A parallel schema track (defaults.version2, sql/Tekki-upgrade-NNN-MMM.sql) keeps Swiss columns from colliding with upstream’s. Point releases are numbered 3.2.12.NN up to .67; then 4.0.0 on 28 December 2025 and 4.1.3 on 17 August 2026. The licence is GPL version 3; the copyright line names DWS to 2023 and Tekki from 2007. LedgerSMB is never mentioned. Mailing lists live on SourceHut; an Ansible role installs it.

Plate 13.1 · Eighteen years in three periods
PeriodCommitsWhat was builtDirection
2008–2016214Every DWS release from 2.8.33 to 3.2.1 imported and tagged; Swiss-German, French and Italian translations in UTF-8 and a UTF-8 twin of every locale; the Swiss KMU chart in three languages with Swiss VAT; WLprinter for printing from a hosted server to a local printer; a template-editor path-traversal fix before upstream had one; batch-print date filters and a PDF combine button; the JSON “Cubulon connector” for Tekki’s own shop.A tracking fork with a Swiss branch; integration begins at the end.
2017–2023241The whole application made UTF-8 end to end; the connector renamed api.pl and given tokens and documentation; XLSX export on every report through a new SL/Spreadsheet.pm; QR-bill variables including the Swico string; camt.054 import; the recently-used module; database snapshots by CREATE DATABASE … TEMPLATE, pg_dump download, GPG backups; Docker for development; exchange rates from the Federal Customs API; keyboard shortcuts; a dark theme; every DWS release to 3.2.12.“DWS plus a Swiss layer”, new code in new files, never rewriting upstream modules.
2024–2026247Structured addresses and the UID-register lookup; pain.001 in the Swiss flavour; QR-bill decoding from a scanned PDF into a vendor invoice; TOTP for every login and the API; reference-document drag-and-drop with hash deduplication; customer and vendor XLSX round-trip; a resource-planning report; then 4.0: Perl 5.40 signatures, PostgreSQL 17, YAML config compiled to Storable, lynx and WLprinter removed, Markdown documentation, a Makefile, and 146 test files across six tiers; 4.1 adds orders on hold and an upgrade tool for every dataset.Deliberate divergence from upstream, which stopped in 2023; the fork becomes its own line.

Three consequences. There is no closed half, so nothing has to be inferred from tables. The accounting core, table layout and qq|…| screens are unchanged from 3.2.12, so features port in either direction with moderate effort. And the 4.0 runtime — signatures, compiled config, compiled locales — does not port at all; it is a fork of the plumbing, and it is the plumbing we replaced differently (a database registry for members and sessions, Bootstrap, a persistent application server).

13.2The features, and how each was built

All open. have already in 3.2.12 or this book, partial the substance exists, port worth taking, skip incompatible with our runtime or not wanted.

FeatureHow it was builtUsNote
Integration
JSON APIapi.pl is the root dispatcher with every $form->error swapped for Form::json_error; bin/mozilla/api.pl holds the actions: get_token, list_accounts, search_customer, customer_details, search_order, search_transaction, invoice_details (by id, reference, number or waybill), add_payment through CP::post_payment, add_reference, upload_file. Token = SHA-256 of the session key in an SL-Token header, valid until the next login; TOTP honoured. Always HTTP 200, errors in the body. Documented in doc/api.md with Mojo::UserAgent examples and an “HTML API” that drives the normal screens with HTML::Form.portThe first working shape of chapter 6 item 1, on this exact codebase; ours adds routes, scoped tokens on session rows, and status codes.
XLSX export everywhereSL/Spreadsheet.pm (921 lines over Excel::Writer::XLSX: title, header, data, subtotal and total rows, group-by, typed cells, freeze panes) and one companion per screen — arapss.pl, oess.pl, rpss.pl, glss.pl, cass.pl, rdss.pl, amss.pl — each with a Spreadsheet button.portBetter than the CSV of chapter 1 item 7: subtotals survive, dates are dates. Self-contained.
camt.054 import, pain.001 exportim.pl im_camt054_payment unzips and parses with Mojo::DOM, extracts date, amount, the QR or ESR reference and IBAN, rewrites itself as a CSV payment import matched on ar.dcn. SL/SPS.pm writes pain.001.001.09 Swiss Payment Standard 2.0 from the payments screen, domestic by IBAN or QR-IBAN, foreign with bank details, warning on non-working days.partialChapter 12 designs A and B; SPS.pm is the writer to port, the camt reader is the reference for our parser.
Exchange rates from the Federal Customs APIutil/fxupdate.pl: Mojolicious client, reads the members file, writes today’s rates into every dataset, rebases if the base currency is not CHF.portAgainst our registry instead of the members file; add the ECB feed for euro tenants.
Swiss
QR-bill outForm::qr_variables builds company and customer fields in three encodings (native, double-encoded for xelatex, transliterated for pdflatex) and the Swico S1 string; SL/QRCode.pm plot_latex renders the code as TikZ rectangles from Text::QRCode, plot_svg for HTML, through a template directive <%VAR qrcode=…%>. No template with a QR block is shipped; doc/templates/distribute.pl generates per-dataset templates from a private source.portWith chapter 12’s open templates this is the complete QR-bill: their variables, RMA’s layout, no image library.
QR-bill inSL::QRCode::decode_qrbill: Image::Magick rasterises the PDF page, Imager::zxing decodes, the SPC payload is parsed; im.pl import_qrbill finds the vendor by QR-IBAN and opens a prefilled AP transaction or vendor invoice with the PDF attached.portThe Swiss form of the receipt inbox’s extraction (chapter 1 item 4): a QR-bill needs no OCR.
Structured addresses and the UID registeraddress.streetname, buildingnumber, shipto likewise, defaults.companycountry; SL/ADR.pm formats addresses by country in UPU order and validates them; uid_register does a SOAP call to the federal UID service and fills name, address and CHE-… MWST number on the customer screen.portStructured addresses are the November 2026 pain.001 requirement; the register lookup is a one-day delight for Swiss users.
Swiss charts and localesSwiss-German-KMU, Swiss-French-PME, Swiss-Italian-PMI with identical numbering and VAT rows; chd_utf and chf_utf locales, maintained continuously.portAlongside RMA’s NRLR charts; theirs are smaller and consistent across the three languages.
Operations
Database snapshots, dump download, GPG backupsAM::save_snapshot is CREATE DATABASE "name_timestamp" TEMPLATE "name"; restore is a rename; pg_dump streamed to the browser; $gpg in the config encrypts the backup to a public key the user imports.partialChapter 4 item 6’s company copies, in fifty lines; needs CREATEDB for the dataset role, which our provisioning can grant.
TOTP two-factorSL/TOTP.pm, 150 lines on Digest::SHA: secret, base32, RFC 6238 check, otpauth:// URL drawn as SVG on the login screen; per-employee activation; enforced for admins by config; honoured by the API.portBacklog item 28; the secret goes on our member row.
Recently usedrecent and recentdescr tables; every save registers a two-letter object code per employee, capped at twelve; ru.pl lists them and replaces the company logo as the home frame.partialOur home screen from the usability pass lists recent work by query; theirs is cheaper and survives restarts. Merge.
Reference documents: drag-and-drop, hash deduplication, ZIP queuearchive.hash with a unique index; duplicate uploads refused; grouped in the report; queued batch output stored as a ZIP with document numbers in file names.portThe hash column is the first half of chapter 12 design G.
Customer and vendor XLSX round-tripExport from the search screens, re-import through Spreadsheet::ParseXLSX.partialChapter 4 item 7 wanted exactly this.
Test suite146 files in six tiers: unit tests per module, a compile test of every screen, config validation, and live tests that walk every menu report and post invoices, orders, payroll and backups against a running server through SL::TestClient (a Perl 5.38 class over Test::Mojo and HTML::Form, checking MIME types and file signatures of downloads).partialWe have t/ Python suites for templates and the appserver; their live-test client is the model for the posting tests backlog item 37 asks for.
Resource planning, orders on hold, keyboard shortcuts, customisable report defaults, helping notesA report of open orders, quotations and invoices per part; oe.onhold; accesskeys on every button and digit jumps to rows; _search_defaults hooks in five screens for per-dataset column overrides; helpref notes stored under notes/.portSmall, each. The notes are the “tutorial panels” we added to the storefront admin, for the ledger.
Runtime and packaging
UTF-8 throughoutCharset hard-coded; use utf8 in every module; every file handle opened with an encoding layer; datasets created UTF-8 only; locales as *_utf siblings.haveOur UTF-8 sweep of August 2026 reached the same place by a different path.
Dockerhttpd:trixie image with the Perl dependencies, compose with PostgreSQL 17, Adminer and pgAdmin, the repository bind-mounted; an AlmaLinux variant. “Test environment, not production.”portBacklog item 21; theirs is the starting point, ours adds the appserver.
4.0 runtime: Perl 5.40 signatures, %slconfig from YAML compiled to Storable, members and per-user files as .bin, locales compiled to .bin, Locale.pm split from Form.pm, lynx and DB2/Oracle removed, <button> elementsutil/update-config.pl, util/v4-upgrade.pl, SL/Locale.pm, config/sql-ledger.yml.skipWe keep members and sessions in PostgreSQL and configuration in sql-ledger.conf; compiled files are the wrong direction for a multi-tenant host. Removing lynx and DB2 we did too.
Themes and layoutHorizon, Quartz, Silver and Everforest in CSS custom properties with a prefers-color-scheme variant; a hideable menu; no Bootstrap.skipIncompatible with our Bootstrap layer; the dark variant is worth copying as a theme.
WLprinterRemoved in 4.0.skipAs chapter 12 concluded.
Finding

Twenty features: eleven to port outright, five to merge with things this book already specifies, one we have, and three to leave because they are the 4.0 runtime or its styling. Nothing is closed. The fork and this repository have diverged in the plumbing — they compiled the configuration, we moved it into a database — and converged everywhere else: both removed lynx, both went UTF-8, both wrote an API on CP::post_payment, both want a test suite. That is the shape of two forks that should share an upstream.

13.3What the fork says of itself

There is no product website; the fork speaks through its README, its Changes file and its Markdown documentation, all in the repository, and through the SourceHut lists (sql-ledger-news, active; sql-ledger-discuss, almost silent) that chapter 10 identified as the revived community list. The README describes “a web-based open source ERP and accounting platform” with the Swiss additions named — UID register, Federal Customs rates, QR-bill variables, Swiss Payment Standard import and export — and points to an Ansible role for installation. The installation guide names the branches: main for version 4, full for the extended 3.x, dws for the pristine upstream. The Changes file is the only complete changelog of SQL-Ledger from 3.0.0 to 2026 that exists anywhere, upstream included.

13.4Who comes from Tekki, and what they expect

Self-hosters, mostly. A firm that installed the fork because it was the maintained SQL-Ledger with UTF-8 and a Docker file, and now wants hosting; a Swiss SME that chose it for the charts, the QR-bill and the pain.001 file; a developer who built on api.pl. They arrive with a UTF-8 dataset at 3.2.12.NN or 4.x — the schema differs from ours by six tables and columns, all documented in one upgrade script — and with expectations that are the fork’s features by name.

Plate 13.2 · Day one for a Tekki user
  • Their dataset restores: recent, archive.hash, bank.qriban, structured addresses and oe.onhold are mapped or kept; a 4.x dataset’s version row is recognised.
  • Every report has the Spreadsheet button they are used to.
  • The API still answers: the same actions and the same token header, behind our routes.
  • Two-factor login is there, and their secret carries across.
  • QR-bills print and QR-bills scan into vendor invoices.
  • The rates update themselves overnight.
  • Their custom report defaults and helping notes still load.

13.5What to build

Tier 1Ports that pay at once: 1–4
Tier 2Swiss: 5–7
Tier 3Operations and tests: 8–10
Tier 4Upstream: 11

1XLSX export, ported

SL/Spreadsheet.pm and the seven companion screens as they are, with the Spreadsheet button rendered in our button conventions; the CSV of chapter 1 item 7 stays as the plain option. Install Excel::Writer::XLSX and Spreadsheet::ParseXLSX on the host.

Touches: seven new bin/mozilla/*ss.pl, one module, buttons on nine report screens. Effort: small.

2The API actions, behind our routes

Take the ten actions of bin/mozilla/api.pl as the first handlers of chapter 6 item 1; keep their names and parameters as action= aliases for a year so a Tekki client keeps working; map the SL-Token header onto our session rows with a scoped token; return real status codes.

Touches: SL/API.pm, the appserver. Effort: small once the API exists.

3TOTP, ported

SL/TOTP.pm unchanged; the secret on the member row; the enrolment screen in Preferences with the SVG code; a per-dataset “require for admins” default; the API and the MCP server honour it.

Touches: SL/Members.pm, bin/mozilla/login.pl, am.pl preferences, dispatch.pl. Effort: small.

4Snapshots and encrypted backups, ported

The CREATE DATABASE … TEMPLATE snapshot as the self-service company copy of chapter 4 item 6, with the panel granting CREATEDB to the dataset role; GPG encryption of downloaded backups to a key the user pastes in.

Touches: SL/AM.pm, the panel. Effort: small.

5QR-bill, both directions

Out: Form::qr_variables and SL/QRCode.pm with RMA’s open template block from chapter 12 item 2 — variables from one fork, layout from the other, TikZ rendering so no image library is needed for LaTeX, SVG for HTML. In: decode_qrbill and import_qrbill as the Swiss branch of the receipt inbox, so a QR-bill PDF becomes a vendor invoice without OCR.

Touches: SL/Form.pm, SL/IS.pm, SL/RP.pm, bin/mozilla/im.pl, inbox.pl; dependencies Text::QRCode, Imager::zxing, Image::Magick. Effort: small-medium.

6pain.001 and camt.054, merged

SL/SPS.pm is the writer for chapter 12 design B; its camt.054 reader is the reference implementation for design A’s parser, which goes further (camt.053, batch splits, the ISO 20022 codes) but should produce the same CSV-shaped result for the existing payment matcher as a fallback.

Touches: SL/BK.pm, SL/PR.pm. Effort: small on top of chapter 12.

7Structured addresses, the UID register, the charts

The streetname and buildingnumber columns on address and shipto, SL/ADR.pm’s per-country formatting, and the UID lookup button on the customer and vendor screens; the three Swiss charts and the two locales. Address structure is required by pain.001 from November 2026 and helps every country’s e-invoicing.

Touches: SL/CT.pm, SL/AA.pm, SL/OE.pm, bin/mozilla/ct.pl, io.pl, templates; one schema upgrade. Effort: medium.

8Recently used, reference-document hashes, XLSX round-trip, rates

Four small ports: the recent tables feeding our home screen; the archive.hash dedup as the first step of chapter 12 design G; customer and vendor XLSX import; fxupdate.pl against the registry with the ECB added.

Touches: scattered; each a day. Effort: small in aggregate.

9The live-test client

SL::TestClient adapted to our appserver and Bootstrap markup, and their 54 live tests as the seed of the posting, tax and rounding suite backlog item 37 asks for. The unit tiers are inventories; the live tiers are the value.

Touches: t/. Effort: medium.

10Docker, shortcuts, report defaults, notes

Their Dockerfile with our appserver as the entrypoint; accesskeys copied by hand onto our buttons; the _search_defaults hooks and the helpref notes as the per-dataset customisation layer the storefront admin already has.

Effort: small each.

11A shared upstream

The one item that is not code. DWS stopped in January 2023. Two maintained forks now exist, one Swiss and self-hosted, one hosted and Bootstrap, both GPL, with the same core and different plumbing. The offer to make: our fixes to SL/ — the security sweep, the strictness pass, the UTF-8 work, the dispatcher consolidation — as pull requests against his dws-derived branches where they apply; his Swiss modules into ours with his copyright line intact; the SourceHut list as the one list; and a written note of who maintains what. Chapter 10 put “talk to Rolf first” on the first quarter’s list for the mailing list; this is the longer version of the same conversation.

13.6Architecture: the ports, specified

The Run my Accounts chapter needed designs because its code was missing. Here the code exists, so each item below is a porting note: what to take, what to change for our runtime, the screen as it will look here, and the prompt. The prompts assume the fork is cloned beside this repository at ../tekki.

AXLSX export

Port notes

SL/Spreadsheet.pm uses Perl 5.40 signatures, which our Perl runs; keep them. It writes to a temp file and calls Form::download_tmpfile, which we lack — add it beside Form::redirect, streaming with a Content-Disposition filename per backlog item 6. The companion scripts re-run the report’s query; call our report subs instead of copying theirs.

AR → Reports → Transactions                                       12 rows, 3 customers
 Customer     Invoice     Date        Due         Amount     Paid      Open
 Mueller AG   2026-0143   12.08.2026  11.09.2026  2'150.00      0.00   2'150.00
 …
 Subtotal Mueller AG                                4'300.00  2'150.00  2'150.00
 Total                                             18'940.00  9'110.00  9'830.00
 [Continue]  [Print]  [CSV]  [Spreadsheet]   ← new: .xlsx with the subtotal rows as rows,
                                               numbers as numbers, frozen header
Claude Code prompt
Read CLAUDE.md. Port XLSX export from the Tekki fork at ../tekki.

Copy ../tekki/SL/Spreadsheet.pm into SL/ unchanged except the copyright header (keep Tekki's line, add ours). Add Form::download_tmpfile (stream a file from $userspath with Content-Type from Form::mimetype, Content-Length, a Content-Disposition filename, then unlink) and Form::mimetype for xlsx if absent. For each of arapss.pl, oess.pl, rpss.pl, glss.pl, cass.pl, rdss.pl, amss.pl in ../tekki/bin/mozilla, create the same file under bin/mozilla but make each *_spreadsheet sub call our own report data sub (AA::transactions, OE::transactions, RP::balance_sheet/income_statement/trial_balance, GL::transactions, CA::all_transactions, RD::search, the SQL monitor) rather than re-querying; keep the column order of the on-screen report. Add a submit button "Spreadsheet" (locale entries in every locale) to the report screens in aa.pl, oe.pl, rp.pl, gl.pl, ca.pl, rd.pl, sm.pl following the btn conventions in CLAUDE.md; the button posts nextsub to the ss script via the existing two-stage flow. Install Excel::Writer::XLSX if missing (note it in doc/). Add t/xlsx.t that renders an AR transactions spreadsheet for a test dataset and reads it back with Spreadsheet::ParseXLSX asserting header, one data row and the total row. perl -c every file; run the test.

BThe API actions behind our routes

Port notes

Their dispatcher-copy design is replaced by routes on the appserver; their action subs become handler bodies. Their token is a hash of a file-stored session key; ours is a row in session with a scope. Keep action= compatibility for a year.

POST /api/v1/token        {login, password, totp?}        → {token, expires}
GET  /api/v1/accounts     SL-Token: …                     → [{accno, description, …}]
GET  /api/v1/invoices?dcn=210000000003…                   → {invoice…, lines:[…], references:[…]}
POST /api/v1/invoices/4711/payments {amount, datepaid, account, source} → 201 {trans_id}
POST /api/v1/documents    multipart                       → 201 {archive_id}
POST /api.pl  action=invoice_details&dcn=…  SL-Token: …   → same body  (compatibility, one year)
Claude Code prompt
Read CLAUDE.md, the appserver code, SL/Session.pm and ../tekki/bin/mozilla/api.pl, ../tekki/SL/API.pm, ../tekki/doc/api.md. Build the first version of the JSON API on the appserver using the Tekki actions as the handler set.

Tokens: add api_token(id, login, token_hash, scope, created_at, expires_at, last_used) to sql/Pg-members.sql and SL/Members.pm (create/revoke/lookup); POST /api/v1/token exchanges login+password(+totp when the member has a secret) for a token stored hashed; every /api route requires SL-Token or Authorization: Bearer and loads %myconfig for the token's login with the roles applied.

Routes (JSON in and out, real status codes, errors as {error, message}): GET /accounts, GET /customers?search, GET /customers/:id, GET /orders?open&type&vc, GET /transactions?open&summary&outstanding, GET /invoices/:id and GET /invoices?dcn|invnumber|waybill, POST /invoices/:id/payments, POST /invoices/:id/references, POST /documents. Each calls the same SL::* methods the Tekki subs call (CT::search, CT::create_links, OE::transactions, AA::transactions, IS::retrieve_invoice, CP::post_payment, the reference insert, RD storage). Add POST /api.pl with the ten Tekki action names as aliases returning their exact response shapes, marked deprecated in the docs.

Docs: doc/api.md adapted from Tekki's with our URLs and status codes. Tests: t/api.t (Test::Mojo against the appserver) covering token issue, an unauthorised call, invoice lookup by dcn, and a payment post that changes ar.paid. perl -c all files; run tests.

CTOTP

Preferences → Two-factor login
 Status: not enabled
 [Enable]  → shows an SVG QR code and the secret in base32; "enter the six-digit code to confirm" [______] [Confirm]
 Login screen after enabling:  Login [____]  Password [____]  Code [______]  [Login]
 Admin: System → Defaults → "Require two-factor for administrators" ☑
Claude Code prompt
Read CLAUDE.md, dispatch.pl, SL/Members.pm and ../tekki/SL/TOTP.pm, ../tekki/bin/mozilla/login.pl (totp_screen, totp_login). Port TOTP two-factor login.

Copy SL/TOTP.pm as-is (keep attribution). Add member.totp_secret text and member.totp_activated bool to sql/Pg-members.sql and SL/Members.pm. Preferences (am.pl config): an Enable/Disable section that generates a secret, renders the otpauth URL as an SVG QR (Text::QRCode → SVG, no external image library), and confirms with one valid code before activating. Login: after password success in dispatch.pl's check_password/User::login path, if totp_activated, show a code screen and verify with TOTP::check_code allowing one step of drift; on success create the session as today. Defaults: admin_totp_required per dataset enforced for logins with admin rights. The API token endpoint accepts totp when the member has it enabled. Locale entries for the new labels. Tests: t/totp.t generating a secret, computing a code with the module and asserting check_code accepts it and rejects a stale one; and a login flow test through the appserver requiring the code. perl -c; run tests.

DSnapshots and encrypted backups

System → Backup → Snapshots                                  dataset: mueller  (412 MB)
 Name                      Created            Size    
 mueller_20260826T0600     26.08.2026 06:00   412 MB   [Restore]  [Delete]
 mueller_20260801T0600     01.08.2026 06:00   398 MB   [Restore]  [Delete]
 [Take snapshot now]   [Download dump]  Encrypt to key [ABCD1234 ▾] [Import public key]
Claude Code prompt
Read CLAUDE.md, SL/AM.pm (backup) and ../tekki/SL/AM.pm (snapshots, save_snapshot, restore_snapshot, delete_snapshots, encrypt_file, import_publickey, delete_publickey) and ../tekki/bin/mozilla/am.pl (list_snapshots, pg_dump). Port snapshots, dump download and GPG-encrypted backups.

Snapshots: implement with CREATE DATABASE "_" TEMPLATE "" (requires no active connections: terminate other backends of the dataset first with pg_terminate_backend for our own role's sessions, refuse if others remain), restore by renaming the live database aside and the snapshot into its place inside one admin transaction, delete with DROP DATABASE; list by prefix from pg_database with sizes. Document that the dataset role needs CREATEDB and add the GRANT to the panel's provisioning in /var/www/ledger123web/website.pl. Dump: stream pg_dump -Fc through Form::download_tmpfile. GPG: $gpg in sql-ledger.conf; import a pasted public key into a per-dataset keyring under $userspath/gnupg/; encrypt the dump to the chosen key id before download. Menu entries under System → Backup; locale entries. Tests: t/snapshot.t taking, listing and deleting a snapshot of a test dataset (skip unless the role has CREATEDB). perl -c; run tests.

EQR-bill in and out

Import → QR Vendor Invoice        [Choose PDF or image]  [Continue]
 Decoded:  Creditor  Swisscom (Schweiz) AG, Alte Tiefenaustrasse 6, 3050 Bern
           QR-IBAN   CH44 3199 9123 0008 8901 2      Reference 21 00000 00003 13947 14300 09017
           Amount    389.40 CHF                        Vendor match: Swisscom AG (QR-IBAN) ✓
 → opens AP → Vendor Invoice prefilled, PDF attached as reference document   [Post]
Claude Code prompt
Read CLAUDE.md, ../tekki/SL/QRCode.pm, ../tekki/SL/Form.pm (qr_variables), ../tekki/bin/mozilla/im.pl (import_qrbill, im_qrbill, process_qrbill), ../tekki/SL/IM.pm (qrbill_links), and the RMA QR template block described in doc/enhancement/qrbill.md (write it from chapter 12 of the enhancement book if absent). Port the Swiss QR-bill in both directions.

Out: copy SL/QRCode.pm (plot_latex, plot_svg) and Form::qr_variables; add bank.qriban, bank.strdbkginf if absent; expose the qr_* variables in IS::invoice_details and RP::reminder through one shared sub; implement the <%VAR qrcode=…%> template directive in Form::parse_template calling plot_latex for .tex and plot_svg for .html; add a QR payment-part block to doc/templates/Default and German invoice, statement and reminder templates gated on currency CHF and a QR-IBAN, laid out per the Swiss Payment Standard (payment part 148×105 mm, receipt 62 mm, Swiss cross, sections in the specified order).

In: copy decode_qrbill (Image::Magick + Imager::zxing) and the import_qrbill flow into bin/mozilla/im.pl and SL/IM.pm; match the vendor by QR-IBAN then IBAN; open a prefilled vendor invoice or AP transaction with the source PDF attached as a reference document; add menu.ini entries under Import; hook the same decoder into the receipt inbox so a mailed QR-bill PDF is decoded before OCR is attempted.

Dependencies: Text::QRCode, Imager::zxing, Image::Magick — note installation in doc/. Tests: t/qrbill.t rendering a CHF invoice to LaTeX and asserting the SPC payload lines (SPC, 0200, 1, QR-IBAN, creditor, amount, currency, reference type QRR) appear in order; and decoding t/fixtures/qrbill-sample.png into the expected fields. perl -c; run tests.

FStructured addresses and the UID register

Customers → Add Customer
 Name        [Mueller AG            ]   UID [CHE-123.456.789] [Look up in UID register]
 Street      [Bahnhofstrasse        ]   No. [12  ]     ← filled from the register
 ZIP / City  [6300 ] [Zug           ]   Country [CH ▾]
 VAT number  CHE-123.456.789 MWST  ✓ registered
Claude Code prompt
Read CLAUDE.md, SL/CT.pm, bin/mozilla/ct.pl, bin/mozilla/io.pl and ../tekki/SL/ADR.pm, ../tekki/bin/mozilla/ct.pl (uid_register), ../tekki/sql/Pg-upgrade-3.2.4-4.0.0.sql. Port structured addresses and the Swiss UID register lookup.

Schema upgrade: address.streetname, address.buildingnumber, shipto.shiptostreetname, shipto.shiptobuildingnumber, defaults companycountry and checkaddress; a one-time split of existing address1 values of the form "Street 12" into the two columns where unambiguous (log the rest). Copy SL/ADR.pm (country table, local_address formatting in UPU order, check_country). Customer, vendor, employee and ship-to screens get street and number fields; templates expose <%streetname%>, <%buildingnumber%> and <%localaddress%>; existing <%address1%> stays composed for compatibility. UID lookup: a button on the customer and vendor screens that POSTs the SOAP GetByUID request to https://www.uid-wse.admin.ch/V5.0/PublicServices.svc via Mojo::UserAgent (timeout 10 s, errors shown with Form::error), filling name, street, number, zip, city and taxnumber "CHE-xxx.xxx.xxx MWST" when VAT-registered. Locale entries. Tests: t/address.t asserting the split of "Bahnhofstrasse 12" and formatting for CH, DE, US, GB; the UID call mocked with a saved response in t/fixtures/uid-response.xml. perl -c; run tests.

GThe live-test client

Claude Code prompt
Read CLAUDE.md, t/ (the existing Python suites), the appserver code, and ../tekki/SL/TestClient.pm with ../tekki/t/05-live-safe and ../tekki/t/06-live-unsafe. Build a live-test suite for this repository.

Port SL::TestClient as t/lib/SL/TestClient.pm: keep its Test::Mojo + HTML::Form design and download checks (MIME and file(1) signature), but log in through our login flow (registry members, session cookie from SL/Session.pm), resolve buttons by their value text inside Bootstrap markup, and read t/testdata/testconfig.yml for the base URL, login and dataset. Port the safe tier first: one .t per menu module that opens every report in menu.ini for the module and asserts 200 and a table; then the unsafe tier: create a customer, a part, post a sales invoice, receive a payment, print the invoice to PDF and check the signature, post a vendor invoice and a GL entry, run a recurring transaction, take a backup, and delete what was created, asserting acc_trans balances after each posting. Gate the unsafe tier on SL_LIVETEST=1 and a dataset name containing "test". Add make targets or a t/README explaining how to run against a local appserver. perl -c and run the safe tier.
Plate 13.3 · Build order and dependencies
PortDepends onDelivers toEffort
A XLSX exportevery report; chapter 2’s report librarysmall
C TOTPB, the MCP server, the accountant seatsmall
D Snapshotspanel grantchapter 4 item 6small
B API actionsappserver routes, Cchapter 6 item 1 and everything on itsmall-medium
F Addresses, UIDE, pain.001 (chapter 12 B)medium
E QR-billF, chapter 12 templatesinvoices, reminders, the receipt inboxsmall-medium
G Live testsappserverevery later chaptermedium

13.7What not to take

The compiled configuration and members files: a multi-tenant host wants rows, not .bin files per login. The compiled locales: our translation path through locales.pl and the %{$self{subs}} map is what makes button labels safe. The themes and the hideable menu: Bootstrap owns that layer here. Perl 5.40 signatures in shared modules: fine in new files, but rewriting SL/AA.pm to signatures would make every future exchange of patches with either fork harder. And the <button> conversion, which they themselves flagged as breaking for custom scripts.

In one sentence

Tekki kept SQL-Ledger current for eighteen years and then took it somewhere we would not go; port the nine things that are simply good — XLSX, the API actions, TOTP, snapshots, QR-bill in and out, pain.001, structured addresses, the rates, the live tests — leave the 4.0 plumbing, and make the maintainer an upstream partner rather than a competitor.

13.8Glossary

TermWhat it is
CubulonTekki’s own web application, for which the first JSON connector (2015) was written; renamed api.pl in 2017.
Federal Customs ratesThe daily exchange rates published by the Swiss Federal Office for Customs and Border Security (formerly EZV), accepted for VAT purposes; fxupdate.pl reads them.
StorablePerl’s binary serialisation; version 4 compiles YAML configuration and locales into .bin files with it.
SPSSwiss Payment Standard, SIX’s implementation guide for ISO 20022 in Switzerland; SL/SPS.pm writes pain.001 to it.
Swico S1The tax-breakdown string in a QR-bill’s billing information, as defined by the Swico association.
TOTPTime-based one-time password, RFC 6238; the six-digit code from an authenticator app.
UID registerThe Swiss federal register of enterprise identification numbers (CHE-…), with a public web service.
UPU orderThe Universal Postal Union’s conventions for the order of address lines by country.
version2The fork’s parallel schema version in defaults, used by its own upgrade scripts in the 3.x line.

13.9Sources

The repository: github.com/Tekki/sql-ledger, cloned at version 4.1.3 (17 August 2026), 702 commits; its README.md, Changes, doc/*.md, doc/api.md and LICENSE; the SourceHut lists at lists.sr.ht/~tekki. Read in three periods and as a whole-tree diff against this repository’s schema, menu and modules. Chapter 10 of this book for the earlier assessment of the fork as community infrastructure.