Case study
BTP AI: supplier invoices from a Telegram photo to a checked draft in Odoo
Odoo 19 Community, n8n and self-hosted OCR for Moroccan construction companies: progressive billing, the 7% public-contract retention cap, a margin per site, and a posting guard so nothing is posted unchecked.
- Client
- Built for Moroccan BTP companies; no client deployment yet
- Stack
- Odoo 19 Community, PostgreSQL 16, n8n, Tesseract OCR, Docker Compose, GitHub Actions
- Status
- CI green on main (2026-10-18), demo data, private repository
Draft
every automatically read bill, never posted
7%
public-contract retention cap enforced
37/37
n8n workflow tests passing
0
Odoo Enterprise licences needed
This is a product I built, not a client engagement. It runs on demo data for a fictional company; the figures below come from the repository and its CI, not from a customer.
The problem
On a Moroccan construction company's sites, the site manager collects supplier invoices and delivery notes on paper, and the accountant retypes them. Nobody can see the margin of a single site (chantier) until the books are closed. On top of that, Moroccan contracts bill progressively (situations de travaux), hold back a guarantee retention on each bill, and cap that retention at 7% of the contract amount on public contracts.
Constraints
- Odoo Community only. No Enterprise licence, so no Enterprise features to lean on.
- Four users with very different needs. The site manager has a phone, Telegram and no Odoo account. The accountant is careful and distrusts automation. The manager wants the margin at a glance. The admin needs to know quickly whether the setup works.
- Accounting cannot be wrong silently. A misread total must never reach the ledger.
- Free by default. OCR has to work without a paid AI service; a client can plug in their own key for better reading, with a daily cap.
- Small server. The production target is a free ARM instance that already runs n8n and a reverse proxy, so the stack reuses them and the OCR container is held to 512 MB of RAM and one CPU.
Approach
- The site manager sends a photo or PDF to a Telegram bot. n8n checks the sender against a whitelist, hashes the file, and asks Odoo for the OCR settings.
- The file is read. By default a self-hosted Tesseract service (French, Arabic and English) plus a rules parser; if the client set a key, an OpenAI-compatible vision model, falling back to the free path on any error.
- Odoo decides. It re-checks everything it receives (file type and size, totals, VAT rates, the 15-digit ICE, duplicates, dates, which site the bill belongs to) and creates a draft vendor bill on the right site, with the original attached and the failed checks listed in plain French.
- The accountant reviews. A "Factures à vérifier" list, oldest first, with a confidence level (Fiable, À vérifier, Illisible) instead of a percentage, and one button to confirm the extraction.
- Billing and margin. Progressive situations compute the period amount from the cumulative percentage, apply the retention, cap it at 7% on public contracts, and print an A4 PDF with the three signature blocks. A SQL view gives revenue, costs and margin per site, readable on a phone.
Key decisions
- Thin orchestrator, business rules in Odoo. n8n moves files and fills reply templates; every accounting rule lives in the Odoo module. Swapping n8n out would not change a single rule.
- The posting guard sits in
_post, not only on the button. Every posting path in Odoo ends in_post, so a bill still under review cannot be posted from anywhere, and the integration user cannot post at all, even a clean bill. Tests cover both. - Idempotent intake. The file's SHA-256 plus the company is unique, so sending the same photo twice returns "already sent" instead of a second bill.
- Least privilege for the automation. n8n uses a dedicated Odoo user with no direct model access; its only entry point is the intake method, which checks its group.
- Invoice text is data, not instructions. The vision model has no tools and must return JSON that matches a schema, and Odoo validates it again.
- Deploys can roll back. A release copies the database, upgrades the module, waits for the health check and starts the previous release again if anything fails.
What went wrong, and the fix
- A 22% margin showed as 2200%. Margin and progress are stored as 0 to 100, but four views used Odoo's percentage widget, which multiplies by 100. Fixed in all four views (pull request #18).
- The first live import into n8n 2.43.6 refused to publish, and review found the run would have failed right after: a node defaulting to the wrong resource, the OCR step receiving a response with no file, parameters that version 4.2 of the HTTP Request node ignores, and binary data read the old way. Each point is now covered by a workflow structure test that fails on the old workflow (pull request #17).
Verified results
- The legal and accounting rules are tested against a reference case. Contract of 1,000,000 HT, VAT 20%, retention 10% on TTC: the first situation at 30% must give a net payable of exactly 324,000, and the test asserts it to the cent.
- CI is green on
main(run of 2026-10-18): lint, a clean module install on a fresh Odoo 19 database with no warnings allowed, the Odoo module tests, the OCR service tests plus a memory check under three parallel requests, the n8n tests (one of which scans the exported workflows for secrets), and gitleaks. - The 37 n8n workflow and parser tests pass (run on 2026-10-20).
- Backups and restore are scripted: a PostgreSQL dump plus the filestore, keeping 7 daily and 4 weekly copies, with optional encryption and off-site copy, and a restore script that loads a backup into a scratch database.
What I would do next
- Put it in front of one real company. The design targets (a photo answered in under a minute, a draft reviewed in under a minute) are targets until a site manager and an accountant use it.
- Record a demo video so the flow can be seen without access to the private repository.
- Schedule and document a monthly restore test on the production server, not just the script.
Want the same approach on your AWS bill? The AWS Cost Audit is $250 fixed, delivered in 48 hours.