You Outsource Production, But Your ERP Still Thinks You Make Everything In-House
Most mid-market manufacturers outsource at least some of their production. Metal stamping, PCB assembly, powder coating, plastic injection molding — these are specialized processes that require expensive equipment you don't want to own. The problem is that many ERP implementations treat subcontracting as a manual workaround: you ship components via a delivery order, wait for the finished product, receive it against a purchase order, and hope the lot numbers match up in the end.
Odoo 19's subcontracting module eliminates this gap. It lets you define subcontracting Bills of Materials, automatically generate component shipments when you place a purchase order with a subcontractor, track components at the subcontractor's location, receive finished goods with full traceability back to the original raw materials, and run quality inspections on subcontracted products before they enter your stock.
This guide walks through every step — from enabling the module and configuring your first subcontractor through to advanced scenarios like multi-level subcontracting, valuation, and quality control. Every code example is tested against Odoo 19 Community and Enterprise.
Setting Up Subcontractors in Odoo 19: Vendor Configuration and Subcontracting Location
Before you can use subcontracting, you need to enable the module and configure your vendors as subcontractors. Odoo 19 creates a dedicated subcontracting location for each subcontractor — this is how the system tracks components that have left your warehouse but haven't been consumed yet.
Step 1: Enable the Subcontracting Module
Navigate to Settings → Manufacturing and enable Subcontracting. This installs mrp_subcontracting and creates the parent virtual location Partner Locations / Subcontracting. If you also want quality checks on subcontracted receipts, enable Quality under the same settings page — this installs mrp_subcontracting_quality.
Step 2: Configure the Vendor as a Subcontractor
Open the vendor contact form. Under the Purchase tab, you'll notice no special "subcontractor" checkbox — in Odoo 19, a vendor becomes a subcontractor the moment you assign them to a subcontracting BoM. However, you should still set up the vendor properly:
- Payment terms — subcontractors often invoice per batch, not per unit. Set terms that match your agreement.
- Delivery lead time — this is the subcontractor's production lead time. Odoo uses it for scheduling.
- Currency — if the subcontractor invoices in a different currency, set it here to get automatic conversion on POs.
Step 3: Understand the Subcontracting Location
When you confirm a PO to a subcontractor, Odoo automatically creates a child location under Partner Locations / Subcontracting named after the vendor. Components are moved from your warehouse to this location. This gives you real-time visibility into what materials are sitting at each subcontractor's facility.
# Useful when migrating vendors from a legacy system
# Run via shell or server action
subcontractor = env['res.partner'].search([
('name', '=', 'Precision Metal Works Inc.')
], limit=1)
# The subcontracting location is auto-created, but you can
# customize its properties (e.g., for valuation or removal strategy)
sub_location = env['stock.location'].search([
('is_subcontracting_location', '=', True),
('company_id', '=', env.company.id),
], limit=1)
# Log the location for verification
_logger.info(
'Subcontracting location: %s (ID: %s)',
sub_location.complete_name,
sub_location.id,
) By default, Odoo 19 uses a single shared subcontracting location. If you need separate locations per subcontractor (e.g., for valuation or audit purposes), you can create child locations manually under Partner Locations / Subcontracting and assign them via the BoM's operation type. This is especially useful when different subcontractors hold different insurance or bonding requirements for your materials.
Creating a Subcontracting Bill of Materials in Odoo 19
The subcontracting BoM is the heart of the entire workflow. It defines what you send to the subcontractor (components), what you get back (finished product), and who does the work (which vendors are designated subcontractors for this BoM).
Creating the BoM via the UI
Navigate to Manufacturing → Bills of Materials → New. Set the BoM type to Subcontracting. Then:
- Product: The finished good you'll receive back (e.g., "Powder Coated Bracket - Red").
- Quantity: The quantity this BoM produces (typically 1.00).
- Subcontractors: Select one or more vendors. When you create a PO for this product to any of these vendors, Odoo triggers the subcontracting flow.
- Components: Add every raw material or semi-finished product the subcontractor needs. Include packaging materials if you supply them.
# Example: Powder coating BoM
# The subcontractor receives raw brackets and returns coated brackets
product_finished = env['product.product'].search([
('default_code', '=', 'BRACKET-COATED-RED')
], limit=1)
product_raw = env['product.product'].search([
('default_code', '=', 'BRACKET-RAW')
], limit=1)
product_powder = env['product.product'].search([
('default_code', '=', 'POWDER-RED-1KG')
], limit=1)
subcontractor = env['res.partner'].search([
('name', '=', 'Precision Metal Works Inc.')
], limit=1)
bom = env['mrp.bom'].create({
'product_tmpl_id': product_finished.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'subcontract',
'subcontractor_ids': [(4, subcontractor.id)],
'bom_line_ids': [
(0, 0, {
'product_id': product_raw.id,
'product_qty': 1.0,
}),
(0, 0, {
'product_id': product_powder.id,
'product_qty': 0.05, # 50g per bracket
}),
],
})XML Data File for Module Deployment
If you're packaging subcontracting configuration as a custom module (recommended for staging/production parity), here's the XML data file pattern:
<odoo>
<data noupdate="1">
<!-- Subcontracting BoM: Powder Coated Bracket -->
<record id="bom_bracket_coated_red" model="mrp.bom">
<field name="product_tmpl_id" ref="product.product_tmpl_bracket_coated_red"/>
<field name="product_qty">1.0</field>
<field name="type">subcontract</field>
<field name="subcontractor_ids"
eval="[(4, ref('base.res_partner_precision_metal'))]"/>
</record>
<record id="bom_line_bracket_raw" model="mrp.bom.line">
<field name="bom_id" ref="bom_bracket_coated_red"/>
<field name="product_id" ref="product.product_bracket_raw"/>
<field name="product_qty">1.0</field>
</record>
<record id="bom_line_powder_red" model="mrp.bom.line">
<field name="bom_id" ref="bom_bracket_coated_red"/>
<field name="product_id" ref="product.product_powder_red_1kg"/>
<field name="product_qty">0.05</field>
</record>
</data>
</odoo>You can assign multiple subcontractors to the same BoM. This is ideal when you have a primary and backup subcontractor for the same process. When you create a PO, Odoo matches the vendor to the BoM and triggers the correct flow regardless of which subcontractor you choose. If different subcontractors require different component quantities (e.g., one wastes more material), create separate BoMs — one per vendor.
Shipping Components to the Subcontractor: Automatic Resupply and Manual Shipments
When you confirm a purchase order for a subcontracted product, Odoo needs to get the components to the subcontractor. There are two models: automatic resupply on order (Odoo creates a delivery order for the components when the PO is confirmed) and manual resupply (you ship components independently of the PO). Most companies use automatic resupply — it's less error-prone and keeps the paper trail clean.
Automatic Resupply (Recommended)
This is the default behavior. When you confirm a PO to a subcontractor with a subcontracting BoM:
- Odoo creates a picking (delivery order) to transfer components from your warehouse to the subcontractor's location.
- The picking type is Resupply Subcontractor — a dedicated operation type created when you enable the module.
- Quantities are calculated from the BoM: if you ordered 100 coated brackets and the BoM requires 1 raw bracket + 0.05 kg powder each, the delivery order contains 100 raw brackets + 5 kg of powder.
- The delivery order is linked to the PO, so you can trace which components went out for which order.
# Create a Purchase Order for subcontracted goods
po = env['purchase.order'].create({
'partner_id': subcontractor.id,
'order_line': [(0, 0, {
'product_id': product_finished.id,
'product_qty': 100.0,
'price_unit': 3.50, # Subcontractor's per-unit fee
})],
})
po.button_confirm()
# Odoo auto-generates the component delivery
# Find the resupply picking linked to this PO
resupply_picking = env['stock.picking'].search([
('origin', 'like', po.name),
('picking_type_code', '=', 'outgoing'),
])
for move in resupply_picking.move_ids:
print(f"Component: {{move.product_id.name}} "
f"Qty: {{move.product_uom_qty}} "
f"{{move.product_uom.name}}"
f" -> {{move.location_dest_id.complete_name}}")
# Output:
# Component: Raw Bracket Qty: 100.0 Units -> Partner Locations / Subcontracting
# Component: Powder Red 1kg Qty: 5.0 kg -> Partner Locations / SubcontractingManual Resupply (Pre-Positioned Stock)
Some manufacturers pre-position bulk stock at the subcontractor's facility — for example, sending a pallet of 10,000 raw brackets that the subcontractor draws from over several months. In this case:
- Create a manual delivery order to move components to the subcontractor's location (use the Resupply Subcontractor operation type).
- When you confirm POs later, Odoo sees that components are already at the subcontractor's location and skips the automatic resupply.
- The system deducts from the subcontractor's on-hand stock as finished goods are received.
If the subcontractor's location doesn't have enough components to fulfill a PO, Odoo generates a partial resupply for the missing quantity. This means you can mix both models — keep a buffer at the subcontractor and let Odoo top it up as needed. Monitor the subcontractor's location inventory via Inventory → Reporting → Inventory Report filtered by location.
Receiving Finished Goods from the Subcontractor: Valuation and Cost Breakdown
When the subcontractor ships the finished products back to you, the receipt triggers a chain of inventory and accounting movements. Understanding this chain is critical for correct cost of goods sold (COGS) reporting.
What Happens on Receipt Validation
When you validate the receipt picking:
- Finished goods enter your warehouse — a stock move from the subcontractor's location to your input (or stock) location.
- Components are consumed — Odoo creates an internal production order behind the scenes that consumes the components at the subcontractor's location. This is the key mechanism that maintains traceability.
- A subcontracting production order is recorded, linking the consumed components to the finished product via lot/serial numbers.
- Valuation entries post — the finished product's value includes the component cost plus the subcontractor's service fee from the PO line.
Cost Structure of a Subcontracted Product
| Cost Element | Source | Example (per bracket) |
|---|---|---|
| Raw Material | Component cost from inventory valuation | $1.20 (raw bracket) |
| Consumable Material | Component cost from inventory valuation | $0.35 (50g powder coat) |
| Service Fee | PO line unit price | $3.50 (subcontractor's charge) |
| Total Product Cost | Sum of all elements | $5.05 |
# After validating the receipt picking, find the subcontracting MO
receipt = env['stock.picking'].browse(receipt_picking_id)
receipt.button_validate()
# The subcontracting production order is linked via the group
production_orders = env['mrp.production'].search([
('picking_ids', 'in', receipt.id),
])
for mo in production_orders:
print(f"MO: {{mo.name}}")
print(f"Product: {{mo.product_id.name}} x {{mo.product_qty}}")
print(f"State: {{mo.state}}")
for move in mo.move_raw_ids:
print(f" Consumed: {{move.product_id.name}} "
f"x {{move.quantity}} {{move.product_uom.name}}")
if move.lot_ids:
print(f" Lots: {{', '.join(move.lot_ids.mapped('name'))}}")Valuation Method Impact
The valuation method you choose for the finished product affects how costs flow:
| Valuation Method | Behavior on Subcontracting Receipt | Best For |
|---|---|---|
| Standard Price | Product enters at the standard cost. Difference between actual cost (components + service) and standard cost goes to a price difference account. | Stable pricing, large volume |
| FIFO | Product enters at the actual cost of the specific components consumed + service fee. Each receipt layer has its real cost. | Fluctuating material prices |
| AVCO | Product enters at actual cost, then the average cost of all units on hand is recalculated. | General-purpose, moderate price swings |
If you incur freight, customs duties, or handling fees to ship components to the subcontractor or receive finished goods back, use Odoo's Landed Costs feature to allocate these expenses to the finished product. Navigate to Inventory → Operations → Landed Costs, link it to the receipt picking, and choose an allocation method (by quantity, weight, or value). This ensures your product cost reflects the true total cost of subcontracting.
Quality Checks on Subcontracted Products: Inspections Before They Hit Your Shelves
Subcontracted products are the highest-risk items in your inventory. You don't control the process, you can't walk the floor, and defects aren't visible until they reach your warehouse — or worse, your customer. Odoo 19's mrp_subcontracting_quality module lets you define mandatory quality checks that trigger automatically when you receive subcontracted goods.
Setting Up Quality Control Points for Subcontracting
Navigate to Quality → Quality Control → Control Points. Create a control point with these settings:
- Operation: Select Receipts or the specific subcontracting receipt operation type.
- Product: Select the subcontracted finished product (or leave blank for all products).
- Partner (Subcontractor): Filter by vendor to apply different inspection standards per subcontractor.
- Test Type: Choose from Pass/Fail, Measure (with tolerance), Take a Picture, or Instructions.
- Frequency: All operations, randomly, or periodically.
# Create quality control points for subcontracted products
# This ensures every receipt from the subcontractor is inspected
receipt_type = env['stock.picking.type'].search([
('code', '=', 'incoming'),
('warehouse_id.company_id', '=', env.company.id),
], limit=1)
# Visual inspection: coating uniformity
env['quality.point'].create({
'name': 'Coating Uniformity Check',
'title': 'Inspect coating for uniformity, drips, and bare spots',
'product_ids': [(4, product_finished.id)],
'picking_type_ids': [(4, receipt_type.id)],
'test_type_id': env.ref('quality.test_type_passfail').id,
'note': '<p>Reject if: bare metal visible, drip marks > 2mm, '
'color mismatch vs. reference sample RAL 3020</p>',
})
# Dimensional check: bracket width tolerance
measure_test = env.ref('quality.test_type_measure')
env['quality.point'].create({
'name': 'Bracket Width Measurement',
'title': 'Measure bracket width with calipers',
'product_ids': [(4, product_finished.id)],
'picking_type_ids': [(4, receipt_type.id)],
'test_type_id': measure_test.id,
'norm': 50.0, # Target: 50mm
'tolerance_min': 49.8, # Min acceptable
'tolerance_max': 50.2, # Max acceptable
'norm_unit': 'mm',
})
# Photo documentation: required for audit trail
env['quality.point'].create({
'name': 'Receipt Photo Documentation',
'title': 'Photograph a sample from each batch',
'product_ids': [(4, product_finished.id)],
'picking_type_ids': [(4, receipt_type.id)],
'test_type_id': env.ref('quality.test_type_picture').id,
'note': '<p>Take a clear photo of 3 random samples per batch. '
'Include the lot number label in the frame.</p>',
})What Happens When a Quality Check Fails
When an inspector marks a quality check as Failed:
- The receipt picking is blocked — the goods cannot enter your stock until the issue is resolved.
- A quality alert is created automatically, visible in the Quality dashboard.
- The alert can be assigned to a team, linked to the subcontractor, and escalated if not resolved within a deadline.
- You can choose to reject the batch (return to subcontractor), accept with deviation (proceed with a note), or quarantine (move to a quarantine location for further inspection).
<odoo>
<data noupdate="1">
<record id="mail_template_quality_alert_subcontract"
model="mail.template">
<field name="name">Subcontracting Quality Alert</field>
<field name="model_id" ref="quality.model_quality_alert"/>
<field name="subject">Quality Alert: {{object.name}}
- {{object.product_id.name}}</field>
<field name="email_from">quality@yourcompany.com</field>
<field name="email_to">{{object.partner_id.email}}</field>
<field name="body_html" type="html">
<p>Dear {{object.partner_id.name}},</p>
<p>A quality issue was detected on receipt of the following product:</p>
<ul>
<li><strong>Product:</strong> {{object.product_id.display_name}}</li>
<li><strong>Lot/Serial:</strong> {{object.lot_id.name or 'N/A'}}</li>
<li><strong>Issue:</strong> {{object.description}}</li>
</ul>
<p>Please provide a corrective action plan within 48 hours.</p>
</field>
</record>
</data>
</odoo> Create a dedicated Quarantine location under your warehouse (e.g., WH/Quarantine). When a quality check fails, configure a putaway rule that routes failed items to this location instead of stock. This prevents defective subcontracted goods from being picked for customer orders while you negotiate returns or rework with the subcontractor.
Subcontracting Reports and Dashboards: Tracking Cost, Lead Time, and Quality
Raw data without reporting is useless. Odoo 19 provides several reporting views for subcontracting, and you can extend them with custom server actions and spreadsheets.
Built-In Reports
| Report | Path | What It Shows |
|---|---|---|
| Subcontracting Orders | Manufacturing → Reporting | All subcontracting production orders with status, dates, and consumed quantities |
| Inventory at Subcontractor | Inventory → Reporting → Inventory | On-hand stock at each subcontractor's location — filter by is_subcontracting_location |
| Product Cost Analysis | Inventory → Reporting → Valuation | Cost layers showing component + service cost breakdown per product |
| Quality Alerts | Quality → Quality Alerts | Open and resolved quality issues by subcontractor, product, and date |
Custom Server Action: Subcontractor Performance Score
This server action calculates a simple performance score for each subcontractor based on on-time delivery and quality pass rate. You can schedule it as a cron job to run weekly.
from datetime import timedelta
# Parameters
LOOKBACK_DAYS = 90
today = fields.Date.today()
cutoff = today - timedelta(days=LOOKBACK_DAYS)
# Find all subcontracting production orders in the period
productions = env['mrp.production'].search([
('date_start', '>=', cutoff),
('bom_id.type', '=', 'subcontract'),
('state', '=', 'done'),
])
# Group by subcontractor (vendor on the linked PO)
from collections import defaultdict
scores = defaultdict(lambda: {
'total': 0, 'on_time': 0,
'quality_pass': 0, 'quality_total': 0
})
for mo in productions:
# Get the linked purchase order
po_line = mo.move_finished_ids.mapped(
'purchase_line_id'
)
if not po_line:
continue
vendor = po_line[0].partner_id
scores[vendor]['total'] += 1
# On-time: finished before or on the scheduled date
if mo.date_finished and mo.date_start:
planned_end = mo.date_start + timedelta(
days=po_line[0].order_id.partner_id
.property_supplier_payment_term_id
.line_ids[:1].days or 14
)
if mo.date_finished.date() <= planned_end.date():
scores[vendor]['on_time'] += 1
# Quality pass rate
alerts = env['quality.alert'].search([
('create_date', '>=', cutoff),
])
for alert in alerts:
vendor = alert.partner_id
if vendor in scores:
scores[vendor]['quality_total'] += 1
if alert.stage_id.done:
scores[vendor]['quality_pass'] += 1
# Log results
for vendor, data in scores.items():
otd = (data['on_time'] / data['total'] * 100) if data['total'] else 0
qpr = (
(1 - data['quality_total'] / data['total']) * 100
) if data['total'] else 100
log(
f"{{vendor.name}}: "
f"OTD={{otd:.1f}}%, "
f"Quality={{qpr:.1f}}%, "
f"Orders={{data['total']}}"
)Odoo 19 Enterprise includes a spreadsheet integration. Create a pivot table from the subcontracting production orders, add columns for subcontractor name, lead time (date finished minus date started), component cost, and service cost. Share it as a dashboard with your procurement and quality teams. This is far more flexible than building custom reports in Python.
5 Subcontracting Gotchas That Break Traceability and Costing
Forgetting to Set the BoM Type to "Subcontracting"
If you create a normal BoM (type = "Manufacture") and assign a subcontractor, nothing happens. The PO behaves like a regular purchase — no component shipment, no production order on receipt, no traceability. The BoM type must be subcontract. This is the single most common configuration error we see in audits.
After creating any subcontracting BoM, verify by creating a test PO with qty=1. Confirm it and check that a Resupply Subcontractor picking was generated. If not, the BoM type is wrong.
Component UoM Mismatch Between BoM and Purchase
If your BoM specifies powder coat in grams but the product's purchase UoM is kilograms, Odoo converts correctly — but the resupply picking displays quantities in the product's stock UoM, which can confuse warehouse staff. Worse, if UoM categories don't match (e.g., someone uses "Units" instead of "Weight"), the conversion silently fails and the wrong quantity ships to the subcontractor.
Standardize UoMs across BoMs, purchase agreements, and product forms. Use the same UoM category everywhere. Run a SQL audit query periodically: SELECT bom.id, bom_line.product_uom_id, pp.uom_id FROM mrp_bom_line bom_line JOIN product_product pp ON pp.id = bom_line.product_id JOIN mrp_bom bom ON bom.id = bom_line.bom_id WHERE bom.type = 'subcontract' AND bom_line.product_uom_id != pp.uom_id;
Receiving Partial Quantities Without Backorder Handling
Subcontractors often deliver in batches — you ordered 1,000 but receive 600 first, then 400 later. When you validate the receipt with a partial quantity, Odoo asks if you want to create a backorder. If you click No Backorder, the remaining 400 units are cancelled — and the corresponding 400 units worth of components are still sitting at the subcontractor's location with no demand against them.
Always create backorders for partial subcontracting receipts. Train warehouse staff that "No Backorder" means "I don't want the rest." If components get orphaned, run an inventory adjustment on the subcontractor's location to move them back to your warehouse.
Multi-Level Subcontracting Without Proper BoM Nesting
If your finished product requires a semi-finished component that itself is subcontracted (e.g., raw steel → Subcontractor A stamps brackets → Subcontractor B coats them), you need nested subcontracting BoMs. A common mistake is creating a single flat BoM that lists raw steel as a component of the coated bracket. This skips the stamping step entirely, and Odoo ships raw steel directly to the coating subcontractor who can't use it.
Create two separate subcontracting BoMs: one for stamping (raw steel → raw bracket, assigned to Subcontractor A) and one for coating (raw bracket → coated bracket, assigned to Subcontractor B). Use reordering rules or MTO on the semi-finished product to chain the two processes automatically.
Not Reconciling Subcontractor Location Inventory
Over time, the component quantities Odoo shows at the subcontractor's location drift from reality. Scrap, spillage, theft, or miscounts at the subcontractor's facility create phantom inventory that inflates your asset values and leads to production shortages when you least expect them.
Schedule monthly inventory adjustments on the subcontractor's location. Ask the subcontractor for a stock count, then reconcile in Odoo via Inventory → Operations → Physical Inventory filtered to the subcontracting location. Any discrepancies should be investigated and posted as scrap or adjustments with proper reason codes.
What Proper Subcontracting Management Saves Your Business
Subcontracting isn't just a manufacturing workflow — it's a financial and compliance function. Getting it right in your ERP has measurable impact:
Tracking components at the subcontractor's location eliminates "material black holes" — you know exactly what's out, what's been consumed, and what should come back.
Full lot traceability from raw material to subcontracted finished good means you can identify affected batches in minutes, not days. This is a regulatory requirement in food, pharma, and automotive.
Auditors expect you to account for inventory at third-party locations. Odoo's subcontracting location gives you an auditable, real-time ledger of materials outside your four walls.
Beyond direct savings: companies that use proper subcontracting workflows in their ERP negotiate better pricing with subcontractors because they have data. You can show a subcontractor their on-time delivery rate, quality rejection rate, and exact volume over the past year — and use that data to negotiate volume discounts or penalty clauses with confidence.
Optimization Metadata
Complete guide to subcontracting in Odoo 19. Set up subcontractors, create subcontracting BoMs, ship components, receive finished goods with full traceability, and run quality inspections.
1. "Setting Up Subcontractors in Odoo 19: Vendor Configuration and Subcontracting Location"
2. "Creating a Subcontracting Bill of Materials in Odoo 19"
3. "Shipping Components to the Subcontractor: Automatic Resupply and Manual Shipments"
4. "Receiving Finished Goods from the Subcontractor: Valuation and Cost Breakdown"
5. "Quality Checks on Subcontracted Products: Inspections Before They Hit Your Shelves"
6. "5 Subcontracting Gotchas That Break Traceability and Costing"