| Availability |
Odoo Online
Odoo.sh
On Premise
|
| Odoo Apps Dependencies |
•
Invoicing (account)
• Discuss (mail) |
| Lines of code | 2880 |
| Technical Name |
eh_account_base |
| License | LGPL-3 |
| Website | https://www.erpheritage.com.au/ |
| Availability |
Odoo Online
Odoo.sh
On Premise
|
| Odoo Apps Dependencies |
•
Invoicing (account)
• Discuss (mail) |
| Lines of code | 2880 |
| Technical Name |
eh_account_base |
| License | LGPL-3 |
| Website | https://www.erpheritage.com.au/ |
Accounting Suite Base Engine
The shared reporting engine under the ERP Heritage accounting modules: an append-only render audit log, precise per-company cache invalidation, and a whitelist-only parameterised SQL builder.
Why this module
Accounting Suite Base Engine
Every render is on the record
Each report run writes an append-only execution row: the user, the timestamp, the full options snapshot, the runtime, and a SHA-256 result hash for XLSX and PDF. Re-run the same options against an unchanged ledger and the figures reproduce exactly. Even a cache hit writes its own row, and a failed render is recorded as an error, not lost.
Cache that knows when to expire
A per-company move-version counter bumps atomically on every account.move state change. A cache hit therefore means nothing has posted, drafted, or cancelled since the payload was built. The counter is a single SQL UPDATE that adds one, so it is correct under concurrency, and it invalidates only the companies that actually changed.
SQL with no room for injection
The reporting hot path runs through a parameterised, analytic-aware SQL builder. Every value binds as a parameter, every identifier comes from a fixed whitelist, and every query is company-scoped, so there is no raw interpolation and no cross-company leakage. It is plain Python you can unit-test without spinning up the Odoo registry.
Day in the life
An auditor asks how last quarter's Balance Sheet was produced. Can you prove it?
You open the report-execution log and filter to that report. There is the exact row: who ran it, the timestamp, the full options snapshot (period, companies, comparison settings), the runtime, and the SHA-256 hash of the exported file. You re-run the report from that stored snapshot against the unchanged ledger and the cache returns the same payload, so the figures match line for line. The auditor logs in under the read-only Auditor role and reads the same trail without any ability to post, edit, or delete a thing. Meanwhile a colleague posts a correcting journal in another company: that company's move-version counter ticks, its cached reports recompute on the next run, and the company you are reviewing is untouched.
Edge cases
The cases most modules quietly ignore.
In the shipped code today, each one a place where a cheaper module silently does the wrong thing.
A write() that includes state in the values but does not actually change it (a routine recompute) does not spuriously bump the move-version counter or invalidate the cache. The override snapshots the prior state per record and bumps only the ids where the state genuinely transitioned.
A move created already in state posted, bypassing action_post entirely, still invalidates the cache. The create() override filters posted moves and bumps their companies' counters, so a back-dated or scripted insert cannot leave a stale report standing.
A render for companies [1,2] is never served a payload computed for [1] alone. The cache keys on a sorted-comma company set for exact equality, not an IN match that would treat overlapping sets as a hit, so a wider scope can never collide with a narrower cached one.
A cache hit still writes its own audit row pointing back at the source execution, so the compliance trail records every render, not just the first. You can prove how many times a figure was produced and from which cached source.
The per-company counter uses UPDATE col = col + 1 rather than a read-then-write, so two posts landing at the same instant both count and neither is lost, and only the affected company rows are invalidated.
Per-company account code and translated account name are resolved at SQL time from jsonb stores with en_US fallback, and GROUP BY is kept in lock-step with SELECT and ORDER BY so the query does not throw a Postgres GROUP BY error under a non-en_US language.
When the companies in scope hold different currencies, the payload is marked multi_currency and the XLSX writer renders amounts with no symbol rather than stamping a wrong one. It refuses to imply a single currency that does not exist.
The cache codec carries a one-byte version prefix so future compression formats can coexist with already-stored entries, and an unknown version raises rather than silently mis-decoding an old payload into garbage.
What is inside
Built to do the job, end to end.
- Append-only execution audit log. eh.account.report.execution captures user, timestamp, the full options dict, runtime, and a SHA-256 result hash for XLSX and PDF output. Failed renders are marked error with the message and the exception is re-raised, so nothing disappears.
- Atomic move-version cache. res.company gains an ORM-readonly eh_move_version field maintained by a direct SQL UPDATE and read only by the cache infrastructure. Payloads are served on a strict company-scope hit and recomputed on the next state change.
- Whitelist-only parameterised SQL builder. A multi-company, analytic-aware query composer for the reporting hot path. Values bind as parameters, identifiers come from a fixed whitelist, no user input touches raw SQL, every query is company-scoped, and it runs as plain Python under its own unit tests.
- Three-tier privilege model. User, Manager, and a read-only Auditor role, decoupled from the standard Odoo accounting groups, so an auditor reads the execution log and reports with no write access.
- Currency-aware XLSX writer. Report payloads carry a per-scope currency block; the bundled XLSX writer renders amounts with the correct symbol, decimal places, and symbol position, and falls back to no symbol when the scope is genuinely multi-currency.
- ORM-free, unit-tested tooling. The SQL builder, the zlib payload codec, and the XLSX writer are plain Python backed by standalone unit tests that need no Odoo registry, which keeps the hot path predictable and fast to test.
Honest about the edges
What this does not do, so nothing surprises you.
- This is an engine module, not an end-user reporting app. It auto-installs as a dependency of the ERP Heritage accounting modules and exposes the audit log and infrastructure; the user-facing financial reports (P&L, Balance Sheet, General Ledger, and so on) live in the reporting modules that depend on it.
- This module ships no OWL report viewer and no web assets bundle. The on-screen interactive viewer is provided by the downstream reporting modules, which consume the currency block this engine produces.
- Currency-aware rendering here covers the bundled XLSX writer only. The PDF report shipped in this module prints plain numeric values with no currency symbol; symbol-aware PDF rendering of the currency block is done by the downstream reporting modules.
- The SHA-256 result hash is written only when the caller supplies it, which the XLSX and PDF export paths do. The default JSON render path stores the full options snapshot but no result hash. Reproducibility on that path comes from replaying the stored cached payload, not from a re-proven byte-identical recompute.
- The move-version counter bumps on account.move state changes (post, set-to-draft, cancel). Hard-deleting a move record at the database level does not bump the counter.
Odoo 19 Community accounting reporting engine, reproducible financial report audit log, report execution audit trail Odoo, move-version report cache invalidation, multi-company parameterised SQL builder Odoo, append-only audit log accounting, read-only auditor role Odoo accounting, SHA-256 report hash compliance Odoo, currency-aware XLSX report export Odoo, analytic-aware journal item aggregation
Please log in to comment on this module