ERP World View · Doctrine
Every ERP's vocabulary, mapped to the one canonical model the engine folds into. Read a row across: the same business fact, named five ways, retained faithfully in one AD shape.
kernel_ops log + its projection. Every
other ERP is not a second system — it is data re-keyed into that one model. account.account
(Odoo), SKA1 (SAP) and GL_CODE_COMBINATIONS (Oracle) all land as one thing:
C_ElementValue. Once folded there is no "Odoo logic" or "SAP logic" resident — only the canonical fold +
the closed verb set. This table is the dictionary that lets an iDempiere veteran read an Odoo (or SAP) chart and
confidently fold a counterpart into the same shape, because they recognise every term in the Canonical column.
maxDiff=0c)
○ REFERENCE public data-model mapping; fold not yet witnessed
Provenance by source column: iDempiere ◆ canonical · Odoo ✅ folded (the O2C/P2P rows
shipped in gen_ad_odoo.js, oracle-diffed) · SAP S/4 · Oracle EBS · Dynamics BC ○
reference (their standard table/term names are public and accurate; our fold of them is master-mapping only so
far — see the honesty panel). A cell marked “—” means the concept has no direct
named home in that product (often it is imperative code there, not a table).
| Concept | Canonical (AD / Fold) ◆ | iDempiere ◆ | Odoo ✅ | SAP S/4 ○ | Oracle EBS ○ | Dynamics BC ○ |
|---|---|---|---|---|---|---|
| Tenant / company | AD_Client | AD_Client | res.company | T001 (BUKRS) | GL Ledger / OU | Company |
| Organisation / unit | AD_Org | AD_Org | (branch / analytic) | WERKS · VKORG | Inventory Org / OU | Location / Dimension |
| User | AD_User | AD_User | res.users | USR02 (SU01) | FND_USER | User |
| Role / access grant | AD_Role + *_Access | AD_Role | res.groups · ir.model.access | PFCG role / auth obj | Responsibility | Permission Set |
| Accounting schema | C_AcctSchema | C_AcctSchema | (company CoA + currency) | Ledger 0L / Ctrl Area | Set of Books / Ledger | G/L Setup |
| Concept | Canonical (AD / Fold) ◆ | iDempiere ◆ | Odoo ✅ | SAP S/4 ○ | Oracle EBS ○ | Dynamics BC ○ |
|---|---|---|---|---|---|---|
| Business partner | C_BPartner | C_BPartner | res.partner | BUT000 · KNA1 / LFA1 | HZ_PARTIES / AP_SUPPLIERS | Customer / Vendor |
| Product / item | M_Product | M_Product | product.product | MARA (+MARC/MBEW) | MTL_SYSTEM_ITEMS_B | Item |
| Product category | M_Product_Category | M_Product_Category | product.category | MATKL (material group) | Item Category | Item Category |
| Price list | M_PriceList(_Version) | M_PriceList | product.pricelist | A004 / VK11 conditions | QP price list | Sales Price |
| Tax | C_Tax | C_Tax | account.tax | MWSKZ (FTXP) | eBTax | VAT Posting Setup |
| G/L account (CoA) | C_ElementValue | C_ElementValue | account.account | SKA1 / SKB1 | GL_CODE_COMBINATIONS (seg) | G/L Account |
| Account combination | C_ValidCombination | C_ValidCombination | (account + analytic) | ACDOCA assignment fields | GL_CODE_COMBINATIONS | Dimension combination |
| Concept | Canonical (AD / Fold) ◆ | iDempiere ◆ | Odoo ✅ | SAP S/4 ○ | Oracle EBS ○ | Dynamics BC ○ |
|---|---|---|---|---|---|---|
| Sales order | C_Order (SO) | C_Order | sale.order(.line) | VBAK / VBAP | OE_ORDER_HEADERS_ALL | Sales Header / Line |
| Delivery / shipment | M_InOut | M_InOut | stock.picking (out) | LIKP / LIPS | WSH_DELIVERY | Posted Sales Shipment |
| Sales invoice | C_Invoice (ARI) | C_Invoice | account.move (out_invoice) | VBRK / VBRP | RA_CUSTOMER_TRX | Sales Invoice Header |
| Customer receipt | C_Payment | C_Payment | account.payment | BSEG (incoming) / DZ | AR_CASH_RECEIPTS_ALL | Cash Receipt |
| Concept | Canonical (AD / Fold) ◆ | iDempiere ◆ | Odoo ✅ | SAP S/4 ○ | Oracle EBS ○ | Dynamics BC ○ |
|---|---|---|---|---|---|---|
| Purchase order | C_Order (PO) | C_Order | purchase.order(.line) | EKKO / EKPO | PO_HEADERS_ALL | Purchase Header / Line |
| Goods receipt | M_InOut (vendor) | M_InOut | stock.picking (in) | MKPF / MSEG (MIGO) | RCV_TRANSACTIONS | Posted Purchase Receipt |
| Vendor invoice | C_Invoice (API) | C_Invoice | account.move (in_invoice) | RBKP / RSEG (MIRO) | AP_INVOICES_ALL | Purchase Invoice |
| 3-way match | M_MatchInv | M_MatchInv | (reconcile / valuation) | GR/IR clearing (WRX) | PO–receipt–invoice match | Item Application |
| Concept | Canonical (AD / Fold) ◆ | iDempiere ◆ | Odoo ✅ | SAP S/4 ○ | Oracle EBS ○ | Dynamics BC ○ |
|---|---|---|---|---|---|---|
| Posted journal (the books) | fact_acct / journal | Fact_Acct | account.move.line | ACDOCA (BSEG+BKPF) | GL_JE_LINES / XLA_AE_LINES | G/L Entry |
| Document flow / lineage | kernel_ops chain | (doc origin fields) | (invoice_origin) | VBFA (document flow) | XLA links | Navigate / applied entries |
| Trial balance / statement | foldStatement / foldTrialBalance | PA_Report | (financial report) | FAGLB03 / Report Painter | FSG / FRx | Account Schedule |
| Concept | Canonical (AD / Fold) ◆ | iDempiere ◆ | Odoo ✅ | SAP S/4 ○ | Oracle EBS ○ | Dynamics BC ○ |
|---|---|---|---|---|---|---|
| Document status | DocStatus + C_DocType FSM | C_DocType / DocumentEngine | state field (Python) | Status mgmt (BSVZ) | Workflow status | Document Status enum |
| Workflow engine (WfMC) | AD_Workflow | AD_Workflow | automated actions wkf removed v8 | SAP Business Workflow | Oracle Workflow Builder | Power Automate / approvals |
| Validation rule | AdModelVal · AD_Val_Rule | ModelValidator | @api.constrains (code) | BAdI / GGB0 (code) | business event (code) | OnValidate trigger (code) |
| Field derive / callout | AdCallout · AD_Column.Callout | Callout | onchange (code) | field exit / PBO (code) | defaulting rule | OnAfterValidate (code) |
| Change log / provenance | kernel_ops (signed op-log) | AD_ChangeLog | mail.tracking.value | CDHDR / CDPOS | FND audit trail | Change Log Entry |
The Rosetta rows above show where a fact lives. This section shows what it costs to change one. Start with the most boring change in any ERP: a clerk keeps overwriting an auto-filled field, so the business says “lock the Sales Order Description field — make it read-only.” Read the two columns down.
| Step | iDempiere (Java monolith) ◆ | Fold Engine (AD-declarative) ✅ |
|---|---|---|
| What you edit | AD_Field.IsReadOnly = 'Y' — one dictionary row | the same AD_Field.IsReadOnly = 'Y' |
| Through what | running Java server + Postgres + admin login, via the AD window | edit the row → signed op-log append (kernel_ops) |
| Recompile? | No — it is AD-driven | No — re-fold = re-read, not recompile |
| Takes effect | reset cache / re-login | on the spot — the form repaints, no reload |
| Reaches other sites | central DB only | op-log replays to every node, signature verified |
| Server needed | yes | none |
Witnessed live: W-AD-SELFEDIT — a signed AD_Field edit re-folds the open form
on the spot (field count 26→25→26), no reload, no recompile. The honest point: even iDempiere makes this change
declarative — that is exactly why the engine borrows its dictionary rather than re-authoring it. The Fold
Engine does not win on “is it config”; it wins on no server, no cache reset, and self-propagation by op-log.
Now the gradient. Cost stays near-zero while a change is declarative, and rises only when the logic is genuinely imperative — and even then it is bounded to a single plugin.
| Change request | Nature | iDempiere cost ◆ | Fold Engine cost ✅ |
|---|---|---|---|
| Lock a field (read-only / mandatory) | declarative | edit AD row, reset cache | edit AD row → op-log → re-fold, no server |
| Default payment term from the partner | declarative binding | set AD_Column.Callout | set AD_Column.Callout → AdCallout dispatch |
| Block an order over credit limit | declarative rule | AD_Val_Rule config | AD_Val_Rule → AdModelVal fold |
| Bespoke rebate arithmetic | imperative (custom) | new Java class, recompile, restart | one plugin (.foldbundle), engine untouched |
| Change how an order posts to GL | imperative (core) | edit MOrder.completeIt(), rebuild + redeploy the monolith | fold the op-log effect (config + post_resolver), else a plugin |
§7 in numbers — one real change, counted. Not a stopwatch (noisy) and
not the whole-codebase LOC ratio — the effort surface of one identical change done both ways:
“block completing a Sales Order when it would breach a custom CreditCeiling on the customer” (a real
customisation that genuinely needs a Java ModelValidator on the iDempiere side, not config).
| Effort axis (one real change) | iDempiere — OSGi ◆ | Here — fold ✅ |
|---|---|---|
| Lines to touch * | ~50 across 4 files | ~16 across 2 files |
| Handling checks (steps that can fail) | 9 — import-package · register · AD sync · build · resolve · restart · log-clean · 2× acceptance | 4 — re-fold reads · handler pinned · 2× acceptance |
| Data / DB artifacts | 5 AD rows + DDL, carried by a 2Pack/SQL migration re-applied & verified on every target instance (×N) | 1 declarative slice edit — the slice is the schema; 0 migrations: one signed op-log append self-replays to all N nodes |
| Forced build + restart | 2 — compile + server restart | 0 |
* Lines is a forecast estimate (each line enumerated in the
witness card), pending an actual wc -l on a built artifact each side. The other three axes — checks,
DB artifacts, build/restart — are structural facts, exact: they are fixed by OSGi (which forces a
build→resolve→restart loop and a per-instance migration) versus the fold (which forces neither). The decisive saving is
the bottom two rows: build+restart 2 → 0, and a per-instance migration ×N collapsing to one signed op-log append.
A common first reading is that this architecture just moves cost: you save on UI and DB binding, then pay it back managing a cryptographic op-log. That is the wrong equation — it confuses a one-time substrate with a recurring tax. The honest accounting:
| Cost component | Scales with… | Honest size |
|---|---|---|
| Op-log / integrity substrate (“git-for-data”) | nothing — written once | fixed. Orthogonal to rule count; it secures whatever the fold produces across nodes. Not a per-rule tax. |
| Business-rule complexity (O2C, P2P, GL, FX) | dictionary coverage | cheap because the dictionary is borrowed and already proven by 20 years of iDempiere production — you pay to interpret, not to author. |
| UI / dictionary stitch | surfaces exposed | the real ongoing engineering — surfacing the engine correctly per window. |
| Oracle-diffing discipline | claims of equivalence | the proof the interpretation is faithful — diff to real iDempiere (idempiere_test) to the cent. |
W-AD-SELFEDIT — §10).
Sources: maintenance share of TCO — ScienceSoft · Galorath (IEEE 60–80%); ERP vendor support % of license — Panorama Consulting (18–25%) · Panorama — Cloud ERP Cost; build-once vs maintain-forever — Idealink. Peer-reviewed primary sources: Lientz & Swanson, CACM (1978) — the foundational 487-organisation survey · Glass, Facts and Fallacies of Software Engineering (2002) — the canonical 40–80% of lifecycle cost range · Erlikh, IEEE IT Professional 2(3):17–23 (2000) — legacy upkeep ≈80% of TCO; categories standardised in ISO/IEC 14764. Not a universal law: the range is wide by design — long-lived, regulated, heavily-customised systems (ERP is the textbook case) sit high (70–90%); short-lived or throwaway software sits low (20–40%). So this is strongest exactly where ERP lives, not a claim about all software. Figures are third-party research; our contribution is only the architecture that moves the saving onto that bucket.
If you have shipped ModelValidators, Callouts and custom Doc_* posting as
OSGi bundles, most of what you maintain is not business logic — it is OSGi plumbing and upgrade-breakage.
That is exactly the recurring cost the table below removes. Read it idiom for idiom: the OSGi tax disappears; the
business logic does not.
| What you maintain in iDempiere / OSGi today | Here (Fold Engine) ✅ |
|---|---|
MANIFEST.MF + Import-Package/Export-Package version ranges, bnd | none — a plugin is a JS .foldbundle module, no manifest wiring |
| Tycho/Maven build, target platform, p2 repository | none — no compile step, no target platform |
Declarative Services components, Bundle Activator, IModelValidationEngine.addModelValidator | register a handler in the registry — no DS lifecycle |
Drop bundle in plugins/, restart the OSGi runtime, resolve wiring | ship the module — re-fold = re-read, no restart |
ClassNotFoundException / unresolved-bundle / classloader-leak debugging | gone — no classloader graph to resolve |
| Every iDempiere upgrade can break your plugin (Java API drift, package bumps) | you fold the dictionary, not compile against a moving Java API — the AD is the stable contract |
.foldbundle plugin instead of an OSGi DS
component (doctrine: foreign imperative code is never auto-imported — it becomes a plugin). Money discipline is
identical: your BigDecimal · HALF_UP · integer-cents habits carry 1:1 —
bigdecimal.js is proven bit-equal to Java BigDecimal. And you recognise every dictionary
point: a ModelValidator hook → AdModelVal/AD_Val_Rule; a Callout →
AD_Column.Callout → AdCallout; custom Doc_* posting → post_resolver (config) or a plugin.
ModelValidators/Callouts
expressing what the AD can hold declaratively — mandatory, read-only, default, validation. Those stop being a bundle
at all: they become an AD_Field/AD_Val_Rule edit that re-folds live (witnessed: W-AD-SELFEDIT).
The plugin you'd have built and maintained forever no longer exists.
kernel_ops chain, not AD_ChangeLog. Registration is explicit, not OSGi service auto-wiring
(the host handler set is pinned). Plugins are JS, not Java — idiom translation for a model/callout/validator dev;
a bigger shift if your work was deep ZK UI. Honest caveats: the JS plugin SDK is younger than 20 years of OSGi
ecosystem (fewer examples, more edges), and deep ZK customisation does not map 1:1 — that is the “stitch” cost §8 names.
The sections above argue the shape of the cost. This one is just a tape measure. Every figure below is
a real du/wc/sqlite count of iDempiere itself against this engine — same demo
tenant (GardenWorld), measured 2026-06-06 / -12, no estimates in the left three columns.
| Layer (measured) | iDempiere — Java / Postgres ◆ | This — JS / SQLite ✅ | Reduction |
|---|---|---|---|
| Definition — the AD seed | Adempiere_pg.dmp = 45.2 MB | ad_seed.db = 26.1 MB (full-width, no slice-holes) | ~1.7× |
| A live tenant on disk | Postgres 143 MB (41 heap + 39 idx + 6 TOAST + ~57 bloat) | SQLite 43 MB (925 tables · 187,133 rows) → 8.5 MB zstd backup | 3.3× (16.8× compressed) |
| Runtime code | 1,427,147 Java LOC · 4,465 files · 294 .zul · 60 plugins | 28,184 JS LOC · 132 files (static + SQLite-WASM) | ~51× lines, built so far |
| Stack to run it | JVM + Postgres + ~3.7 GB build, server-bound | static files, offline-capable, no server of record | server → none |
M*.java ≈ 105K code-LOC). The code ratio
falls as real coverage grows (it was 89× → 76× → 51× as more was folded) — so the honest headline is the
conservative ~21× projected at full parity (≈68K JS), not the eye-catching early number. What is genuinely
removable is the server/build stack and the generic AD-interpretation engine (leaner because the dictionary
is self-describing); what is irreducible is each transactional verb, folded deterministically — that is the whole
thesis, stated against its own measuring tape.
Source: real measurement, not estimate — internal/BLOAT_MEASUREMENT.md (live docker Postgres
→ migrate_pg_to_sqlite.js, 2026-06-06) + LOC re-counted 2026-06-12; the model-layer denominator is
docs/ERP_MODEL_ARCHETYPE.md (the MOrder archetype + ~25 completeIt deltas). Full coverage
accounting: Coverage Matrix.
Companions: Benchmark Comparison — the engine vs iDempiere live in-browser; both sides measured from real source (51× LOC, 26× by size), re-counted each deploy · Fold Engine Black Book — the deep dev manual (op-log programming, how to extend the engine) · ERP Rosetta Stone (iDempiere-Java → Fold Engine) — the developer idiom map · ACDOCA Fold Plan — the SAP headline (the Universal-Journal fold) · Coverage Matrix — which surfaces are proven equivalent · Migrate & Compare — the thesis & honesty panel. · Reference table/term names are public product documentation; their fold into the canonical column is proven only for iDempiere (◆) and Odoo (✅) today — SAP/Oracle/Dynamics (○) are the campaign ahead.