GuideMarch 13, 2026

MRP Planning in Odoo 19:
Master Production Scheduling & Capacity Analysis

INTRODUCTION

Your Production Floor Is Running Blind Without MRP

We audit manufacturing companies running Odoo where the production manager still maintains a spreadsheet of what to build next week. Sales orders flow into Odoo, inventory levels update in real time, but the actual production schedule lives in Excel — manually cross-referenced against stock levels, supplier lead times, and work center availability. The inevitable result: stockouts on high-demand SKUs, overproduction of slow movers, and work centers that alternate between idle and overwhelmed.

Odoo 19's MRP module closes this gap. It connects sales demand to production scheduling to material procurement to shop floor execution — in a single system. But the module ships with almost nothing configured. Out of the box, you get a blank Master Production Schedule, unconstrained manufacturing orders, and work centers with infinite capacity. Turning this into a real planning system requires deliberate configuration.

This guide walks through the complete MRP planning setup in Odoo 19: configuring the Master Production Schedule, setting up multi-level material requirements planning, defining work center capacities and scheduling rules, and implementing demand-driven MRP buffers. Every code example is production-tested across discrete and process manufacturing clients.

01

Configuring the Master Production Schedule in Odoo 19

The Master Production Schedule (MPS) is the top-level plan that tells your factory what to produce, how much, and when. In Odoo 19, the MPS lives under Manufacturing → Planning → Master Production Schedule. Before it shows anything useful, you need to configure three things: the planning horizon, the products to include, and the demand sources.

Step 1: Enable MPS and Set the Planning Horizon

Navigate to Manufacturing → Configuration → Settings and enable Master Production Schedule. Then set your planning horizon. Most discrete manufacturers use 12 weekly buckets; process manufacturers with longer lead times use 6 monthly buckets.

Python — models/res_config_settings.py
from odoo import models, fields

class ResConfigSettings(models.TransientModel):
    _inherit = 'res.config.settings'

    # Extend MPS settings for custom planning horizons
    mps_planning_weeks = fields.Integer(
        related='company_id.mps_planning_weeks',
        string='MPS Horizon (Weeks)',
        default=12,
        readonly=False,
    )
    mps_min_supply_days = fields.Integer(
        related='company_id.mps_min_supply_days',
        string='Minimum Days of Supply',
        default=7,
        readonly=False,
    )

Step 2: Add Products to the MPS

Not every product belongs in the MPS. Include only finished goods and key subassemblies — the items that drive your production schedule. Raw materials are handled by MRP procurement, not the MPS. A typical manufacturer with 5,000 SKUs has 50-200 products in the MPS.

Python — models/mrp_production_schedule.py
from odoo import models, fields, api

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

    safety_stock_qty = fields.Float(
        string='Safety Stock',
        help='Minimum inventory to maintain at all times.',
    )
    demand_source = fields.Selection(
        selection_add=[('forecast_blend', 'Forecast + Orders Blend')],
        ondelete={{'forecast_blend': 'set default'}},
    )

    @api.depends('forecast_ids', 'product_id')
    def _compute_demand_forecast_blend(self):
        """Blend confirmed SO demand with statistical forecast.
        Use confirmed orders for the first 4 weeks,
        then fall back to forecast for weeks 5-12."""
        for schedule in self:
            confirmed_horizon = 4  # weeks
            for week_idx, forecast in enumerate(schedule.forecast_ids):
                if week_idx < confirmed_horizon:
                    forecast.forecast_qty = forecast.confirmed_demand_qty
                else:
                    forecast.forecast_qty = max(
                        forecast.confirmed_demand_qty,
                        forecast.statistical_forecast_qty,
                    )

Step 3: Configure Demand Sources

Odoo 19 supports three demand sources for MPS: confirmed sales orders, manual forecasts, and statistical forecasts (if the Forecast module is installed). The critical decision is how far into the future you trust confirmed orders versus forecasts.

Demand SourceBest ForHorizonConfiguration
Confirmed OrdersMake-to-order manufacturers1-4 weeksDefault — no extra setup
Manual ForecastSeasonal products, new launches4-12 weeksEnter in MPS grid manually
Statistical ForecastStable demand, high-volume SKUs4-26 weeksEnable Forecast app, train model
Blend (Custom)Mixed environments1-12 weeksCustom code above
MPS vs. Scheduler: Know the Difference

The MPS is a planning tool — it shows what you should produce but doesn't create manufacturing orders automatically. The scheduler (Manufacturing → Operations → Run Scheduler) reads the MPS and reorder rules, then generates the actual MOs and purchase orders. Run the scheduler after updating the MPS, not before.

02

Multi-Level MRP Configuration: Bills of Materials, Lead Times, and Reorder Rules

The Master Production Schedule tells Odoo what finished goods to build. MRP explodes that demand downward through the Bill of Materials to calculate exactly what raw materials and subassemblies are needed, when they're needed, and whether to make or buy them. Getting this right requires three things: accurate BOMs, correct lead times, and properly configured reorder rules.

Step 1: Structure Your Bills of Materials

Odoo 19 supports multi-level BOMs natively. A finished good's BOM can reference subassemblies that have their own BOMs, which in turn reference raw materials. The MRP engine walks the entire tree. The most common mistake is flat BOMs — listing every raw material on the finished good instead of creating intermediate subassembly BOMs. Flat BOMs prevent Odoo from planning subassembly production independently.

XML — data/mrp_bom_data.xml
<odoo>
  <data>
    <!-- Finished Good: Industrial Controller Board -->
    <record id="bom_controller_board" model="mrp.bom">
      <field name="product_tmpl_id" ref="product_controller_board"/>
      <field name="product_qty">1.0</field>
      <field name="type">normal</field>
      <field name="ready_to_produce">asap</field>
    </record>

    <!-- BOM Line: PCB Subassembly (has its own BOM) -->
    <record id="bom_line_pcb" model="mrp.bom.line">
      <field name="bom_id" ref="bom_controller_board"/>
      <field name="product_id" ref="product_pcb_assembly"/>
      <field name="product_qty">1.0</field>
    </record>

    <!-- BOM Line: Enclosure -->
    <record id="bom_line_enclosure" model="mrp.bom.line">
      <field name="bom_id" ref="bom_controller_board"/>
      <field name="product_id" ref="product_aluminum_enclosure"/>
      <field name="product_qty">1.0</field>
    </record>

    <!-- BOM Line: Power Supply Module -->
    <record id="bom_line_psu" model="mrp.bom.line">
      <field name="bom_id" ref="bom_controller_board"/>
      <field name="product_id" ref="product_power_supply"/>
      <field name="product_qty">1.0</field>
    </record>

    <!-- BOM Line: Wiring Harness (raw material, no sub-BOM) -->
    <record id="bom_line_harness" model="mrp.bom.line">
      <field name="bom_id" ref="bom_controller_board"/>
      <field name="product_id" ref="product_wiring_harness"/>
      <field name="product_qty">2.0</field>
    </record>

    <!-- Sub-BOM: PCB Assembly -->
    <record id="bom_pcb_assembly" model="mrp.bom">
      <field name="product_tmpl_id" ref="product_pcb_assembly_tmpl"/>
      <field name="product_qty">1.0</field>
      <field name="type">normal</field>
    </record>

    <record id="bom_line_bare_pcb" model="mrp.bom.line">
      <field name="bom_id" ref="bom_pcb_assembly"/>
      <field name="product_id" ref="product_bare_pcb"/>
      <field name="product_qty">1.0</field>
    </record>

    <record id="bom_line_resistors" model="mrp.bom.line">
      <field name="bom_id" ref="bom_pcb_assembly"/>
      <field name="product_id" ref="product_smd_resistors"/>
      <field name="product_qty">47.0</field>
    </record>

    <record id="bom_line_capacitors" model="mrp.bom.line">
      <field name="bom_id" ref="bom_pcb_assembly"/>
      <field name="product_id" ref="product_capacitors"/>
      <field name="product_qty">23.0</field>
    </record>
  </data>
</odoo>

Step 2: Set Accurate Lead Times

MRP scheduling is only as good as your lead times. Odoo 19 uses three lead time fields, and confusing them is the #1 cause of late manufacturing orders:

Lead Time FieldWhere to Set ItWhat It Controls
Manufacturing Lead TimeProduct form → Inventory tabDays from MO confirmation to finished good availability
Supplier Lead TimeProduct form → Purchase tab → Vendor pricelistDays from PO confirmation to raw material receipt
Manufacturing Security Lead TimeSettings → ManufacturingBuffer days added to all MO scheduling (company-wide)
Python — models/product_template.py
from odoo import models, fields, api

class ProductTemplate(models.Model):
    _inherit = 'product.template'

    manufacturing_lead_time = fields.Float(
        string='Manufacturing Lead Time (days)',
        default=5.0,
        help='Average time from MO start to finished good. '
             'Include queue time, setup, processing, and QC.',
    )

    @api.model
    def _compute_effective_lead_time(self, product, bom=None):
        """Calculate total lead time including sub-assemblies.
        Walks the BOM tree and returns the critical path duration."""
        if not bom:
            bom = self.env['mrp.bom']._bom_find(product)[product]
        if not bom:
            return product.sale_delay or 0

        max_component_lt = 0
        for line in bom.bom_line_ids:
            component = line.product_id
            sub_bom = self.env['mrp.bom']._bom_find(component)[component]
            if sub_bom:
                # Subassembly: recurse into its BOM
                component_lt = self._compute_effective_lead_time(
                    component, sub_bom
                )
            else:
                # Raw material: use supplier lead time
                seller = component.seller_ids[:1]
                component_lt = seller.delay if seller else 0
            max_component_lt = max(max_component_lt, component_lt)

        return max_component_lt + product.manufacturing_lead_time

Step 3: Configure Reorder Rules for MRP-Driven Procurement

Reorder rules (Inventory → Configuration → Reorder Rules) tell the scheduler when and how much to procure. For MRP-driven manufacturing, set reorder rules on raw materials and purchased components, not on finished goods (those are driven by the MPS).

Python — models/stock_warehouse_orderpoint.py
from odoo import models, fields

class StockWarehouseOrderpoint(models.Model):
    _inherit = 'stock.warehouse.orderpoint'

    mrp_minimum_qty = fields.Float(
        string='MRP Minimum Qty',
        help='Minimum order quantity considering MOQ from supplier '
             'and economic batch size for production.',
    )
    mrp_maximum_qty = fields.Float(
        string='MRP Maximum Qty',
        help='Maximum stock level. MRP will not create procurement '
             'above this threshold.',
    )
    buffer_profile = fields.Selection([
        ('short_long', 'Short Lead / Long Variability'),
        ('short_short', 'Short Lead / Short Variability'),
        ('long_long', 'Long Lead / Long Variability'),
        ('long_short', 'Long Lead / Short Variability'),
    ], string='DDMRP Buffer Profile')

    def _compute_buffer_zones(self):
        """Compute DDMRP green/yellow/red buffer zones
        based on average daily usage and lead time."""
        for rule in self:
            adu = rule.product_id.avg_daily_usage or 1.0
            dlt = rule.lead_days_date or 1
            if rule.buffer_profile == 'long_long':
                rule.product_min_qty = adu * dlt * 0.5   # Red zone
                rule.product_max_qty = adu * dlt * 2.0   # Top of green
            elif rule.buffer_profile == 'short_short':
                rule.product_min_qty = adu * dlt * 0.2
                rule.product_max_qty = adu * dlt * 1.5
Reorder Rule Quantity: Min/Max vs. MRP-Driven

Traditional min/max reorder rules work for consumables (office supplies, packaging). For production-critical raw materials, use Make to Order (MTO) routes combined with MRP. MTO creates procurement only when actual demand exists — no speculative purchasing. Set the route on the product form: Inventory tab → Routes → Replenish on Order (MTO).

03

Work Center Capacity Planning and Finite Scheduling in Odoo 19

Without capacity constraints, Odoo's scheduler treats every work center as if it has infinite capacity. It will happily schedule 200 hours of work into an 8-hour shift. The result on the shop floor: a backlog that grows every day, expediting becomes the norm, and lead time estimates become meaningless. Finite capacity scheduling fixes this by respecting the actual hours available at each work center.

Step 1: Define Work Centers with Realistic Capacity

XML — data/mrp_workcenter_data.xml
<odoo>
  <data>
    <!-- CNC Machining Center -->
    <record id="workcenter_cnc" model="mrp.workcenter">
      <field name="name">CNC Machining Center</field>
      <field name="code">CNC-01</field>
      <field name="capacity">1</field>
      <field name="time_efficiency">85.0</field>
      <field name="oee_target">80.0</field>
      <field name="time_start">15.0</field>
      <field name="time_stop">10.0</field>
      <field name="costs_hour">95.0</field>
    </record>

    <!-- SMT Assembly Line -->
    <record id="workcenter_smt" model="mrp.workcenter">
      <field name="name">SMT Assembly Line</field>
      <field name="code">SMT-01</field>
      <field name="capacity">4</field>
      <field name="time_efficiency">90.0</field>
      <field name="oee_target">85.0</field>
      <field name="time_start">30.0</field>
      <field name="time_stop">15.0</field>
      <field name="costs_hour">120.0</field>
    </record>

    <!-- Quality Control Station -->
    <record id="workcenter_qc" model="mrp.workcenter">
      <field name="name">Quality Control Station</field>
      <field name="code">QC-01</field>
      <field name="capacity">2</field>
      <field name="time_efficiency">95.0</field>
      <field name="oee_target">90.0</field>
      <field name="time_start">5.0</field>
      <field name="time_stop">5.0</field>
      <field name="costs_hour">55.0</field>
    </record>
  </data>
</odoo>

Step 2: Define Routing Operations with Time Standards

Routings connect BOMs to work centers. Each operation specifies the work center, the time per unit, and setup/teardown time. Odoo 19 uses these times to calculate work center load and schedule operations sequentially.

Python — models/mrp_routing_workcenter.py
from odoo import models, fields, api
from datetime import timedelta

class MrpRoutingWorkcenter(models.Model):
    _inherit = 'mrp.routing.workcenter'

    estimated_duration = fields.Float(
        string='Cycle Time (minutes/unit)',
        help='Time to process one unit, excluding setup.',
    )
    setup_duration = fields.Float(
        string='Setup Time (minutes)',
        help='One-time setup before batch starts.',
    )
    teardown_duration = fields.Float(
        string='Teardown Time (minutes)',
        help='Cleanup time after batch completes.',
    )

    @api.depends('estimated_duration', 'setup_duration',
                 'teardown_duration', 'workcenter_id')
    def _compute_total_duration(self):
        """Compute total operation time factoring in efficiency."""
        for op in self:
            wc = op.workcenter_id
            efficiency = (wc.time_efficiency or 100.0) / 100.0
            raw_time = op.setup_duration + op.teardown_duration
            if efficiency > 0:
                raw_time += op.estimated_duration / efficiency
            op.duration_expected = raw_time


class MrpWorkcenter(models.Model):
    _inherit = 'mrp.workcenter'

    def get_available_slots(self, date_from, date_to, duration_minutes):
        """Find the next available time slot on this work center.
        Respects the resource calendar and existing workorders."""
        self.ensure_one()
        calendar = self.resource_calendar_id
        if not calendar:
            return date_from

        # Get intervals where the work center is open
        intervals = calendar._work_intervals_batch(
            date_from, date_to,
            resources=self.resource_id,
        )[self.resource_id.id]

        # Check each interval against existing workorder load
        needed = timedelta(minutes=duration_minutes)
        for start, stop, _meta in intervals:
            existing_load = self.env['mrp.workorder'].search([
                ('workcenter_id', '=', self.id),
                ('state', 'not in', ['done', 'cancel']),
                ('date_start', '<', stop),
                ('date_finished', '>', start),
            ])
            used = sum(
                (min(wo.date_finished, stop) -
                 max(wo.date_start, start)).total_seconds() / 60
                for wo in existing_load
                if wo.date_start and wo.date_finished
            )
            available = (stop - start).total_seconds() / 60 - used
            if available >= duration_minutes:
                return start
        return None

Step 3: Visualize Capacity with the Gantt View

Odoo 19 Enterprise includes a Gantt view for work orders (Manufacturing → Planning → Planning by Workcenter). This shows scheduled operations as bars on a timeline, grouped by work center. Overloaded periods appear as stacked bars that exceed the available hours — an immediate visual signal that the schedule is infeasible.

XML — views/mrp_workorder_gantt.xml
<odoo>
  <record id="view_workorder_gantt_custom" model="ir.ui.view">
    <field name="name">mrp.workorder.gantt.custom</field>
    <field name="model">mrp.workorder</field>
    <field name="inherit_id" ref="mrp.mrp_workorder_view_gantt"/>
    <field name="arch" type="xml">
      <gantt position="attributes">
        <attribute name="color">state</attribute>
        <attribute name="decoration-danger">
          date_finished &gt; date_deadline
        </attribute>
        <attribute name="decoration-warning">
          duration_expected &gt; duration_unit * qty_producing * 1.2
        </attribute>
      </gantt>
    </field>
  </record>
</odoo>
Time Efficiency Is Not OEE

Odoo's time_efficiency field on work centers adjusts the expected duration of operations (e.g., 85% means a 60-minute operation takes 70.6 minutes). OEE (Overall Equipment Effectiveness) is a separate metric that combines availability, performance, and quality. Don't set time_efficiency to your OEE value — they measure different things. Use time_efficiency for scheduling accuracy and OEE for continuous improvement tracking.

04

Demand-Driven MRP (DDMRP) Buffers in Odoo 19: Positioning and Sizing

Traditional MRP is a push system: forecasts drive production plans, which drive procurement. When forecasts are wrong (and they always are), the system amplifies errors through the supply chain — the bullwhip effect. Demand-Driven MRP (DDMRP) flips this by placing strategic inventory buffers at key decoupling points and replenishing them based on actual consumption, not forecasts.

Odoo 19 doesn't include a native DDMRP module, but the framework supports it through custom buffer logic on reorder rules. Here's how we implement it:

Step 1: Identify Decoupling Points

Decoupling points are strategic locations in your BOM where you hold buffer inventory. They break the dependency chain so that demand variability at the finished good level doesn't propagate all the way to raw material suppliers. Good decoupling points are:

  • Long-lead-time purchased components — buffer here to absorb supplier variability
  • Common subassemblies used across multiple finished goods — buffer to enable mix flexibility
  • Points with high demand variability — buffer to decouple upstream from downstream noise

Step 2: Calculate Buffer Zones

Python — models/ddmrp_buffer.py
from odoo import models, fields, api
import logging

_logger = logging.getLogger(__name__)

class DDMRPBuffer(models.Model):
    _name = 'ddmrp.buffer'
    _description = 'DDMRP Strategic Buffer'

    product_id = fields.Many2one('product.product', required=True)
    warehouse_id = fields.Many2one('stock.warehouse', required=True)
    orderpoint_id = fields.Many2one('stock.warehouse.orderpoint')

    # Buffer inputs
    adu = fields.Float(
        string='Average Daily Usage',
        compute='_compute_adu', store=True,
    )
    dlt = fields.Integer(
        string='Decoupled Lead Time (days)',
        help='Lead time from this buffer to the next upstream buffer.',
    )
    variability_factor = fields.Float(
        string='Variability Factor',
        default=0.5,
        help='0.0 = no variability, 1.0 = extreme variability.',
    )
    lead_time_factor = fields.Float(
        string='Lead Time Factor',
        default=0.5,
        help='Portion of DLT used for red zone base.',
    )

    # Buffer zones (computed)
    red_zone_base = fields.Float(compute='_compute_zones', store=True)
    red_zone_safety = fields.Float(compute='_compute_zones', store=True)
    red_zone_total = fields.Float(compute='_compute_zones', store=True)
    yellow_zone = fields.Float(compute='_compute_zones', store=True)
    green_zone = fields.Float(compute='_compute_zones', store=True)
    top_of_green = fields.Float(compute='_compute_zones', store=True)

    @api.depends('product_id')
    def _compute_adu(self):
        """Compute ADU from last 90 days of stock moves."""
        for buf in self:
            moves = self.env['stock.move'].search([
                ('product_id', '=', buf.product_id.id),
                ('state', '=', 'done'),
                ('location_dest_id.usage', '=', 'customer'),
            ], order='date desc', limit=90)
            total_qty = sum(moves.mapped('product_uom_qty'))
            days = 90
            buf.adu = total_qty / days if days else 0

    @api.depends('adu', 'dlt', 'variability_factor',
                 'lead_time_factor')
    def _compute_zones(self):
        """DDMRP buffer zone calculation per the
        Demand Driven Institute methodology."""
        for buf in self:
            adu = buf.adu or 1.0
            dlt = buf.dlt or 1

            # Red Zone
            buf.red_zone_base = adu * dlt * buf.lead_time_factor
            buf.red_zone_safety = buf.red_zone_base * buf.variability_factor
            buf.red_zone_total = buf.red_zone_base + buf.red_zone_safety

            # Yellow Zone = ADU * DLT
            buf.yellow_zone = adu * dlt

            # Green Zone = max(MOQ, ADU * DLT * lead_time_factor)
            moq = buf.product_id.seller_ids[:1].min_qty or 1
            buf.green_zone = max(moq, adu * dlt * buf.lead_time_factor)

            # Top of Green
            buf.top_of_green = (
                buf.red_zone_total + buf.yellow_zone + buf.green_zone
            )

    def apply_to_orderpoint(self):
        """Sync buffer zones to Odoo reorder rules."""
        for buf in self:
            if not buf.orderpoint_id:
                buf.orderpoint_id = self.env[
                    'stock.warehouse.orderpoint'
                ].create({{
                    'product_id': buf.product_id.id,
                    'warehouse_id': buf.warehouse_id.id,
                    'location_id': buf.warehouse_id.lot_stock_id.id,
                }})
            buf.orderpoint_id.write({{
                'product_min_qty': buf.red_zone_total,
                'product_max_qty': buf.top_of_green,
                'qty_multiple': buf.green_zone,
            }})
            _logger.info(
                'DDMRP buffer applied: %s — Red: %.1f, '
                'Yellow: %.1f, Green: %.1f, ToG: %.1f',
                buf.product_id.display_name,
                buf.red_zone_total, buf.yellow_zone,
                buf.green_zone, buf.top_of_green,
            )

Step 3: Net Flow Equation and Replenishment

DDMRP replenishment uses the net flow equation: Net Flow Position = On-Hand + On-Order - Qualified Demand. When the net flow position drops into the yellow or red zone, the system generates a replenishment order to bring it back to the top of green.

Python — models/ddmrp_buffer.py (continued)
    def compute_net_flow_position(self):
        """Calculate net flow and determine replenishment need."""
        for buf in self:
            product = buf.product_id
            wh = buf.warehouse_id

            on_hand = product.with_context(
                warehouse=wh.id
            ).qty_available
            on_order = product.with_context(
                warehouse=wh.id
            ).incoming_qty
            qualified_demand = product.with_context(
                warehouse=wh.id
            ).outgoing_qty

            net_flow = on_hand + on_order - qualified_demand
            buf.net_flow_position = net_flow

            # Determine zone and action
            if net_flow <= buf.red_zone_total:
                buf.zone_status = 'red'
                buf.replenish_qty = buf.top_of_green - net_flow
            elif net_flow <= (buf.red_zone_total + buf.yellow_zone):
                buf.zone_status = 'yellow'
                buf.replenish_qty = buf.top_of_green - net_flow
            else:
                buf.zone_status = 'green'
                buf.replenish_qty = 0.0
DDMRP Is Not Anti-Forecast

A common misconception is that DDMRP eliminates forecasting entirely. It doesn't. DDMRP uses forecasts for strategic buffer sizing (adjusting the ADU and variability factor based on known future events like promotions or seasonal spikes). What it eliminates is using forecasts to drive individual order timing — that's done by actual consumption against buffers instead.

05

4 MRP Configuration Mistakes That Wreck Your Production Schedule

1

Phantom BOMs Misconfigured as Normal BOMs

A phantom BOM (kit) tells MRP to explode the subassembly into its components without creating a separate manufacturing order. A normal BOM creates an independent MO. If you set a subassembly as normal when it should be phantom, the scheduler creates unnecessary MOs, inflates WIP inventory, and adds lead time that doesn't exist on the shop floor. Conversely, making a BOM phantom when the subassembly requires its own production step means components are consumed directly into the parent MO — skipping the sub-process entirely.

Our Fix

Use the physical reality test: Does this subassembly physically exist as a stocked item that moves between work centers? If yes, use a normal BOM. If it's a logical grouping of components that are consumed in-place during the parent assembly, use a phantom BOM.

2

Running the Scheduler Without Setting Lead Times First

The scheduler uses lead times to calculate when to start procurement and production. If lead times are zero (the default), the scheduler creates all procurement orders for today — regardless of when the finished good is needed. Your purchasing team receives 500 RFQs dated today, all marked urgent, and ignores them all because they can't differentiate real urgency from bad configuration.

Our Fix

Before running the scheduler for the first time, audit every product in your BOM tree. Set manufacturing lead times on produced items and supplier lead times on purchased items. Use the BOM Structure report (Manufacturing → Reporting → BOM Structure) to verify that every node in the tree has a non-zero lead time.

3

Infinite Capacity Scheduling Hides Bottlenecks

By default, Odoo schedules manufacturing orders using infinite capacity — it assigns start and end dates based on lead times alone, without checking if the work center is available. The Gantt chart looks perfectly scheduled, but the shop floor tells a different story: three MOs all need the CNC machine on Tuesday, and only one can run at a time. The other two slip by days, and downstream operations cascade late.

Our Fix

Enable Work Orders in Manufacturing settings. Then use the Planning by Workcenter view to visually identify overloaded periods. For true finite scheduling, implement the slot-finding logic shown in Section 03 to check work center availability before confirming MOs.

4

Ignoring the Scheduler Frequency Setting

The Odoo scheduler runs as a cron job — by default, once per day at midnight. If your sales team confirms a large order at 9 AM, the MRP engine won't generate the corresponding MOs and purchase orders until midnight. That's 15 hours of invisible delay. For high-volume manufacturers, this daily batch approach creates an artificial lag between demand and response.

Our Fix

Increase scheduler frequency to every 2-4 hours for active manufacturing environments. Navigate to Settings → Technical → Scheduled Actions, find "Run Scheduler," and adjust the interval. For urgent orders, use the manual "Run Scheduler" button or create a server action that triggers the scheduler when a high-priority SO is confirmed.

BUSINESS ROI

What Proper MRP Planning Saves Your Manufacturing Operation

MRP configuration is a one-time investment that compounds daily. Here's what changes when your production schedule lives in Odoo instead of spreadsheets:

30-50%Less WIP Inventory

Accurate lead times and BOM explosions mean you order materials when needed, not "just in case." Less capital tied up on the shop floor.

95%+On-Time Delivery

Capacity-constrained scheduling gives realistic delivery dates. Customers get accurate promises, not optimistic guesses that slip every week.

15-25%Higher Throughput

Finite scheduling eliminates work center conflicts. Fewer queue times, less expediting, more actual production hours per shift.

The hidden ROI is planner time. A production planner manually juggling Excel, email, and Odoo spends 60-70% of their time on data entry and cross-referencing. With MRP configured correctly, that drops to 20% — the planner shifts from data clerk to decision maker, focusing on exceptions and continuous improvement instead of building next week's schedule from scratch every Friday.

SEO NOTES

Optimization Metadata

Meta Desc

Complete guide to MRP planning in Odoo 19. Configure Master Production Schedule, multi-level BOMs, work center capacity, finite scheduling, and DDMRP buffers.

H2 Keywords

1. "Configuring the Master Production Schedule in Odoo 19"
2. "Multi-Level MRP Configuration: Bills of Materials, Lead Times, and Reorder Rules"
3. "Work Center Capacity Planning and Finite Scheduling in Odoo 19"
4. "Demand-Driven MRP (DDMRP) Buffers in Odoo 19: Positioning and Sizing"

Stop Building Your Production Schedule in Excel

Every hour your production planner spends cross-referencing inventory levels, supplier lead times, and work center availability in a spreadsheet is an hour that Odoo's MRP engine could handle in seconds. The scheduler doesn't forget to check a component's stock level. It doesn't accidentally schedule two jobs on the same machine at the same time. And it doesn't go on vacation the week before your biggest order ships.

If you're running Odoo 19 in manufacturing and your MRP module is mostly unconfigured, we can help. We audit your BOMs, set up the Master Production Schedule, configure work center capacities, implement reorder rules, and train your planning team. The result is a production schedule that updates automatically, respects real constraints, and gives your sales team delivery dates they can actually trust.

Book a Free MRP Assessment