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.
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.
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.
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 Source | Best For | Horizon | Configuration |
|---|---|---|---|
| Confirmed Orders | Make-to-order manufacturers | 1-4 weeks | Default — no extra setup |
| Manual Forecast | Seasonal products, new launches | 4-12 weeks | Enter in MPS grid manually |
| Statistical Forecast | Stable demand, high-volume SKUs | 4-26 weeks | Enable Forecast app, train model |
| Blend (Custom) | Mixed environments | 1-12 weeks | Custom code above |
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.
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.
<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 Field | Where to Set It | What It Controls |
|---|---|---|
| Manufacturing Lead Time | Product form → Inventory tab | Days from MO confirmation to finished good availability |
| Supplier Lead Time | Product form → Purchase tab → Vendor pricelist | Days from PO confirmation to raw material receipt |
| Manufacturing Security Lead Time | Settings → Manufacturing | Buffer days added to all MO scheduling (company-wide) |
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_timeStep 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).
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 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).
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
<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.
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 NoneStep 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.
<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 > date_deadline
</attribute>
<attribute name="decoration-warning">
duration_expected > duration_unit * qty_producing * 1.2
</attribute>
</gantt>
</field>
</record>
</odoo> 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.
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
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.
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.0A 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.
4 MRP Configuration Mistakes That Wreck Your Production Schedule
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.
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.
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.
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.
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.
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.
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.
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.
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:
Accurate lead times and BOM explosions mean you order materials when needed, not "just in case." Less capital tied up on the shop floor.
Capacity-constrained scheduling gives realistic delivery dates. Customers get accurate promises, not optimistic guesses that slip every week.
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.
Optimization Metadata
Complete guide to MRP planning in Odoo 19. Configure Master Production Schedule, multi-level BOMs, work center capacity, finite scheduling, and DDMRP buffers.
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"