GuideMarch 13, 2026

Batch & Serial Manufacturing in Odoo 19:
Traceability from Raw Materials to Finished Goods

INTRODUCTION

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.

01

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 TypeTracking MethodWhen to UseExample
Raw materials (bulk)By LotsReceived in batches from suppliers, used across multiple production ordersSteel coils, flour, pigment, chemical solvents
Semi-finished goodsBy LotsProduced in batches, consumed in downstream assembliesPCB boards, dough batches, paint mixtures
Finished goods (high value)By Serial NumberEach unit individually identifiable for warranty, recall, or regulatory complianceElectronics, medical devices, machinery
Finished goods (FMCG)By LotsProduced in large batches, tracked by production run, not individual unitCanned food, cosmetics, cleaning products
Consumables (low value)No TrackingNo regulatory or business reason to tracePackaging 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:

XML-RPC — Creating a tracked BOM via API
# 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,
            }),
        ],
    }]
)
Tracking Granularity Rule

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.

02

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:

Python — Custom lot sequence with date encoding
# 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.

Python — Programmatic lot consumption on MO
# 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()
FEFO vs FIFO for Regulated Industries

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.

03

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.

Python — Generating serial numbers for a production run
# 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.

XML — Adding a barcode scan step to work order instructions
<!-- 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>
Serial vs Lot at Work Orders

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.

04

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:

DirectionStarting PointWhat It AnswersUse Case
Upstream (backward)Finished goods lot/serialWhich raw material lots went into this product?Customer complaint, warranty claim, quality investigation
Downstream (forward)Raw material lotWhich 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.

Python — Programmatic downstream traceability query
# 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:

Traceability — Downstream from STEEL-260228-B04
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)
05

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:

Python — Automated quarantine of affected lots
# 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:

Python — Sending recall notifications via Odoo mail
# 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.

Recall Speed Benchmark

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.

06

4 Traceability Mistakes That Break Recall Capability in Odoo 19

1

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.

Our Fix

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.

2

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.

Our Fix

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.

3

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.

Our Fix

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.

4

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.

Our Fix

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.

BUSINESS ROI

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:

92%Smaller Recall Scope

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.

2hrsRecall Execution Time

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.

100%Audit Readiness

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.

SEO NOTES

Optimization Metadata

Meta Desc

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.

H2 Keywords

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"

Traceability Is Not Optional — It's the Cost of Manufacturing Responsibly

Every manufacturer hopes they'll never need to execute a recall. But the ones who survive recalls intact are the ones who can trace a defective component to the exact finished products it touched, quarantine in-stock units before they ship, and notify affected customers with specific lot numbers — not blanket "we're recalling everything made this month" announcements.

If you're running manufacturing in Odoo without lot or serial traceability — or if it's configured but you've never tested a mock recall — we should talk. We run traceability audits that test the full chain: supplier receipt through production through delivery. We'll find the gaps before an auditor or a contamination event does.

Book a Free Traceability Audit