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.
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.
| Period | Commits | What was built | Direction |
|---|---|---|---|
| 2008–2016 | 214 | Every 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–2023 | 241 | The 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–2026 | 247 | Structured 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.
| Feature | How it was built | Us | Note |
|---|---|---|---|
| Integration | |||
| JSON API | api.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. | port | The 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 everywhere | SL/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. | port | Better than the CSV of chapter 1 item 7: subtotals survive, dates are dates. Self-contained. |
| camt.054 import, pain.001 export | im.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. | partial | Chapter 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 API | util/fxupdate.pl: Mojolicious client, reads the members file, writes today’s rates into every dataset, rebases if the base currency is not CHF. | port | Against our registry instead of the members file; add the ECB feed for euro tenants. |
| Swiss | |||
| QR-bill out | Form::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. | port | With chapter 12’s open templates this is the complete QR-bill: their variables, RMA’s layout, no image library. |
| QR-bill in | SL::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. | port | The Swiss form of the receipt inbox’s extraction (chapter 1 item 4): a QR-bill needs no OCR. |
| Structured addresses and the UID register | address.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. | port | Structured addresses are the November 2026 pain.001 requirement; the register lookup is a one-day delight for Swiss users. |
| Swiss charts and locales | Swiss-German-KMU, Swiss-French-PME, Swiss-Italian-PMI with identical numbering and VAT rows; chd_utf and chf_utf locales, maintained continuously. | port | Alongside RMA’s NRLR charts; theirs are smaller and consistent across the three languages. |
| Operations | |||
| Database snapshots, dump download, GPG backups | AM::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. | partial | Chapter 4 item 6’s company copies, in fifty lines; needs CREATEDB for the dataset role, which our provisioning can grant. |
| TOTP two-factor | SL/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. | port | Backlog item 28; the secret goes on our member row. |
| Recently used | recent 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. | partial | Our 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 queue | archive.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. | port | The hash column is the first half of chapter 12 design G. |
| Customer and vendor XLSX round-trip | Export from the search screens, re-import through Spreadsheet::ParseXLSX. | partial | Chapter 4 item 7 wanted exactly this. |
| Test suite | 146 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). | partial | We 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 notes | A 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/. | port | Small, each. The notes are the “tutorial panels” we added to the storefront admin, for the ledger. |
| Runtime and packaging | |||
| UTF-8 throughout | Charset hard-coded; use utf8 in every module; every file handle opened with an encoding layer; datasets created UTF-8 only; locales as *_utf siblings. | have | Our UTF-8 sweep of August 2026 reached the same place by a different path. |
| Docker | httpd:trixie image with the Perl dependencies, compose with PostgreSQL 17, Adminer and pgAdmin, the repository bind-mounted; an AlmaLinux variant. “Test environment, not production.” | port | Backlog 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> elements | util/update-config.pl, util/v4-upgrade.pl, SL/Locale.pm, config/sql-ledger.yml. | skip | We 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 layout | Horizon, Quartz, Silver and Everforest in CSS custom properties with a prefers-color-scheme variant; a hideable menu; no Bootstrap. | skip | Incompatible with our Bootstrap layer; the dark variant is worth copying as a theme. |
| WLprinter | Removed in 4.0. | skip | As chapter 12 concluded. |
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.
- Their dataset restores:
recent,archive.hash,bank.qriban, structured addresses andoe.onholdare mapped or kept; a 4.x dataset’sversionrow 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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 ✓ registeredClaude 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 promptRead 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.
| Port | Depends on | Delivers to | Effort |
|---|---|---|---|
| A XLSX export | — | every report; chapter 2’s report library | small |
| C TOTP | — | B, the MCP server, the accountant seat | small |
| D Snapshots | panel grant | chapter 4 item 6 | small |
| B API actions | appserver routes, C | chapter 6 item 1 and everything on it | small-medium |
| F Addresses, UID | — | E, pain.001 (chapter 12 B) | medium |
| E QR-bill | F, chapter 12 templates | invoices, reminders, the receipt inbox | small-medium |
| G Live tests | appserver | every later chapter | medium |
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.
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
| Term | What it is |
|---|---|
| Cubulon | Tekki’s own web application, for which the first JSON connector (2015) was written; renamed api.pl in 2017. |
| Federal Customs rates | The daily exchange rates published by the Swiss Federal Office for Customs and Border Security (formerly EZV), accepted for VAT purposes; fxupdate.pl reads them. |
| Storable | Perl’s binary serialisation; version 4 compiles YAML configuration and locales into .bin files with it. |
| SPS | Swiss Payment Standard, SIX’s implementation guide for ISO 20022 in Switzerland; SL/SPS.pm writes pain.001 to it. |
| Swico S1 | The tax-breakdown string in a QR-bill’s billing information, as defined by the Swico association. |
| TOTP | Time-based one-time password, RFC 6238; the six-digit code from an authenticator app. |
| UID register | The Swiss federal register of enterprise identification numbers (CHE-…), with a public web service. |
| UPU order | The Universal Postal Union’s conventions for the order of address lines by country. |
| version2 | The 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.