GuideMarch 13, 2026

Bill of Materials in Odoo 19:
Multi-Level BoM, Kits & Phantom Assemblies

INTRODUCTION

The Bill of Materials Is the DNA of Your Manufacturing Process

Every manufactured product starts with a Bill of Materials. It defines what components go in, how many, and in what sequence. Get the BoM wrong and everything downstream breaks: production orders consume the wrong quantities, costs are miscalculated, procurement triggers at the wrong time, and finished goods fail quality checks because a sub-assembly was never built.

Odoo 19's Manufacturing module (MRP) supports flat BoMs, multi-level (nested) BoMs, kit BoMs, phantom assemblies, component substitution, routing operations, and BoM versioning. This guide walks through each pattern with real XML and Python examples you can drop into your Odoo 19 instance. We will cover every scenario from a simple single-level BoM to a complex multi-level structure with phantom sub-assemblies and alternative components.

By the end, you will know how to structure BoMs that drive accurate MRP planning, correct cost roll-ups, and reliable production scheduling. Whether you are building furniture, assembling electronics, or packaging food products, the patterns here apply.

01

Creating Your First Bill of Materials in Odoo 19

A basic BoM links a finished product to its components with quantities. In Odoo 19, navigate to Manufacturing → Bills of Materials → New. Select the product, set the BoM type to "Manufacture this product," and add component lines. Here is the equivalent XML data file you can load as module data:

XML — data/bom_data.xml
<odoo>
  <data noupdate="0">

    <!-- Finished product: Wooden Desk -->
    <record id="product_wooden_desk" model="product.product">
      <field name="name">Wooden Desk</field>
      <field name="type">consu</field>
      <field name="standard_price">0</field>
    </record>

    <!-- Components -->
    <record id="product_oak_plank" model="product.product">
      <field name="name">Oak Plank (120x60cm)</field>
      <field name="type">product</field>
      <field name="standard_price">45.00</field>
    </record>

    <record id="product_steel_leg" model="product.product">
      <field name="name">Steel Leg (72cm)</field>
      <field name="type">product</field>
      <field name="standard_price">12.50</field>
    </record>

    <record id="product_wood_screw" model="product.product">
      <field name="name">Wood Screw M6x30</field>
      <field name="type">product</field>
      <field name="standard_price">0.15</field>
    </record>

    <!-- Bill of Materials -->
    <record id="bom_wooden_desk" model="mrp.bom">
      <field name="product_tmpl_id" ref="product_wooden_desk" />
      <field name="product_qty">1</field>
      <field name="type">normal</field>
    </record>

    <!-- BoM Lines -->
    <record id="bom_line_oak_plank" model="mrp.bom.line">
      <field name="bom_id" ref="bom_wooden_desk" />
      <field name="product_id" ref="product_oak_plank" />
      <field name="product_qty">1</field>
    </record>

    <record id="bom_line_steel_leg" model="mrp.bom.line">
      <field name="bom_id" ref="bom_wooden_desk" />
      <field name="product_id" ref="product_steel_leg" />
      <field name="product_qty">4</field>
    </record>

    <record id="bom_line_wood_screw" model="mrp.bom.line">
      <field name="bom_id" ref="bom_wooden_desk" />
      <field name="product_id" ref="product_wood_screw" />
      <field name="product_qty">16</field>
    </record>

  </data>
</odoo>
product_tmpl_id vs product_id

The mrp.bom record uses product_tmpl_id (the product template), while mrp.bom.line uses product_id (the specific product variant). If your finished good has variants (e.g., desk in walnut vs oak), you can optionally set product_id on the BoM itself to create variant-specific BoMs.

Creating a BoM Programmatically via Python

When migrating from another ERP or importing BoMs from a spreadsheet, creating BoMs via code is more practical. Here is a Python method you can use in a custom module or server action:

Python — models/bom_import.py
from odoo import models, api

class BomImport(models.TransientModel):
    _name = 'bom.import.wizard'
    _description = 'BoM Import Wizard'

    def action_create_sample_bom(self):
        """Create a BoM for Wooden Desk with components."""
        Product = self.env['product.product']
        Bom = self.env['mrp.bom']

        # Find or create the finished product
        desk = Product.search(
            [('name', '=', 'Wooden Desk')], limit=1
        )
        if not desk:
            desk = Product.create({{
                'name': 'Wooden Desk',
                'type': 'consu',
            }})

        # Create the BoM with inline component lines
        bom = Bom.create({{
            'product_tmpl_id': desk.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'normal',
            'bom_line_ids': [
                (0, 0, {{
                    'product_id': self._get_component(
                        'Oak Plank (120x60cm)', 45.00
                    ).id,
                    'product_qty': 1.0,
                }}),
                (0, 0, {{
                    'product_id': self._get_component(
                        'Steel Leg (72cm)', 12.50
                    ).id,
                    'product_qty': 4.0,
                }}),
                (0, 0, {{
                    'product_id': self._get_component(
                        'Wood Screw M6x30', 0.15
                    ).id,
                    'product_qty': 16.0,
                }}),
            ],
        }})
        return bom

    def _get_component(self, name, cost):
        """Find or create a storable component product."""
        product = self.env['product.product'].search(
            [('name', '=', name)], limit=1
        )
        if not product:
            product = self.env['product.product'].create({{
                'name': name,
                'type': 'product',
                'standard_price': cost,
            }})
        return product
02

Multi-Level BoM Structures: Sub-Assemblies and Nested Bills of Materials

A multi-level BoM is a BoM whose components are themselves manufactured products with their own BoMs. Odoo resolves these recursively: when you confirm a manufacturing order for the top-level product, MRP automatically generates manufacturing orders (or purchase orders) for every sub-assembly down the chain.

Consider a Standing Desk that requires a motorized lift mechanism (itself assembled from a motor, brackets, and a control unit) plus a desktop surface. The structure looks like this:

LevelProductQtySource
L0Standing Desk (finished)1Manufacture
L1 Motorized Lift Assembly1Manufacture (sub-BoM)
L2  DC Motor 24V2Purchase
L2  Steel Mounting Bracket4Purchase
L2  Control Unit PCB1Purchase
L1 Bamboo Desktop Surface1Purchase
L1 Cable Management Tray1Purchase
XML — data/multi_level_bom.xml
<odoo>
  <data noupdate="0">

    <!-- ── Level 2: Sub-Assembly BoM ── -->
    <record id="bom_lift_assembly" model="mrp.bom">
      <field name="product_tmpl_id" ref="product_lift_assembly" />
      <field name="product_qty">1</field>
      <field name="type">normal</field>
    </record>

    <record id="bom_line_dc_motor" model="mrp.bom.line">
      <field name="bom_id" ref="bom_lift_assembly" />
      <field name="product_id" ref="product_dc_motor" />
      <field name="product_qty">2</field>
    </record>

    <record id="bom_line_bracket" model="mrp.bom.line">
      <field name="bom_id" ref="bom_lift_assembly" />
      <field name="product_id" ref="product_steel_bracket" />
      <field name="product_qty">4</field>
    </record>

    <record id="bom_line_control_pcb" model="mrp.bom.line">
      <field name="bom_id" ref="bom_lift_assembly" />
      <field name="product_id" ref="product_control_pcb" />
      <field name="product_qty">1</field>
    </record>

    <!-- ── Level 0: Top-Level BoM ── -->
    <record id="bom_standing_desk" model="mrp.bom">
      <field name="product_tmpl_id" ref="product_standing_desk" />
      <field name="product_qty">1</field>
      <field name="type">normal</field>
    </record>

    <!-- This component has its own BoM → triggers sub-MO -->
    <record id="bom_line_lift" model="mrp.bom.line">
      <field name="bom_id" ref="bom_standing_desk" />
      <field name="product_id" ref="product_lift_assembly" />
      <field name="product_qty">1</field>
    </record>

    <record id="bom_line_desktop_surface" model="mrp.bom.line">
      <field name="bom_id" ref="bom_standing_desk" />
      <field name="product_id" ref="product_bamboo_desktop" />
      <field name="product_qty">1</field>
    </record>

    <record id="bom_line_cable_tray" model="mrp.bom.line">
      <field name="bom_id" ref="bom_standing_desk" />
      <field name="product_id" ref="product_cable_tray" />
      <field name="product_qty">1</field>
    </record>

  </data>
</odoo>
How MRP resolves nested BoMs

When you confirm a manufacturing order for the Standing Desk, Odoo's MRP scheduler checks each component line. If a component has its own BoM of type "normal," MRP generates a child manufacturing order for that sub-assembly. The parent MO waits until all child MOs are complete. This cascade works to any depth: you can nest 5, 10, or 20 levels deep. The MRP scheduler resolves them all.

Exploding a Multi-Level BoM in Python

Odoo provides the explode() method on mrp.bom to flatten a multi-level BoM into all its raw material components. This is useful for cost computation, procurement planning, and custom reports:

Python — Exploding a nested BoM
def get_raw_materials(self, product, qty=1.0):
    """Flatten a multi-level BoM to raw materials."""
    bom = self.env['mrp.bom']._bom_find(product)[product]
    if not bom:
        return []

    # explode() returns (bom_lines, line_data)
    # Each entry includes the effective qty at leaf level
    _bom_lines, line_data = bom.explode(
        product, qty
    )

    raw_materials = []
    for bom_line, data in line_data:
        # data['qty'] is the total quantity needed
        raw_materials.append({{
            'product': bom_line.product_id.name,
            'quantity': data['qty'],
            'uom': bom_line.product_uom_id.name,
            'cost': (
                bom_line.product_id.standard_price
                * data['qty']
            ),
        }})

    return raw_materials
03

Kit BoMs and Phantom Assemblies: Ship Components Without Manufacturing

Not every BoM triggers a manufacturing order. Kit BoMs (type = phantom) are Odoo's way of saying "this product does not physically exist in stock; when someone orders it, deliver its components instead." This is the mechanism behind product bundles, starter kits, gift sets, and any scenario where you sell a group of items as one SKU but ship them individually.

Kit BoM vs Normal BoM

BehaviorNormal BoM (Manufacture)Kit / Phantom BoM
Creates Manufacturing Order?YesNo
Consumes components from stock?Yes (during MO)Yes (during delivery)
Produces finished good in stock?YesNo (phantom product has zero on-hand)
Delivery order lines1 line: the finished productN lines: one per component
Inventory valuationOn the finished productOn each component individually
XML — data/kit_bom.xml
<odoo>
  <data noupdate="0">

    <!-- Kit product: Home Office Starter Kit -->
    <record id="product_home_office_kit" model="product.product">
      <field name="name">Home Office Starter Kit</field>
      <field name="type">consu</field>
      <field name="list_price">299.00</field>
    </record>

    <!-- Kit BoM (type=phantom) -->
    <record id="bom_home_office_kit" model="mrp.bom">
      <field name="product_tmpl_id" ref="product_home_office_kit" />
      <field name="product_qty">1</field>
      <field name="type">phantom</field>
    </record>

    <record id="bom_line_kit_desk" model="mrp.bom.line">
      <field name="bom_id" ref="bom_home_office_kit" />
      <field name="product_id" ref="product_wooden_desk" />
      <field name="product_qty">1</field>
    </record>

    <record id="bom_line_kit_chair" model="mrp.bom.line">
      <field name="bom_id" ref="bom_home_office_kit" />
      <field name="product_id" ref="product_ergonomic_chair" />
      <field name="product_qty">1</field>
    </record>

    <record id="bom_line_kit_monitor_arm" model="mrp.bom.line">
      <field name="bom_id" ref="bom_home_office_kit" />
      <field name="product_id" ref="product_monitor_arm" />
      <field name="product_qty">1</field>
    </record>

    <record id="bom_line_kit_cable_organizer" model="mrp.bom.line">
      <field name="bom_id" ref="bom_home_office_kit" />
      <field name="product_id" ref="product_cable_organizer" />
      <field name="product_qty">2</field>
    </record>

  </data>
</odoo>

Phantom Assemblies Inside Multi-Level BoMs

Phantom assemblies are especially powerful when used inside a normal manufacturing BoM. Suppose your Standing Desk has a "Hardware Pack" (screws, bolts, washers) that you want to manage as a logical group but never actually assemble into a separate stock item. Make the Hardware Pack a phantom BoM: when the Standing Desk MO is created, Odoo skips the phantom and pulls its components directly into the parent MO's raw material list.

Python — Checking if a BoM is phantom
def is_kit_product(self, product):
    """Check whether a product is sold as a kit."""
    bom = self.env['mrp.bom']._bom_find(product)[product]
    if bom and bom.type == 'phantom':
        return True
    return False

def get_kit_components(self, product, sale_qty=1.0):
    """Return the delivery lines for a kit product."""
    bom = self.env['mrp.bom']._bom_find(product)[product]
    if not bom or bom.type != 'phantom':
        return [(product, sale_qty)]

    components = []
    for line in bom.bom_line_ids:
        line_qty = line.product_qty * sale_qty
        # Recurse: a kit component can itself be a kit
        components.extend(
            self.get_kit_components(line.product_id, line_qty)
        )
    return components
When to use Phantom vs Normal

Use phantom/kit when you never stock the parent product: bundles, variety packs, starter kits, or logical component groups inside a larger BoM. Use normal when the sub-assembly is a real thing you manufacture, stock, and possibly sell separately. The decision affects inventory valuation, procurement triggers, and delivery order structure.

04

BoM Versioning: Managing Engineering Changes Without Breaking Production

Products evolve. You switch from Supplier A's motor to Supplier B's, replace a plastic bracket with aluminum, or change the screw count from 16 to 12. Odoo 19 handles this through BoM versioning via date effectivity and ECO (Engineering Change Orders) when the PLM module is installed.

Date-Based Effectivity

Every BoM line in Odoo 19 has optional date_start and date_stop fields. This lets you phase components in and out without deleting history:

XML — data/bom_versioning.xml
<odoo>
  <data noupdate="0">

    <!-- Old component: active until March 31 -->
    <record id="bom_line_old_motor" model="mrp.bom.line">
      <field name="bom_id" ref="bom_lift_assembly" />
      <field name="product_id" ref="product_dc_motor_v1" />
      <field name="product_qty">2</field>
      <field name="date_stop">2026-03-31</field>
    </record>

    <!-- New component: active from April 1 -->
    <record id="bom_line_new_motor" model="mrp.bom.line">
      <field name="bom_id" ref="bom_lift_assembly" />
      <field name="product_id" ref="product_dc_motor_v2" />
      <field name="product_qty">2</field>
      <field name="date_start">2026-04-01</field>
    </record>

  </data>
</odoo>

Engineering Change Orders with PLM

For formal version control, install the PLM (Product Lifecycle Management) module. ECOs create a new revision of the BoM, track the change reason, require approval, and only swap the active BoM version when the ECO is validated. This gives you a full audit trail:

Python — Creating an ECO programmatically
def create_engineering_change(self, bom, description):
    """Create an ECO to revise a Bill of Materials."""
    Eco = self.env['mrp.eco']

    eco = Eco.create({{
        'name': f'ECO - {{bom.product_tmpl_id.name}}',
        'bom_id': bom.id,
        'product_tmpl_id': bom.product_tmpl_id.id,
        'type_id': self.env.ref('mrp_plm.ecotype_bom').id,
        'note': description,
        'stage_id': self.env.ref(
            'mrp_plm.eco_stage_new'
        ).id,
    }})

    # The ECO creates a draft copy of the BoM
    # Engineers modify the draft, then validate
    # Validation swaps the active BoM to the new version
    return eco
Version control without PLM

If you do not have the PLM module, you can still version BoMs manually: duplicate the BoM, archive the old one, and modify the new one. The downside is no approval workflow and no audit trail. For regulated industries (medical devices, food, aerospace), PLM is effectively mandatory.

05

Routing Operations on BoM and Component Substitution

A BoM tells Odoo what goes into a product. Routing operations tell Odoo how to build it: the sequence of work center operations, expected durations, and which components are consumed at which step.

Defining Operations on a BoM

In Odoo 19, routing operations are defined directly on the BoM (the separate mrp.routing model was removed in Odoo 15). Each operation specifies a work center, a duration, and optionally which BoM lines are consumed at that step:

XML — data/bom_routing.xml
<odoo>
  <data noupdate="0">

    <!-- Work Centers -->
    <record id="wc_cutting" model="mrp.workcenter">
      <field name="name">Wood Cutting Station</field>
      <field name="costs_hour">35.00</field>
      <field name="capacity">1</field>
    </record>

    <record id="wc_assembly" model="mrp.workcenter">
      <field name="name">Assembly Line</field>
      <field name="costs_hour">28.00</field>
      <field name="capacity">3</field>
    </record>

    <record id="wc_finishing" model="mrp.workcenter">
      <field name="name">Finishing &amp; QC</field>
      <field name="costs_hour">30.00</field>
      <field name="capacity">2</field>
    </record>

    <!-- Operations on the Wooden Desk BoM -->
    <record id="op_cut_plank" model="mrp.routing.workcenter">
      <field name="bom_id" ref="bom_wooden_desk" />
      <field name="name">Cut oak plank to size</field>
      <field name="workcenter_id" ref="wc_cutting" />
      <field name="time_cycle_manual">15</field>
      <field name="sequence">10</field>
    </record>

    <record id="op_assemble_desk" model="mrp.routing.workcenter">
      <field name="bom_id" ref="bom_wooden_desk" />
      <field name="name">Attach legs and hardware</field>
      <field name="workcenter_id" ref="wc_assembly" />
      <field name="time_cycle_manual">25</field>
      <field name="sequence">20</field>
    </record>

    <record id="op_finish_desk" model="mrp.routing.workcenter">
      <field name="bom_id" ref="bom_wooden_desk" />
      <field name="name">Sand, stain, and quality check</field>
      <field name="workcenter_id" ref="wc_finishing" />
      <field name="time_cycle_manual">20</field>
      <field name="sequence">30</field>
    </record>

  </data>
</odoo>

Component Substitution (Allowed Alternatives)

Odoo 19 supports component substitution on BoM lines. When a primary component is out of stock, the shop floor operator can swap it for a pre-approved alternative without editing the BoM. This is configured via the "Allowed alternatives" field on each BoM line:

Python — Setting up component alternatives
def configure_alternatives(self):
    """Allow birch plank as substitute for oak plank."""
    oak_line = self.env.ref(
        'my_module.bom_line_oak_plank'
    )
    birch = self.env.ref('my_module.product_birch_plank')
    maple = self.env.ref('my_module.product_maple_plank')

    # Set allowed alternatives on the BoM line
    oak_line.write({{
        'allowed_operation_ids': [
            (4, birch.id),
            (4, maple.id),
        ],
    }})

    # During manufacturing, if oak is unavailable,
    # the operator sees birch and maple as options
    # in the component substitution wizard
Substitution does not change cost automatically

When an operator substitutes a component, the manufacturing order uses the substitute product's standard cost for that MO. This means your actual production cost reflects reality. However, the BoM cost report still shows the primary component's cost. Run the "BoM Cost" report and the "Manufacturing Analysis" report side by side to catch cost variances from frequent substitutions.

06

BoM Cost Computation: Rolling Up Material and Operation Costs

Odoo 19 computes BoM cost by summing component costs and operation costs recursively through every level of the BoM tree. The formula at each level is:

Text — BoM cost formula
BoM Cost = SUM(component_qty * component_standard_price)
         + SUM(operation_duration_hours * workcenter_cost_per_hour)

For multi-level BoMs, component_standard_price of a
manufactured sub-assembly = its own BoM Cost (recursive).

Computing BoM Cost Programmatically

The built-in BoM cost report is accessible via Manufacturing → Reporting → BoM Overview. For custom integrations or batch cost updates, use the _get_report_data() method:

Python — Custom BoM cost computation
def compute_bom_cost(self, bom):
    """Compute total BoM cost including operations."""
    material_cost = 0.0
    operation_cost = 0.0

    for line in bom.bom_line_ids:
        child_bom = self.env['mrp.bom']._bom_find(
            line.product_id
        )[line.product_id]

        if child_bom and child_bom.type == 'normal':
            # Recursive: sub-assembly cost
            unit_cost = self.compute_bom_cost(child_bom)
        else:
            unit_cost = line.product_id.standard_price

        material_cost += line.product_qty * unit_cost

    # Add operation costs
    for op in bom.operation_ids:
        duration_hours = op.time_cycle_manual / 60.0
        wc_cost = op.workcenter_id.costs_hour
        operation_cost += duration_hours * wc_cost

    total = material_cost + operation_cost
    return total

def update_finished_product_cost(self, bom):
    """Set the finished product's standard_price
    to the computed BoM cost."""
    cost = self.compute_bom_cost(bom)
    bom.product_tmpl_id.standard_price = cost
    return cost

Example Cost Breakdown: Wooden Desk

Component / OperationQtyUnit CostLine Total
Oak Plank (120x60cm)1$45.00$45.00
Steel Leg (72cm)4$12.50$50.00
Wood Screw M6x3016$0.15$2.40
Op: Cut oak plank (15 min)-$35/hr$8.75
Op: Assembly (25 min)-$28/hr$11.67
Op: Finishing & QC (20 min)-$30/hr$10.00
Total BoM Cost$127.82
Standard vs Actual cost

The BoM cost report uses standard_price from each product. If you use FIFO or AVCO valuation, the actual cost of a manufacturing order may differ because components are consumed at their real purchase cost, not the standard price. Compare the BoM Overview report with the Manufacturing Analysis report to track standard-vs-actual variance.

07

5 BoM Configuration Mistakes That Silently Break MRP Planning

1

Circular BoM References

Product A has a BoM containing Product B. Product B has a BoM containing Product A. Odoo will raise a ValidationError if the cycle is direct, but indirect cycles through 3+ levels can slip through in some edge cases. MRP scheduling enters an infinite loop and the scheduler cron times out silently. Always validate new BoMs with the BoM Overview report before running the scheduler.

2

Kit BoM on a Storable Product

If you set a phantom/kit BoM on a product of type "Storable", Odoo will explode it into components during delivery, but you might also have that product sitting in stock from a previous purchase. The result: stock moves that make no sense, inventory valuation that double-counts, and warehouse staff confused by pick lists that show components they never stocked. Kit products should almost always be type "Consumable" or "Service" — never "Storable."

3

Forgetting UoM Conversions on BoM Lines

You stock wire in meters but your BoM specifies 50 centimeters. If the UoM categories do not match or the conversion factor is missing, Odoo will either block the MO with a UserError or — worse — consume 50 meters instead of 50 centimeters. Always double-check that the BoM line UoM belongs to the same UoM category as the product's purchase/stock UoM.

4

Multiple Active BoMs for the Same Product

Odoo allows multiple active BoMs for one product template. The MRP scheduler picks the first one by sequence number. If you duplicated a BoM for testing and forgot to archive it, production may use the wrong BoM silently. Use the "BoM" smart button on the product form to check how many active BoMs exist. Only one should be active for a given product + variant combination unless you intentionally use sequence-based selection.

5

Phantom Sub-Assemblies Not Propagating to MO

You add a phantom sub-assembly to a normal BoM but the manufacturing order still shows the phantom product as a single component line instead of its exploded parts. This happens when the phantom product does not have a BoM assigned. The phantom type lives on the BoM, not the product. If the BoM is archived, deleted, or points to the wrong product template, Odoo treats the component as a regular purchased item.

BUSINESS ROI

What Properly Structured BoMs Save Your Manufacturing Business

A clean BoM structure is not an IT project — it is an operational efficiency multiplier. Here is the measurable impact:

15-25%Lower Material Waste

Accurate BoM quantities mean procurement orders match actual consumption. No more over-ordering "just in case" or emergency purchases when a component runs short mid-production.

3-5xFaster Cost Quoting

With recursive BoM cost roll-ups, your sales team can quote custom configurations in minutes instead of waiting days for engineering to calculate material and labor costs manually.

40%Less Production Downtime

Multi-level BoMs with correct lead times let MRP schedule sub-assemblies ahead of the parent MO. Components arrive at the right workstation at the right time instead of halting the line.

Beyond direct savings, clean BoMs unlock traceability for regulated industries. When a component has a quality recall, you can trace every finished product that used it through the BoM explosion chain. This turns a potential weeks-long recall investigation into a single SQL query or a click on the "Where Used" report in Odoo.

Kit BoMs also have a direct revenue impact. By bundling slow-moving inventory into attractive kits, businesses have reported 12-18% increases in average order value. The kit appears as a single line on the sales order, but delivery picks the individual components — no re-packaging labor required.

SEO NOTES

Optimization Metadata

Meta Desc

Complete guide to Bill of Materials in Odoo 19. Multi-level BoM, kit/phantom assemblies, component substitution, routing operations, BoM versioning, and cost computation with code examples.

H2 Keywords

1. "Creating Your First Bill of Materials in Odoo 19"
2. "Multi-Level BoM Structures: Sub-Assemblies and Nested Bills of Materials"
3. "Kit BoMs and Phantom Assemblies: Ship Components Without Manufacturing"
4. "BoM Cost Computation: Rolling Up Material and Operation Costs"
5. "5 BoM Configuration Mistakes That Silently Break MRP Planning"

Your BoM Is the Foundation — Build It Right

Every manufacturing process in Odoo flows from the Bill of Materials. MRP scheduling, procurement, cost computation, production planning, inventory valuation, and traceability all depend on BoM accuracy. A flat BoM is fine for simple products, but as your product portfolio grows, you need multi-level structures, phantom assemblies for logical groupings, kit BoMs for bundles, and routing operations to capture labor costs.

Start with the basics: one product, one BoM, correct quantities and UoMs. Then layer on complexity as needed. Use the BoM Overview report to validate every new BoM before it enters production. Version your changes through date effectivity or PLM. And always keep the BoM as the single source of truth — if the shop floor deviates from the BoM, update the BoM, do not just adjust the MO.

Need help structuring BoMs for a complex manufacturing process? We have configured BoMs for electronics assembly, food processing, furniture manufacturing, and industrial equipment. We can audit your current BoM structure, fix cost roll-ups, and set up multi-level BoMs that drive accurate MRP planning from day one.

Book a Free Manufacturing Consultation