Mohamed ARKID logo

Command Palette

Search for a command to run...

Command Palette

Search for a command to run...

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.