GUTEX Schweiz · Invoice processing and ERP
Rebuilt invoice processing for Switzerland, brought it to parity and went live without downtime.
GUTEX turns German and French invoices into Swiss documents and ERP postings. I've worked on these systems since 2025, checked every difference against the legacy system and went live in August. Since the fall, France runs through it too.
- Client
- GUTEX Schweiz
- Period
- since 2025
- Stack
- Python, FastAPI, HTMX, MySQL, PostgreSQL, AWS, LlamaParse, abas, Abacus
Starting point
GUTEX makes wood fiber insulation in the Black Forest and sells mainly in Germany, Switzerland and France. Every invoice from Germany has to be reprocessed for Switzerland: separate PDFs for customers and intercompany, separate number ranges, postings in two ERP systems, emails to the right mailboxes.
I’ve been working on the Swiss solution since 2025, together with AM Software. It’s not one app but several systems that have to work together.
What I build here
Inventory before the first commit
On June 18 I had an agent go through all repos and the cloud environment read-only. The result was a map of every system: a document management system with OCR for accounting, the Swiss interim solution, its successor and a system for advice-note invoices. One paragraph per application: what it does, where it runs, which database and which external services it depends on. The most important finding came from the database: 64 new documents had been created in the interim solution that same day. This wasn’t legacy cleanup. It was daily business.
Everything in one place
One part had no maintained repository. I backed up the running state, cleaned it up and moved it into its own repo. Everything operational now lives in a central ops repo with docs, a system map and recovery data.
Understand first, then fix
The first report was about stuck uploads. Instead of guessing, I first expanded logging so every step was traceable, then wrote tests that reproduced the behavior, and only then fixed the bug. I also added idempotency to sending, so nothing reaches the ERP twice.
Parity with the legacy system
The ERP has only a production system. A bug lands directly in accounting. So the new system had to produce exactly the same documents as the old one, “not just roughly”. I reproduced old exports from the legacy system’s database and compared them against a review package of 192 documents. Feedback from accounting, for example on the text on pickup documents, went in as code changes, and I regenerated the export afterwards so the comparison stayed unambiguous. Later came details like signs on credit notes in Abacus, each checked against what the legacy system had done.
A new UI and a queue that holds
I’d have liked to rebuild everything in Rails. I kept the existing Python code anyway, because parity mattered more than my favorite stack. New are a lean UI with FastAPI and HTMX, a queue with retries and an audit trail for every send to email, Abacus and abas. A test mode simulates all sends and says so in German. Multiple PDFs can be uploaded in one go, and pro formas go out automatically if wanted. Interrupted sends aren’t blindly retried. They go to a human for review.
Go-live and the slow afternoon
On August 18 GUTEX started testing. An agent checked logs and the database every 30 minutes, not just the health check. At noon I cleared the system and switched it live. The first real invoice went out at 1:40 pm: two emails, two postings in Abacus, one in abas, all in eleven seconds.
In the afternoon word came that conversion had been fast in the morning and was now slow. The trail led to the PDF parser: default settings, no pinned version, 44 seconds per document on average, almost four minutes in the worst case. I didn’t switch on a hunch. I parsed 24 documents from all three document types with both modes and compared the extracted fields. 24 out of 24 identical, the new mode about four times faster. Pull request, tests, deployment, all the same day. After that, parallelism became configurable, and the benchmark script lives in the repo so anyone can rerun the numbers.
France
In September the French company came on board: different accounts, its own number range, credit notes and zero-value invoices. I moved country behavior behind profiles and feature flags and set up a separate test environment for France. At every step, the French sample documents and current Swiss documents ran through, so Switzerland wouldn’t notice a thing. I checked questions for GUTEX against the spec first, so we didn’t ask anything that had long been settled. When the interface docs for the French abas arrived, I compared them field by field with the Swiss adapter instead of just believing “it’s identical”. Result: same base, but some fields and data types differ.
The other systems
The system for advice-note invoices is part of it too. There I moved the database to a current PostgreSQL version, tested locally with a copy first, then switched over with verified encryption. I backed up and paused an unused staging environment. Since then, a daily check verifies that the application is healthy and jobs run through.
How I work
At GUTEX, agents are read-only by default. Write steps in production are named and approved individually. Deployments go through staging: the same image that was tested there is promoted to production, nothing else. For new sample documents there’s a verification skill that checks the conversion and the finished PDF against expectations. When GUTEX reports a case, such as a pickup invoice uploaded twice, first comes a test that reproduces it, then the fix, then a short reply in German.
Results
The invoice system has been in production since August 18. On the first full day, 44 documents went through, without errors and without gaps in the number ranges. Since the end of July I’ve merged more than 60 pull requests into this repo, from the UI to the French account mapping. France runs in the same system, with accounts that accounting maintains itself. What used to be scattered and undocumented is now in one place, with tests, runbooks and monitoring.
With systems that grew over time, the first job isn’t code. It’s inventory. And on go-live day, the only thing that counts is what the database says.
Got a similar problem? Get in touch.