Learn / Costing
Multi-level BOM cost rollup for layered F&B recipes
An F&B recipe is never one level deep. Concentrate goes into a bulk mix; bulk mix gets packaged into a finished good. Phantom items model conceptual nodes that don't get stocked. Here's how multi-level rollup actually mechanically computes the standard cost — and why your CFO depends on it being right.
A flat BOM is a fiction that some ERPs sell because their cost engine can’t do anything more complicated. Real F&B recipes are nested. Concentrate goes into a bulk mix. Bulk mix gets bottled. Bottles get packed into cases. Each level is its own recipe relationship.
This article walks through how multi-level rollup mechanically computes the finished-good standard cost from primary inputs, including the phantom-item construct that every real F&B recipe uses.
The shape of a real F&B BOM
Take Oasis Mango Juice 1L (FG-SA-MNG-1L). Three levels:
Level 0 (FG): Oasis Mango Juice 1L. Components:
- 1 PET bottle 1L preform
- 1 PCO 28mm cap
- 1 shrink label
- 1L of bulk mango juice mix (WIP)
- 1/12 of a carton case (allocation)
Level 1 (WIP): Bulk Mango Juice Mix (WIP-SA-MNG-MIX). Components per litre:
- 130g mango concentrate
- 95g sugar
- 0.5g citric acid
- 0.05g vitamin C
- ~0.85L treated water
- (Possibly a phantom node — “fruit acid blend” — that mixes citric acid + vitamin C + minor flavour notes inline)
Level 2 (raw materials): Imported mango concentrate, local sugar, citric acid, vitamin C, treated RO water.
Three levels. Standard-cost rollup walks the tree from top to bottom, summing the component costs at each level.
How the rollup mechanically computes
The algorithm in standard-cost-rollup.service.ts (FULL mode) is:
function rollupCost(item):
if item.kind == 'RAW' or 'PACKAGING':
return item.standardCost // leaf — base case
total = 0
for component in item.bom.components:
if component.isPhantom:
// phantom — explode through, don't add the phantom's own cost
total += rollupCost(component) × component.quantityPerUnit
else:
total += rollupCost(component) × component.quantityPerUnit
for routing in item.routing.operations:
total += routing.standardLabourHours × routing.resource.standardLabourRate
total += routing.standardOverheadAbsorption
item.standardCost = total
return total
Three observations:
- Recursion. Each level calls back into the function. A 3-level BOM means 3 levels of recursive calls. AION’s implementation preloads child BOMs in batch to avoid the N+1 query problem (see
preloadChildBoms()in the BOM explosion service). - Phantom items contribute structure, not cost lines. A phantom node exists in the BOM tree but doesn’t have its own standard cost — its children’s costs roll through to the parent directly. This is critical for recipes that have many conceptual sub-mixes.
- Routing labour + overhead are added at each manufactured level. WIP and FG both have routings. Raw materials don’t. So labour and overhead absorbed in the bulk-mix step add to WIP’s cost; labour absorbed in bottling adds to the FG’s cost on top of WIP cost.
Worked example — Oasis Mango Juice 1L
Using realistic May 2026 averages from the Oasis Fresh demo:
Level 2 — raw materials (per kg or per litre):
| Item | Standard cost |
|---|---|
| Mango concentrate | SAR 42.00 / kg |
| Sugar (refined) | SAR 5.00 / kg |
| Citric acid | SAR 14.00 / kg |
| Vitamin C | SAR 90.00 / kg |
| RO water | SAR 0.60 / litre |
Level 1 — Bulk Mango Juice Mix (per litre):
| Component | Qty per litre | Unit cost | Component cost |
|---|---|---|---|
| Mango concentrate | 0.130 kg | 42.00 | 5.46 |
| Sugar | 0.095 kg | 5.00 | 0.48 |
| Citric acid | 0.0005 kg | 14.00 | 0.01 |
| Vitamin C | 0.00005 kg | 90.00 | 0.00 |
| RO water | 0.85 L | 0.60 | 0.51 |
| Routing labour (bulk mix line) | 0.30 | ||
| Routing overhead | 0.20 | ||
| Bulk mix standard cost | 6.96 SAR / L |
Level 0 — Oasis Mango Juice 1L (per bottle):
| Component | Qty | Unit cost | Component cost |
|---|---|---|---|
| Bulk mango mix | 1 L | 6.96 | 6.96 |
| PET bottle 1L | 1 each | 1.40 | 1.40 |
| Cap PCO 28mm | 1 each | 0.18 | 0.18 |
| Shrink label | 1 each | 0.35 | 0.35 |
| Carton allocation | 1/12 | 1.90 | 0.16 |
| Routing labour (bottling line) | 0.40 | ||
| Routing overhead (bottling) | 0.55 | ||
| Pallet allocation | 0.05 | ||
| FG standard cost | 10.05 SAR / bottle |
The Oasis Fresh seeded standard is SAR 10.5, slightly higher because the seed includes a small allowance for shrinkage and QA-reject scrap. Close enough — the algorithm produces the rough number.
The point is that you can audit this calculation. Every line is traceable to a primary input. If concentrate price goes up, you can see exactly how much it pushes the FG standard cost up. If you change packaging supplier, you see the immediate impact at the bulk-mix and FG level.
Phantom items in practice
Why use phantom items if they don’t show up in inventory?
Three reasons specific to F&B:
1. Conceptual purity in the recipe. “Fruit acid blend” reads better than “30% citric acid by mass plus 5% vitamin C plus 1% natural flavour.” The phantom item names the concept; the children carry the actual ingredients.
2. Reuse across multiple FGs. A flavour blend that goes into mango, orange, and pomegranate juice can be defined once as a phantom and referenced in all three recipes. Change the blend ratio once, and all three FG rollups update.
3. R&D workflow. New recipe development creates phantom items as placeholders before deciding whether to track them as real inventory. If the R&D outcome is a stocked sub-mix, the phantom converts to a real WIP item.
In AION, an item is marked as phantom on its definition. The rollup service handles them via the explosion logic — the recursive call still walks through, but the phantom’s own cost line is not added.
Batch rollup — why it matters
When the average cost of mango concentrate moves from SAR 9.4/kg to SAR 10.2/kg, every BOM that uses concentrate needs to re-roll. The Oasis Fresh seed has 12 finished goods, 4 WIP items, and several recipes referencing concentrate.
Running the rollup one BOM at a time is slow and error-prone. AION’s batch rollup (batch-cost-rollup.use-case.ts) re-runs the calculation for every APPROVED or FROZEN BOM in the organisation in a single transaction. Triggered automatically when a configured threshold of price movement happens, or manually by the cost accountant.
The atomic transaction is important: either all updated standards write together, or none do. You can’t have FG standard cost reflecting the new concentrate price while WIP standard cost still uses the old one. The rollup transaction enforces consistency.
What you can’t do today (roadmap)
Two things on the costing audit’s “not yet primary feature” list:
Where-used reverse lookup. Today you can explode a BOM downward — given the FG, see all components. The reverse — given concentrate, see all FGs that use it — isn’t a direct query yet. The data is there (BOM components reference back to their parent), but the report doesn’t surface it as a one-click “where used” view. Workaround: search across BOMs. Roadmap: dedicated where-used view.
Cost simulation / what-if pricing. “What would the FG standard cost be if concentrate went to SAR 12/kg?” — not a built-in calculation today. Workaround: temporarily adjust raw material standard cost, run batch rollup, see the result, revert. Roadmap: dedicated simulation that doesn’t touch the live data.
Common mistakes
Forgetting to re-run rollup after price changes. AION can run batch rollup automatically on configured triggers, but it’s easy to disable that and then forget. Standards drift from reality until someone notices. Set the trigger.
Modelling everything as real WIP. If you model every conceptual blend as a stocked WIP item, you create inventory management overhead — somebody has to count it, value it, reconcile it. Most blends should be phantom. The litmus test: is there ever a point where you’d hold this inventory waiting to use? If no, phantom.
Inconsistent BOM revisions. A FG can have multiple BOM versions (DRAFT, APPROVED, FROZEN). Make sure rollup only uses APPROVED or FROZEN — running rollup on a DRAFT BOM that someone forgot to discard pollutes the standards.
What this looks like in the demo
Walk through it as cfo.saudi:
- Navigate to Manufacturing → BOMs → Oasis Mango Juice 1L. See the FG-level recipe.
- Click into Bulk Mango Juice Mix. See the WIP-level recipe, with raw materials.
- Right-click any raw material to see the average cost history.
- Manufacturing → BOMs → Cost rollup. Run batch rollup. Watch every FG standard update.
- General Ledger → Trial Balance. See the inventory line move as the new standards apply at next period close.
The next article in this series — Yield variance — the 8% you’re losing — picks up where BOM rollup ends. Once you have an accurate standard, the question becomes: how often does actual production match it, and where does it drift?
See this in the Oasis Fresh demo
Log into the Oasis Fresh (Saudi) BG as cfo.saudi
Common questions
What is a phantom item in a BOM and why does F&B use it?
A phantom item is a recipe node that exists conceptually but isn't stocked, valued, or counted. Used for sub-mixes that have no physical inventory presence — for example, a 'fruit acid blend' that's combined inline during bulk mix production. Phantom items get exploded through during rollup but don't contribute a cost line of their own; only their children do. F&B uses phantom items because real recipes have many conceptual blending stages that don't justify creating tracked inventory items for each.
How often should we re-run the BOM rollup?
Automatically — every time a component cost changes materially. AION's batch rollup use case re-runs for every APPROVED or FROZEN BOM in the organisation. For an SMB factory with stable suppliers, this might run weekly or monthly. For a factory with volatile FX exposure (Egypt, post-2022), it might run after every major receipt.
Can a BOM reference itself or create a loop?
No — BOMs must be acyclic. AION's BOM explosion service detects loops and rejects them at BOM creation. This matters because a cyclic recipe would explode infinitely during rollup. Phantom items also can't reference themselves through their children.