
The idea
Most ERP integrations put the accounting inside the ERP. Then the same rules get written again for the next system, slightly differently, and nobody can say which one is right.
This one is built the other way round. All of the accounting lives in ledger_core,
a Python package with no web framework and no ERP in it: events go in, positions and balanced
journal entries come out. Each ERP gets a thin translator that writes those entries down its own
way. The arithmetic is tested once, in one place.
What the core does
- Exact amounts. Quantities and prices are decimals parsed from text, with a precision declared per instrument: two places for a share, eight for bitcoin. A value with more places is refused rather than rounded. No floating point ever touches a balance.
- Positions are replayed, never stored. Every figure is rebuilt from the events in order, so correcting an import corrects the history. Two cost methods: moving average and FIFO.
- Cost that is honest about itself. Units that arrived from elsewhere with no price carry no cost on record. They count in the quantity, stay out of every cost figure, and each position reports how much of it the cost actually speaks for. Nothing is quietly valued at zero.
- Double entry that balances before it is sent. A purchase capitalises its fee. A sale splits into cash received, the fee as an expense, the book cost of the units sold, and the gain or loss. An unbalanced entry is never posted.
- Reconciliation, not adjustment. The custodian's balances are compared with the ledger's and the differences are reported. Draft corrections wait for a person to accept them.
- Imports that cannot double count. Each row gets a stable id from its own content, so the same statement imported twice adds nothing the second time.
The Odoo addon
Written for Odoo 18: a portfolio book with its cost method, currency, journal and account per role; instruments with their own precision; transactions validated through the core as they are written; and positions rebuilt by a daily cron rather than edited by hand. There is an import wizard that previews what it found and what it skipped before anything is written, reader and manager groups with company record rules, and three buttons on the book: rebuild positions, post journal entries, import statement.
A seed script drives it through Odoo's external API, posts the entries, reads the positions back and compares them with the core's own figures:
Zoho Books and SAP Business One
The same entries go to the Zoho Books REST API and to the SAP Business One Service Layer through two small adapters. Every adapter looks for the entry's reference before creating anything, so a repeated run does not duplicate a journal entry. The adapter tests give each one a transport that answers the way the vendor documents it, then read back what the adapter tried to send, including a check that all three systems receive identical totals.
What this is not
Not a tax computation: the reports give realised amounts by period and holding period, and what is owed depends on the jurisdiction. Not a price feed: valuation uses the prices it is given. Not a trading system: it records what happened and places nothing.
The Zoho and SAP adapters are written against the published API shapes and tested against fakes. Running them against a live tenant needs that tenant's credentials and one pass of verification. That is stated here for the same reason it is stated in the repository: a claim that has not been run against the real thing should say so.
Why it is built this way
If you are choosing an ERP, or moving between two, the expensive part is never the buttons. It is whether the numbers mean the same thing on both sides. Keeping the rules in one tested package, and letting each ERP be a translator, is what makes that answerable.