BIM → ERP Bridge — Blueprint¶
Status: PAPER spec (no code yet). Umbrella for the element↔ERP link. Prime alignment: EXTRACT OR COMPILE ONLY. The bridge reads what extraction produced and writes only what it can trace to an element. Never invent a product, a price, a quantity, or an asset. Money/qty math via
site/bigdecimal.jsonly (seememory feedback_numbers_via_bigdecimal). Three Concerns (BOM PRINCIPLE) stay split: WHAT =M_Product/M_Product_Category; HOW = UOM + price + BOM rules; WHERE = the ERP.db (build/erp/ad_*.db).
Executable lane: prompts/BIM_TO_ERP.md. Related interop: prompts/BCF_INTEROP.md
(BCF rides the same Export icon). ERP engine source-of-truth: build/erp/ (see docs/ERP.md).
§0 — THE JOIN (the one fact everything hangs on)¶
The bridge key is the IFC GUID already carried as elements_meta.guid in every served
building DB. On the ERP side it must land on a single, deterministic column so read
and write agree. Candidate (CONFIRM in build/erp/ad_*.db):
A_Asset.SerNo(or a dedicatedIFC_GUIDcolumn /M_AttributeSetInstance) holds the element GUID → one physical occurrence = one asset row.M_Product.Value= the element's Type/classification (e.g. IfcDoor family-type, or a Uniclass/OmniClass code if present) → one product = N occurrences of that type.
So: Type → M_Product (the recipe), occurrence(GUID) → A_Asset (the instance).
That is exactly the BOM PRINCIPLE — one parent product, N child assets.
OPEN-1 (confirm before build): which column carries the GUID on
A_Asset? OPEN-2: is the warehouse tableM_StorageOnHand(newer iDempiere) orM_Storage(grep foundM_Storagein the seed)? Pick the one the canonical ERP.db actually has. OPEN-3: whichbuild/erp/ad_*.dbis the canonical ERP.db the viewer reads/writes?
§A — READ side: "Check ERP" on a selected element (data-gated)¶
Scenario. User selects an element (single-select is cleaner than right-click — reuse the
existing picking.js → info panel path). The info-panel box shows its properties. If — and
only if — there is a real linked record or a cost in the OpLog for that GUID, a small
"Check ERP" chip lights up on the panel. Plain element with nothing behind it → no chip.
This is the non-invent gate, identical in spirit to the lens probe (W-LENS-PROBE in
docs/RevitParity.md): an affordance appears only when its query returns rows.
Probe (what lights the chip). At select time, run the cheap checks:
1. Does the OpLog (kernel_ops) / 5D path hold a computed cost for this GUID? AND/OR
2. Does an A_Asset (by GUID) or a matching M_Product (by Type→Value) exist in ERP.db?
If either is true → chip on, else chip stays hidden.
Drawer (what the chip opens). Tapping the chip slides a drawer (side or bottom — match the
existing panel/drawer style) showing the element's ERP facts, all read from ERP.db + OpLog:
- M_Product (Value, Name, Category, C_UOM)
- price from the active M_PriceList → M_ProductPrice.PriceStd (BigDecimal)
- A_Asset row(s) for this GUID (asset value, serial/GUID)
- on-hand qty (M_StorageOnHand/M_Storage) by UOM
- (later) linked documents — POs, maintenance — if the schema carries them
Witnesses.
- W-ERP-PROBE: §ERP_PROBE guid=<g> cost=<bool> product=<bool> asset=<bool> — chip
visibility == (cost || product || asset). NO chip when all three false (the non-invent proof).
- W-ERP-DRAWER: §ERP_DRAWER guid=<g> product=<Value> price=<BigDecimal> uom=<EA|M|M2|M3> asset=<n>
— every field traces to a row; price folds exact == golden.
§B — WRITE side: "Construction → ERP" clean export (another Export-icon child)¶
Scenario. From a selected element (or all elements of a Type), "clean export" pushes the construction reality into ERP.db, creating only what's missing. Per element/Type:
- Resolve UOM. From the element's QTO: count→
EA, length→M, area→M2, volume→M3(map to the realC_UOMrows). Skip any element with no resolvable UOM qty — non-invent. - Find-or-create
M_Product. Look up byValue= Type/classification. If absent, INSERTM_Product(Value, Name,M_Product_Category_ID,C_UOM_ID). Deterministic Value derivation (no random IDs) so re-export is idempotent. - Price →
M_ProductPrice. Upsert into the activeM_PriceList/version:PriceStd= 5D pack rate × (unit) folded viasite/bigdecimal.js(rates fromrates/*.json— locale pack, currency-tagged; never the unlabeledA.MATERIAL_COSTSfallback). Currency follows the pack'smeta.currency. - Occurrence →
A_Asset. Find-or-create oneA_Assetper element GUID (link column per §0/OPEN-1). Asset value = qty × PriceStd (BigDecimal). - On-hand →
M_StorageOnHand/M_Storage. SetQtyOnHandby UOM at the chosen locator/warehouse (CONFIRM OPEN-2/locator).
Idempotent + traceable. Re-running export must not duplicate: find-or-create keyed on
Value/GUID. Every created row carries the GUID or Value that produced it. Nothing is written
that isn't extracted from the model.
Witnesses.
- W-ERP-EXPORT: §ERP_EXPORT type=<T> products=+<np> prices=+<npr> assets=+<na> onhand=+<ns> uom=<...>
— counts == elements processed; second run reports +0 (idempotent).
- W-ERP-FOLD: total exported asset value folds exact == sum(qty×PriceStd) golden (BigDecimal).
§C — UI surface: the Export chooser (one reusable component)¶
The Export pill (report in panels.js — barChart, already ui_tt_export) opens a small
chooser box, not a one-shot. The chooser presents the model/data outputs as multi-select:
Export ▸ ┌─────────────────────────┐
│ Model/Data │
│ ☐ IFC ☐ BCF │
│ ☐ DB ☐ ERP │
│ Reports │
│ ☐ 4D ☐ 5D ☐ DXF ☐ Excel│
│ ─────────────── [All] │
│ [ Export ] │
└─────────────────────────┘
- IFC =
ifc_export_worker.js(the model out). BCF =prompts/BCF_INTEROP.md(issues + viewpoints). DB = the served building SQLite (*_extracted.db) as-is. ERP = the §B push. - Two sections, one chooser: Model/Data (IFC·BCF·DB·ERP) + Reports (4D·5D·DXF·Excel).
- Multi-select + [All] — tick several across both sections (or All) → one bundle/zip in a single
action. KISS: tiles are checkboxes; "All" ticks every tile; one Export button fires the selected
fns. - "Check ERP" (§A) is NOT in this chooser — it lives on the element info-panel (per-selection read).
- Registry-native: build the chooser once, drive it from the action
fn(route via fn, never a DOM.click()— the=lesson). No new top-level keys (no-clutter rule).
Reuse — the same chooser serves "Share / Drop IFC" later. Share and the planned Drop-IFC flow
ask the same question ("which artifact(s)?"), so the chooser is a shared component, not bolted
into Export alone. Build it format-agnostic (a list of {id, label, fn} selectables + an All) so
Share can mount it with the same or a subset of tiles. Tracked as a follow-on, not this lane's gate.
§D — What this is NOT (scope fences)¶
- Not a live ERP sync / two-way daemon — it is a deterministic push + read against ERP.db.
- Not invented financials — price only from the 5D rates pack, qty only from QTO, BigDecimal only.
- Not a new geometry path — pure data join over
elements_meta.guid. - gbXML/COBie are siblings, not part of this lane (COBie reuses §B's product/asset rows later).
§E — Build order (paper → code, when GO)¶
- Confirm OPEN-1/2/3 against the canonical
build/erp/ad_*.db(one query each). - §A probe + chip (read-only, lowest risk, proves the join).
- §A drawer.
- §B export (find-or-create, idempotent) behind the Export icon.
- §C Export-parent consolidation + BCF child.
Each step lands with its witness
§-line before the next. Whitebox first; Playwright only drives.