← Back to work
2025 · Church platform · Odoo 19 + Vue + Flask + Nuxt + Expo

English Chapel Antananarivo

A five-surface platform we built for our own congregation — an Odoo 19 administrative board with the Madagascar accounting localisation we had to ship to make it work, a public Vue 3 website, a Flask + Nuxt 3 cockpit for the worship team, and an Expo companion app for the congregation. Five stacks, one community, made with gratitude.

Role
Architecture · implementation · QA — all five surfaces
Duration
Since 2025, ongoing
Team
All three of us
PROJECT HERO · PLACEHOLDER
FIG. 01
Context

The English Chapel Antananarivo is our home congregation — the English-speaking church the three of us attend on Sundays. It is also, quietly, the most complete thing the workshop has shipped this year: an Odoo 19 board that runs the administration, a Vue 3 website that introduces the chapel, a Nuxt cockpit and Flask API that plan the services and consolidate the slides, and an Expo companion app that puts announcements and lyrics in the congregation's pocket. Five surfaces. One community. The same family wrote them; the same family uses them.

We had to write a Madagascar accounting localisation before we could ship the board. Odoo 19 had no l10n_mg in its base, so we wrote one — the PCG chart of accounts, the taxes (TVA, IRSA, withholding, import VAT), fiscal positions, a fiscal-year model at the company level, and a post-init hook that auto-activates the whole thing for any company whose country is Madagascar. l10n_mg_report adds the statutory documents — Bilan, Compte de Résultat, Tableau des Flux de Trésorerie (direct and indirect), Variation des Capitaux Propres — as both PDF (QWeb) and XLSX (via the OCA report_xlsx engine), with a cron job refreshing the cached values nightly. l10n_mg_tax_form ships the Bordereau de Versement de l'Impôt Synthétique (HETRA TAMBATRA), filled automatically from the Bilan. The chapel needed it. Anyone else with a Malagasy company will, too.

The interesting parts sit where the surfaces meet. eca_offering records each gift against one of five Malagasy payment methods — Espèces, Virement, Mvola, Airtel Money, Orange Money — and posts a journal entry that debits the matching account (531100 / 512100 / 530002 / 530003 / 530004) and credits 756100, Libéralités perçues. References follow OFF/YYYY/NNNN from an `ir.sequence`. Once posted, the record is write-locked except for notes and state — cancellation produces a reversal, not a quiet delete. The mobile-money trio is the half of the country's giving habits a generic localisation would have missed.

The companion app and the worship cockpit close the loop. The announcement module exposes a bearer-auth JSON-RPC controller — because Odoo 19's stock `/web/dataset/call_kw` is session-only and refuses API keys outright — wired against an `ECA Bot` user whose login is `bot@english-chapel-antananarivo.org` and whose API key the mobile app carries. The Flask worship API holds the songs and, for any given service, opens each song's PowerPoint, copies the slides into a single consolidated deck — layouts, backgrounds, placeholder text, font sizes, paragraph alignment — and hands a URL back to the Nuxt cockpit. On Sunday morning the diaporama screen on the companion app projects the resulting deck. None of it requires anyone to be in the same room as a laptop.

Scope

What we built.

eca01

Root Odoo 19 module: top-level menu, security groups, an access-request flow for new staff joining the board.

eca_offering02

Offering management — five Malagasy payment methods, immutable-once-posted, auto journal entry against 756100 with OFF/YYYY/NNNN references.

eca_announcement03

Announcement board with a custom bearer-auth REST controller for the mobile companion. Backed by an `ECA Bot` user whose API key gates inbound calls.

eca_pv04

Meeting minutes (CA, AG, AGE) — rich-text content threaded through mail.thread.

l10n_mg05

Madagascar accounting localisation: PCG chart of accounts, taxes, fiscal positions, fiscal-year model. Auto-activates via `_l10n_mg_post_init_hook` for any MG-based company.

l10n_mg_report06

Statutory reports — Bilan, CDR, TFT direct / indirect, Variation Capitaux Propres — as PDF (QWeb) and XLSX (OCA report_xlsx), refreshed by cron.

l10n_mg_tax_form07

Impôt Synthétique (HETRA TAMBATRA) — Bordereau de Versement filled automatically from Bilan data, with NIF / STAT fields on res.company.

eca_website08

Vue 3 + Vite SPA — public church website. Home, Community, Leadership, Mobile App, About, Privacy. Bilingual EN / FR via vue-i18n; Pinia store; ChapelMap component for directions.

worship_team_backend09

Flask + SQLAlchemy REST API — songs, services, users, file uploads. JWT (HS256) auth. Generates consolidated service PowerPoints with python-pptx. Storage factory; APK distribution through presigned S3.

worship_team_frontend10

Nuxt 3 cockpit for the worship team — services, songs, users. Pinia auth store; bilingual; drag-and-drop service composition (vuedraggable).

eca_companion11

Expo / React Native mobile app — announcements, services, songs, a live diaporama screen for Sundays. Talks to both the Flask worship API and the Odoo announcement controller.

infra (Terraform)12

AWS resources behind the platform — S3 buckets for APK distribution, two IAM users (reader + publisher) so a leaked reader key cannot grant write.

Approach

What the work looked like, in four pieces.

01

Madagascar's accounting, end to end

Odoo 19 didn't ship a Madagascar localisation, so we wrote it. PCG chart of accounts, TVA / IRSA / withholding / import-VAT taxes, fiscal-year model at company level, statutory reports (Bilan, CDR, TFT, Variation Capitaux Propres) as PDF and XLSX, Impôt Synthétique tax form filled from the Bilan. Auto-activates via `_l10n_mg_post_init_hook` for any company with country = MG. The chapel needed it; so will any other entity registered in Madagascar.

02

Mobile money where the country actually pays

eca_offering records each gift against one of five payment methods — Espèces, Virement, Mvola, Airtel Money, Orange Money — and posts a journal entry that debits the matching account and credits 756100, Libéralités perçues. Posted offerings are write-locked except for notes and state. Cancelling produces a reversal, not a quiet delete. The mobile-money trio is the half of the country’s giving habits a generic localisation would have missed.

03

Five surfaces, one community

The Odoo board is where the chapel lives administratively. The website introduces it to visitors. The worship cockpit plans the services. The Flask API holds the songs and consolidates the Sunday slides. The mobile companion reads announcements and shows the diaporama. The shapes are different; the seams are deliberate. The announcement controller speaks bearer-auth REST because Odoo 19's call_kw is session-only and refuses API keys — so we built the contract the mobile app actually needs.

04

For our own people, at our own pace

ECA is our congregation. There is no client deadline, no commercial scope creep — just the work the chapel actually needs, shipped at the cadence it can absorb. The platform exists because three engineers attend the same church and like the idea of being useful to it. The standards are the workshop's standards, applied without negotiation.

Engineering highlights

A handful of the solves we are proudest of.

01

l10n_mg auto-activation hook

`_l10n_mg_post_init_hook` runs at install and walks every `res.company` whose `country_id` is Madagascar, loading the chart template, taxes, fiscal positions and default accounts. A new MG company set up later inherits the localisation through the same hook without a manual step. Statutory reports get a daily cron (`data/ir_cron.xml`) refreshing the cached values for Bilan, CDR and TFT.

02

Offering → journal entry mapping

eca_offering defines a `_DEBIT_ACCOUNT_CODES` map — `{cash: 531100, bank: 512100, mvola: 530002, airtel: 530003, orange: 530004}` — and on confirmation posts an `account.move` that debits the matching cash / bank / mobile-money account and credits 756100 (Libéralités perçues). The reference follows `OFF/YYYY/NNNN` from an `ir.sequence`. `write()` is overridden to refuse anything but notes and state once the offering is posted; cancellation goes through `unlink()` carefully.

03

Bearer-auth REST for the mobile app

Odoo 19's stock `/web/dataset/call_kw` uses `auth='user'` and rejects API keys outright. `eca_announcement.controllers.AnnouncementController` therefore ships `@http.route(..., auth='bearer', methods=['POST'], csrf=False)` endpoints — `/api/announcements` and `/api/announcements/<id>` — wired against an `ECA Bot` user. `./eca/manage.sh apikey` mints the key without going through the Odoo UI. The mobile app carries the key in a Bearer header; nothing else needs to know it exists.

04

Cross-presentation PPT consolidation

`Service.generate_consolidated_ppt()` opens each song's PowerPoint with python-pptx and copies the slides into a fresh deck — chosen layouts (`title and body` for content, `blank` otherwise), slide background fills (so a song's black background travels), placeholder text by index, free-floating text shapes with word-wrap and vertical-anchor preserved, font sizes and colours and bold flags, paragraph alignment. Songs without a stored PPT are generated from their lyrics on demand. The output is named `service_YYYYMMDD_<hash>.pptx` and the old consolidated file is removed on regenerate.

05

Two IAM users for APK distribution

The companion app is delivered as an APK in S3. The Flask backend signs short-lived GetObject URLs for users (`APK_URL_TTL` defaults to 3600s) using a `worship-releases-reader` IAM user. The release script (`./scripts/release-apk.sh`) uploads new builds using a separate `worship-releases-publisher` user. A leaked reader key cannot push a malicious APK — it can only read what is already there.

06

Bilingual UI across every surface

The website, the cockpit and the companion app all carry an EN / FR pair. The website uses vue-i18n with `tm` + `rt` for translated arrays; the cockpit uses Nuxt + @nuxt/i18n; the companion uses i18n-js. Strings live in one place per app, never inline in components. The chapel is bilingual; the platform refuses to be less so.

07

Statutory reports as both PDF and XLSX

Every Madagascar report (Bilan, CDR, TFT direct / indirect, Variation Capitaux Propres) is implemented twice — once as a QWeb PDF template, once as an XLSX class extending the OCA `report_xlsx`'s `AbstractReportXlsx`. The two views share the same computed payload; whichever the accountant prefers, the numbers match.

Outcomes

A few shapes, in their raw form.

5
Surfaces — Odoo, website, API, cockpit, mobile
8
Custom Odoo modules, incl. full l10n_mg
5
Payment methods — incl. Mvola / Airtel / Orange
For us
Made with gratitude in Antananarivo

Stack
Odoo 19PythonFlaskPostgreSQLVue 3Nuxt 3ExpoReact Nativepython-pptxAWS S3TerraformDocker

Have a project that deserves this kind of care?

Start a conversation