A Contaminated Batch Without Traceability Costs You the Entire Product Line
In 2024, a mid-size food manufacturer recalled 12,000 units because a supplier delivered flour contaminated with an undeclared allergen. Without lot traceability, they couldn't determine which production batches used that specific flour delivery. The result: every product made in the same week was pulled from shelves, costing them $380,000 in destroyed inventory and $1.2 million in lost retail contracts.
Had they tracked supplier lot numbers through to finished goods, the recall would have hit 800 units across 3 batches instead of 12,000 across the entire week. The difference between targeted and blanket recalls is the difference between a manageable incident and an existential threat.
Odoo 19's manufacturing module supports full upstream and downstream traceability: lot numbers on raw materials flow through work orders into finished goods, serial numbers get assigned at specific operations, and the traceability report links every finished product back to its exact components. This guide covers the complete setup — from enabling tracking on products through to running compliance reports and executing recalls.
Configuring Lot and Serial Tracking on Products and BOMs in Odoo 19
Traceability starts at the product level. Every product involved in manufacturing — raw materials, semi-finished goods, and finished products — needs its tracking method explicitly set. Odoo 19 offers three options: No Tracking, By Lots (batch tracking), and By Unique Serial Number.
Step 1 — Enable Lot and Serial Number Tracking
Navigate to Inventory → Configuration → Settings. Under Traceability, enable Lots & Serial Numbers. This unlocks the tracking field on every product form and activates lot/serial selection on all stock operations.
Step 2 — Set Tracking per Product
On each product form, go to the Inventory tab and set the Tracking field. The decision tree is straightforward:
| Product Type | Tracking Method | When to Use | Example |
|---|---|---|---|
| Raw materials (bulk) | By Lots | Received in batches from suppliers, used across multiple production orders | Steel coils, flour, pigment, chemical solvents |
| Semi-finished goods | By Lots | Produced in batches, consumed in downstream assemblies | PCB boards, dough batches, paint mixtures |
| Finished goods (high value) | By Serial Number | Each unit individually identifiable for warranty, recall, or regulatory compliance | Electronics, medical devices, machinery |
| Finished goods (FMCG) | By Lots | Produced in large batches, tracked by production run, not individual unit | Canned food, cosmetics, cleaning products |
| Consumables (low value) | No Tracking | No regulatory or business reason to trace | Packaging tape, labels, disposable gloves |
Step 3 — Configure the Bill of Materials
The BOM defines which tracked components feed into your finished product. For traceability to work end-to-end, every tracked component must appear in the BOM with the correct product variant. Here is a sample BOM for an electronic controller with both lot-tracked and serial-tracked items:
# Create the BOM for "Smart Controller v3"
bom_id = models.execute_kw(db, uid, password,
'mrp.bom', 'create', [{
'product_tmpl_id': smart_controller_tmpl_id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {
'product_id': pcb_board_id, # Tracked: By Lots
'product_qty': 1.0,
}),
(0, 0, {
'product_id': capacitor_kit_id, # Tracked: By Lots
'product_qty': 4.0,
}),
(0, 0, {
'product_id': power_supply_id, # Tracked: By Serial Number
'product_qty': 1.0,
}),
(0, 0, {
'product_id': enclosure_id, # No Tracking
'product_qty': 1.0,
}),
],
'operation_ids': [
(0, 0, {
'name': 'SMT Assembly',
'workcenter_id': smt_line_id,
'time_cycle_manual': 12.0,
}),
(0, 0, {
'name': 'Functional Test',
'workcenter_id': test_bench_id,
'time_cycle_manual': 8.0,
}),
(0, 0, {
'name': 'Final Assembly & Serial Assignment',
'workcenter_id': assembly_station_id,
'time_cycle_manual': 15.0,
}),
],
}]
)Track at the level where a quality issue would require action. If a defective capacitor batch affects all boards assembled that day, track capacitors by lot. If a defective power supply affects exactly one finished unit, track power supplies by serial number. Over-tracking creates operator friction; under-tracking creates recall nightmares.
Running Batch Production with Automatic Lot Assignment in Odoo 19
Batch production is the default mode for lot-tracked finished goods. You create a manufacturing order for a quantity (e.g., 500 units), Odoo assigns a lot number to the entire batch, and all 500 units share that lot. The lot number becomes the key that links back to every component consumed during that production run.
Step 1 — Create the Manufacturing Order
Navigate to Manufacturing → Operations → Manufacturing Orders and create a new order. Select your tracked product and set the quantity. Odoo will auto-generate a lot number using the sequence defined on the product category (or you can assign one manually).
Step 2 — Configure Lot Number Sequences
Default lot numbers like LOT/00001 are useless for compliance. Configure meaningful sequences that encode production context:
# In your custom module: models/stock_lot.py
from odoo import models, fields, api
class StockLot(models.Model):
_inherit = 'stock.lot'
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('name'):
product = self.env['product.product'].browse(
vals.get('product_id')
)
prefix = product.default_code or 'LOT'
date_code = fields.Date.today().strftime('%y%m%d')
seq = self.env['ir.sequence'].next_by_code(
'stock.lot.serial'
) or '0001'
# Result: SC19-260313-0001
# (product code - date - sequence)
vals['name'] = f"{{prefix}}-{{date_code}}-{{seq}}"
return super().create(vals_list)Step 3 — Consume Components with Lot Selection
When you confirm the manufacturing order, Odoo creates stock moves for each BOM component. For lot-tracked components, the operator must select which lot to consume. This is where traceability happens — the link between raw material lot FLOUR-260301-A and finished goods lot BREAD-260313-0042 is recorded permanently.
# Confirm the manufacturing order
mo = env['mrp.production'].browse(mo_id)
mo.action_confirm()
# For each tracked component, assign the source lot
for move in mo.move_raw_ids:
if move.product_id.tracking in ('lot', 'serial'):
# Find available lots using FEFO (First Expired, First Out)
available_lot = env['stock.lot'].search([
('product_id', '=', move.product_id.id),
('quant_ids.location_id', '=', mo.location_src_id.id),
('quant_ids.quantity', '>', 0),
], order='use_date asc', limit=1)
move.move_line_ids.write({
'lot_id': available_lot.id,
'quantity': move.product_uom_qty,
})
# Produce: assign the finished goods lot
mo.lot_producing_id = finished_lot_id
mo.qty_producing = mo.product_qty
mo.button_mark_done()If your raw materials have expiration dates (food, pharmaceuticals, chemicals), enable Expiration Dates in Inventory settings. Odoo will then use FEFO (First Expired, First Out) removal strategy instead of FIFO — consuming the lot closest to expiry first. This is mandatory for FDA 21 CFR Part 211 and EU GMP Annex 11 compliance.
Serial Number Assignment at Work Orders: Per-Unit Tracking Through Operations
For serial-tracked finished goods, each unit gets a unique identifier. In Odoo 19, serial numbers can be assigned at specific work order operations — typically the final assembly or quality check step. This means you know not just what was produced, but which workstation and which operator handled each unit.
Step 1 — Enable Work Orders
Go to Manufacturing → Configuration → Settings and enable Work Orders. This activates the routing (operation sequence) on BOMs and creates individual work orders for each operation when a manufacturing order is confirmed.
Step 2 — Configure Serial Assignment on the Last Operation
On the BOM, set the Manufacturing Readiness field to define when serial numbers are required. Best practice: assign serials at the last operation (typically final assembly or QC inspection) so that scrapped units from earlier operations don't consume serial numbers.
# Generate sequential serial numbers for a batch of 50 units
product = env['product.product'].browse(controller_product_id)
serials = []
for i in range(1, 51):
serial = env['stock.lot'].create({
'name': f"SCV3-2603-{{str(i).zfill(5)}}",
'product_id': product.id,
'company_id': env.company.id,
})
serials.append(serial.id)
# Assign serials to the manufacturing order
# Each unit in the MO gets one serial from the list
mo = env['mrp.production'].browse(mo_id)
mo.action_confirm()
# Process work orders one unit at a time
for idx, wo in enumerate(mo.workorder_ids.filtered(
lambda w: w.state == 'ready'
)):
wo.button_start()
# At the final operation, assign serial
if wo.is_last_unfinished_wo:
wo.finished_lot_id = serials[idx]
wo.button_finish()Step 3 — Tablet Interface for Shop Floor Operators
Odoo 19's shop floor module provides a tablet-friendly interface for operators. At each work order, the operator sees: the operation instructions, required components (with lot numbers pre-selected by FEFO/FIFO), a barcode scan field for the serial number, and quality check points if configured. The operator scans or enters the serial, completes the operation, and moves to the next unit.
<!-- In your custom module: views/mrp_workorder_views.xml -->
<record id="quality_point_serial_scan" model="quality.point">
<field name="name">Scan Serial Number Label</field>
<field name="product_ids"
eval="[(4, ref('product_smart_controller_v3'))]"/>
<field name="picking_type_ids"
eval="[(4, ref('mrp.picking_type_manufacturing'))]"/>
<field name="operation_id"
ref="mrp_operation_final_assembly"/>
<field name="test_type">passfail</field>
<field name="note">
Scan the printed serial label. Verify it matches
the label on the unit enclosure. If mismatched,
flag for rework.
</field>
</record>If your finished product is tracked by serial number, Odoo 19 forces each work order to process exactly one unit at a time. For a manufacturing order of 50 units, the operator completes 50 cycles through each work order. This is by design — it guarantees that each serial number maps to exactly one set of component lots. If you need to produce 500 units/hour, make sure your cycle time accounts for this per-unit processing overhead.
Upstream and Downstream Traceability Reports: From Component Lot to Customer Delivery
The traceability report is where everything comes together. Odoo 19 provides two directions of traceability, and both are critical for different scenarios:
| Direction | Starting Point | What It Answers | Use Case |
|---|---|---|---|
| Upstream (backward) | Finished goods lot/serial | Which raw material lots went into this product? | Customer complaint, warranty claim, quality investigation |
| Downstream (forward) | Raw material lot | Which finished products contain this material? Who received them? | Supplier recall, contamination alert, regulatory audit |
Accessing the Traceability Report
Navigate to Inventory → Products → Lots/Serial Numbers. Select any lot or serial number. Click the Traceability button in the top menu. Odoo displays a chronological table showing every stock move that involved this lot: receipt from supplier, consumption in manufacturing, production of finished goods, delivery to customer.
# Find all finished goods that consumed a specific raw material lot
raw_lot = env['stock.lot'].search([
('name', '=', 'STEEL-260228-B04'),
('product_id.name', '=', 'Carbon Steel Sheet 2mm'),
])
# Get all stock moves where this lot was consumed in manufacturing
consumption_moves = env['stock.move.line'].search([
('lot_id', '=', raw_lot.id),
('move_id.raw_material_production_id', '!=', False),
])
# Extract the manufacturing orders
affected_mos = consumption_moves.mapped(
'move_id.raw_material_production_id'
)
# Get finished goods lots from those MOs
finished_lots = affected_mos.mapped('lot_producing_id')
# Find customer deliveries containing those finished lots
affected_deliveries = env['stock.move.line'].search([
('lot_id', 'in', finished_lots.ids),
('move_id.picking_id.picking_type_code', '=', 'outgoing'),
('state', '=', 'done'),
])
# Build the recall list: customer + product + serial/lot + delivery date
for line in affected_deliveries:
picking = line.move_id.picking_id
partner = picking.partner_id
print(f"Customer: {{partner.name}}")
print(f" Product: {{line.product_id.name}}")
print(f" Lot/Serial: {{line.lot_id.name}}")
print(f" Delivered: {{picking.date_done}}")
print(f" Delivery Order: {{picking.name}}")
print("---")Traceability Report Output Example
A properly configured traceability chain produces a report like this:
Raw Material Lot: STEEL-260228-B04
Supplier: Acme Steel Corp (PO: PO/2026/00312)
Received: 2026-02-28 | Qty: 500 kg
Consumed In:
+-- MO/2026/00891 (Bracket Assembly A)
| Produced Lot: BRKT-260305-0012 | Qty: 200 units
| +-- Delivered: SO/2026/01445 → Delta Mfg (2026-03-07)
| +-- Delivered: SO/2026/01502 → Echo Industries (2026-03-10)
| +-- In Stock: WH/Stock | Qty: 45 units
|
+-- MO/2026/00903 (Bracket Assembly A)
| Produced Lot: BRKT-260307-0013 | Qty: 180 units
| +-- Delivered: SO/2026/01523 → Foxtrot Corp (2026-03-11)
| +-- In Stock: WH/Stock | Qty: 180 units
Total Affected: 380 units across 2 production lots
Already Shipped: 155 units to 3 customers
Still In Stock: 225 units (can be quarantined immediately)Executing a Product Recall in Odoo 19: Quarantine, Notify, and Document
When a quality issue surfaces — a failed lab test, a customer complaint, a supplier notification — the clock starts. Regulatory bodies like the FDA expect documented recall actions within 24 hours of identifying a safety concern. Here is the step-by-step recall workflow using Odoo 19's native features.
Step 1 — Identify Affected Lots
Use the downstream traceability report (described above) to identify every finished goods lot that contains the affected raw material. Record the manufacturing orders, finished lot numbers, quantities produced, and quantities already shipped.
Step 2 — Quarantine In-Stock Units
Create a scrap location or dedicated quarantine location (Inventory → Configuration → Warehouses → Locations). Then move affected lots from stock to quarantine:
# Define the quarantine location
quarantine_loc = env['stock.location'].search([
('name', '=', 'Quarantine'),
('usage', '=', 'internal'),
], limit=1)
# For each affected finished lot still in stock
for lot in affected_finished_lots:
quants = env['stock.quant'].search([
('lot_id', '=', lot.id),
('location_id.usage', '=', 'internal'),
('location_id', '!=', quarantine_loc.id),
('quantity', '>', 0),
])
for quant in quants:
# Create an internal transfer to quarantine
picking_type = env.ref('stock.picking_type_internal')
picking = env['stock.picking'].create({
'picking_type_id': picking_type.id,
'location_id': quant.location_id.id,
'location_dest_id': quarantine_loc.id,
'origin': f"RECALL-{{recall_reference}}",
'move_ids': [(0, 0, {
'name': f"Quarantine: {{lot.name}}",
'product_id': lot.product_id.id,
'product_uom_qty': quant.quantity,
'product_uom': lot.product_id.uom_id.id,
'location_id': quant.location_id.id,
'location_dest_id': quarantine_loc.id,
})],
})
picking.action_confirm()
picking.action_assign()
# Set the lot on the move line
picking.move_line_ids.write({
'lot_id': lot.id,
'quantity': quant.quantity,
})
picking.button_validate()Step 3 — Notify Affected Customers
Using the delivery data from the traceability report, generate a recall notification list. Odoo's mail templates can automate this:
# Build customer recall notifications
for delivery in affected_deliveries:
partner = delivery.move_id.picking_id.partner_id
lot_name = delivery.lot_id.name
product_name = delivery.product_id.name
delivery_ref = delivery.move_id.picking_id.name
# Create and send the recall email
template = env.ref('your_module.recall_notification_template')
template.with_context(
lot_name=lot_name,
product_name=product_name,
delivery_ref=delivery_ref,
recall_ref=recall_reference,
).send_mail(partner.id, force_send=True)Step 4 — Document for Compliance
Regulatory audits require documented proof of recall execution. Use Odoo's chatter on the affected manufacturing orders and lots to log every action: when the issue was identified, who authorized the quarantine, which customers were notified, and the disposition of recalled units (rework, scrap, or return to supplier). Every chatter message is timestamped and tied to a user — this is your audit trail.
With proper traceability configured, a recall that took one company 5 days of manual spreadsheet work (cross-referencing purchase orders, production logs, and shipping records) takes under 2 hours in Odoo: 30 minutes to run the traceability report, 30 minutes to quarantine in-stock units, and 1 hour to notify affected customers. The difference isn't just speed — it's completeness. Manual processes miss lots; Odoo's traceability report catches every stock move.
4 Traceability Mistakes That Break Recall Capability in Odoo 19
Changing Product Tracking After Stock Moves Exist
If a product has existing stock moves with No Tracking and you later switch to By Lots, Odoo won't retroactively assign lot numbers to historical moves. Your traceability chain has a gap. Worse, the on-hand quantity may split between tracked and untracked quants, causing inventory discrepancies that are extremely difficult to reconcile.
Set tracking methods before the first receipt. If you must change tracking on a product with history, zero out all existing stock first (physical inventory adjustment), change the tracking method, then receive the existing stock back in with proper lot numbers.
Operators Selecting "New Lot" Instead of the Received Supplier Lot
During manufacturing, when an operator is prompted to select a component lot, they sometimes click "Create New" instead of selecting the existing lot received from the supplier. This creates a phantom lot that traces to nothing upstream — no purchase order, no supplier, no receipt date. The downstream traceability looks complete, but the upstream link is broken.
Remove the "Create New" option from the lot selection dropdown on manufacturing order forms using an access right or UI customization. Lots should only be created during receipts (purchase order receiving) and production (for finished goods). Never during component consumption.
Backflush Consumption Without Lot Verification
Odoo supports backflush component consumption — materials are automatically consumed when the finished product is produced, without the operator explicitly picking each component. For non-tracked products, this is efficient. For lot-tracked components, backflush uses FIFO by default, which may not reflect the actual lot the operator physically used. If the warehouse has lots A, B, and C on the shelf, and the operator grabs lot B, Odoo records lot A.
For any component where traceability is critical, set the Manual Consumption flag on the BOM line. This forces the operator to scan or select the actual lot before the work order can be completed. Yes, it's slower. But backflushed traceability is fictional traceability.
No Expiration Dates on Lot-Tracked Raw Materials
Enabling lot tracking without expiration dates means Odoo defaults to FIFO removal — oldest lot consumed first. But if your supplier delivers a lot with a shorter shelf life alongside an older lot with a longer one, FIFO consumes the wrong lot first. In regulated industries, using expired raw materials in production is a critical non-conformance that can shut down your facility.
Enable Expiration Dates in Inventory settings. Configure the four date fields on each tracked product: Best Before Date, Use Date, Removal Date, and End of Life Date. Set the removal strategy on the location to FEFO. Odoo will then always propose the lot closest to expiry first.
What End-to-End Traceability Saves Your Manufacturing Operation
Traceability isn't a cost center — it's insurance with a measurable return. Here's what we see across manufacturing clients:
Targeted lot-level recalls affect only the specific batches containing the defective component, not the entire product line. Average recall scope drops from weeks of production to individual runs.
From issue identification to customer notification, a fully traced recall executes in under 2 hours. Without traceability, the same process takes 3-5 business days of manual investigation.
FDA, ISO 22000, and EU GMP auditors can pull any finished product lot and trace it back to raw material suppliers within minutes. No binders, no spreadsheets, no scrambling.
Beyond recalls, traceability delivers operational intelligence. When you can trace a quality defect to a specific supplier lot, you make data-driven vendor decisions. When you can track which production line produces more rework, you target maintenance and training. The data you collect for compliance becomes the data that drives continuous improvement.
Optimization Metadata
Complete guide to lot and serial traceability in Odoo 19 manufacturing. Covers batch production, serial assignment at work orders, component tracing, recall workflows, and compliance reporting.
1. "Configuring Lot and Serial Tracking on Products and BOMs in Odoo 19"
2. "Serial Number Assignment at Work Orders: Per-Unit Tracking Through Operations"
3. "Executing a Product Recall in Odoo 19: Quarantine, Notify, and Document"
4. "4 Traceability Mistakes That Break Recall Capability in Odoo 19"