DEV Community

Cover image for Mastering Odoo Manufacturing: Architecture, Workflows & Production Scaling
MOR Insights
MOR Insights

Posted on

Mastering Odoo Manufacturing: Architecture, Workflows & Production Scaling

Introduction

If you've ever sat in a meeting room with a plant manager running three production lines, 200 BOMs, and tens of thousands of work orders a month, you know the real problem isn't "do we have software" — it's whether data stays synchronized in real time. Distributed shop floors, raw material inventory drift caused by mismatches between accounting figures and what's actually on the floor, scheduling still running off spreadsheets — these are the classic cracks that make fragmented MES/ERP setups fail.

This is exactly where odoo for manufacturing becomes a serious contender — not just because it's free and open source, but because its MRP (Manufacturing Resource Planning) engine is built on a pull-based, event-driven model, tightly integrated with Inventory, Purchase, Quality, and Accounting inside a single database schema — instead of forcing you to build middleware to sync disconnected systems like SAP PP plus a separate MES plus a separate WMS.

This isn't a marketing post. We're going straight into the data structures, the ORM execution flow, and the things you need to watch out for when scaling the odoo manufacturing module for a real production environment.


Core Data Structures & Architecture of the MRP Engine

1. Bills of Materials (BOM) — Multi-level, Kits & Variants

In mrp.bom, each BOM isn't just a static list of materials — it's a recursive tree structure via the bom_line_ids field pointing to mrp.bom.line. A few technical points worth understanding:

  • Multi-level BOM: A child product.template inside a parent BOM can own its own BOM (type = 'normal'). When Odoo "explodes" a BOM (the _get_bom + explode() logic), it recurses down each level to produce a flattened component list, used for both costing and inventory availability checks.
  • Kit BOM (type = 'phantom'): This is where a lot of newer devs get tripped up. A kit doesn't spawn its own Manufacturing Order — when sold or delivered, Odoo "explodes" the phantom BOM directly at the stock move level, splitting a parent move into multiple child moves in the correct component ratio. This directly affects FIFO/valuation logic if you're customizing cost calculation.
  • BOM Variants: Through bom_line_ids.bom_product_template_attribute_value_ids, a single BOM can apply conditionally per variant (product attribute value) — avoiding the need to create N BOMs for N product variants, which matters a lot once SKU count grows.

2. Routing, Work Centers & Operations

mrp.routing.workcenter defines operations tied to a mrp.workcenter. Each Work Center stores:

  • capacity (how many units can be processed in parallel per cycle)
  • time_efficiency (an efficiency coefficient applied to standard time)
  • time_start / time_stop (setup and teardown time)

The expected duration formula for a Work Order:

duration_expected = (time_cycle × qty / capacity) / (time_efficiency / 100) + time_start + time_stop
Enter fullscreen mode Exit fullscreen mode

OEE (Overall Equipment Effectiveness) in Odoo is calculated from mrp.workcenter.productivity — a log table recording time blocks where a Work Center is in a performance state (running) versus downtime (stopped, tagged with a reason via mrp.workcenter.productivity.loss). This is the table you'll touch most when building custom machine-performance reports or sensor integrations.

3. Manufacturing Orders (MO) & Work Orders (WO)

mrp.production (MO) is the top-level entity representing a production order for a defined quantity of a product. Each MO:

  • Generates move_raw_ids (raw material consumption moves) and move_finished_ids (finished goods receipt moves)
  • If a routing exists, the MO automatically spawns a chain of mrp.workorder (WO) records matching each operation, each with an independent state (pending, ready, progress, done) — allowing progress to be tracked at the operation level instead of only at the aggregate order level.

Key architectural point: MO and WO states aren't 1:1 synchronized. An MO can sit in confirmed while its first WO is already in_progress — this is where most custom logic bugs originate when developers override the state machine incorrectly.


End-to-End Workflow in Odoo Production Management

From Sales Order to Manufacturing Order

When a Sale Order is confirmed, if the product's route is configured with Make to Order (MTO) combined with the Manufacture route, Odoo generates a stock.rule procurement group. The mechanism:

  1. SO confirmed → a procurement.group is generated → _run_manufacture() fires inside stock.rule
  2. The rule creates an mrp.production with origin pointing back to the SO and procure_method = 'make_to_order'
  3. If lower-level components are also flagged MTO, the system recursively generates procurements down multiple BOM levels — this is the chain of MOs mechanism for multi-tier assembled products.

In parallel, Reordering Rules (stock.warehouse.orderpoint) work under a different pull mechanism: the Reordering Rule cron job periodically scans stock levels (qty_available vs. product_min_qty) and auto-generates procurement once the minimum threshold is hit — suited to Make to Stock (MTS) rather than reacting order-by-order.

Material Availability Checks & Automatic Scheduling

Before a Work Order transitions to ready, Odoo checks material availability through _check_company() and, more importantly, the reservation mechanism on stock.move.line. If Manufacturing Readiness component-availability checking is enabled, the MO will surface a shortage warning right in the UI, comparing the required product_qty against the actual reserved_availability at the source location (typically the "Production" virtual location).

Integration with Inventory/Barcode & Quality Control

  • Barcode: Each Work Order can be configured with Tablet View, letting operators scan a barcode to confirm component lot/serial numbers right at the station — writing directly to lot_id on stock.move.line, ensuring full traceability.
  • Quality Control Points (quality.point): Attached to a specific Operation or Work Center, triggering an automatic Quality Check (quality.check) when the Work Order reaches that step. On failure (fail), the system can block the next WO via a Quality Alert — a powerful integration point if you need to enforce compliance in food or pharmaceutical production.

Technical Customization & Integration Opportunities

This section is for developers looking to extend the odoo manufacturing module beyond its default configuration — for example, integrating IoT sensors to auto-log downtime, or enforcing business conditions before an MO can be confirmed.

Example: Material Availability Check & Auto-Logging Before Confirming an MO

from odoo import models, fields, api
from odoo.exceptions import UserError


class MrpProductionInherit(models.Model):
    _inherit = 'mrp.production'

    iot_sensor_ref = fields.Char(
        string="IoT Sensor Reference",
        help="Identifier of the sensor attached to the Work Center executing this order"
    )

    def action_confirm(self):
        """Override: enforce material checks + logging before allowing MO confirmation."""
        for production in self:
            production._check_critical_component_availability()
        res = super().action_confirm()
        for production in self:
            production._log_confirmation_event()
        return res

    def _check_critical_component_availability(self):
        """Block confirmation if a 'critical' component (custom flag on the BOM line)
        doesn't have enough available stock at the source location."""
        self.ensure_one()
        for move in self.move_raw_ids:
            bom_line = move.bom_line_id
            if bom_line and bom_line.product_id.critical_component:
                available_qty = move.product_id.with_context(
                    location=move.location_id.id
                ).qty_available
                if available_qty < move.product_qty:
                    raise UserError(
                        f"Cannot confirm MO {self.name}: "
                        f"critical component '{move.product_id.display_name}' "
                        f"only has {available_qty} available, {move.product_qty} required."
                    )

    def _log_confirmation_event(self):
        """Log the confirmation event — e.g. push to an external MES/IoT system via a queue job."""
        self.ensure_one()
        self.env['mrp.production.confirmation.log'].sudo().create({
            'production_id': self.id,
            'sensor_ref': self.iot_sensor_ref,
            'confirmed_date': fields.Datetime.now(),
            'user_id': self.env.user.id,
        })
Enter fullscreen mode Exit fullscreen mode

A few technical notes when going down this path:

  • Don't override the WO state machine directly (the state field on mrp.workorder) without calling super() — several internal automated actions (updating duration, mrp.workcenter.productivity) depend on the standard transition chain firing correctly.
  • For IoT sensor integration at the Work Center level to calculate real machine-hour cost, the recommended approach is using queue_job (from the OCA queue_job module) to receive webhooks from the IoT gateway, avoiding blocking synchronous requests inside the main ORM transaction — especially important at high logging frequency (every few seconds).
  • Real machine-hour cost should hook into mrp.workcenter.productivity rather than a new field on mrp.workorder, since that table is purpose-built for time tracking and already has cost calculation logic in _cal_cost().

Performance & Scalability Considerations

As Manufacturing Order volume scales to thousands per day, a few bottlenecks commonly show up in odoo manufacturing:

1. Stock Move & Reservation Locking

stock.move.line reservation uses row-level locking (SELECT ... FOR UPDATE) when multiple MOs compete for materials from the same location. If you're batch-confirming a large number of MOs at once (e.g. via script or automated action), consider:

  • Splitting batches and processing sequentially rather than confirming concurrently against the same source location, to avoid deadlocks.
  • Splitting locations per production line (multi-location Production) instead of using a single virtual location for the entire plant — significantly reducing lock contention.

2. Cron Jobs & Automated Actions

The Reordering Rule cron and the MRP scheduler cron (Procurement: run scheduler) can become a bottleneck when the number of orderpoints and BOMs grows large, since by nature they're a full-table scan per product/location on every run. Recommendations:

  • Reduce the frequency of the global scheduler (_run_scheduler) to match actual production cadence instead of leaving it at a default aggressive interval, and favor event-driven triggers (immediate procurement when an SO is confirmed) over relying entirely on periodic cron scans.
  • Move custom Automated Actions (e.g. downtime logging, quality checks) to asynchronous processing via queue_job, avoiding heavy logic running synchronously inside write()/create() on mrp.workorder, since it's a model with very high write frequency on the shop floor.
  • On PostgreSQL, make sure proper indexes exist on fields frequently used to filter MOs by state and date (state, date_planned_start) — Odoo indexes some of these by default, but any custom field used for frequent filtering needs a manual index.

3. Database Growth from Quality & Productivity Logs

mrp.workcenter.productivity and quality.check are the two fastest-growing tables in the MRP system, since they log at operation-level frequency. You'll need a periodic archiving strategy (not hard deletion — use active = False or time-based partitioning) to avoid degrading long-term OEE reporting query performance.


Conclusion

The core value of implementing manufacturing odoo isn't "replacing spreadsheets with software" — it's having a unified data model, from BOM, routing, and work orders through to inventory and quality, where every customization (whether a new business rule or an IoT integration) is built on the same single source of truth, instead of cross-syncing between disconnected systems.

For technical teams, the real power lies in the ability to inherit models, hook into the standard state machine, and leverage the built-in procurement/reservation mechanisms — rather than rewriting MRP logic from scratch.

Open question for the community: What's the most complex MRP customization challenge you've run into implementing Odoo Manufacturing — dynamic multi-tier BOMs configured per customer, real-time PLC/SCADA integration, or handling concurrency when multiple lines compete for the same raw material source? Share your approach in the comments!

Top comments (0)