Subcontracting Order Workflow in ERPNext: Managing Supplied Materials from PO to Receipt
The Subcontracting Order workflow in ERPNext automates the four-phase process of creating orders from Purchase Orders, reserving raw materials via Stock Reservation Entries, receiving finished goods through Subcontracting Receipts, and updating order status based on material consumption and receipt quantities.
The Subcontracting Order (SCO) in frappe/erpnext is the central document that links Purchase Orders to external manufacturers. It tracks raw materials—called supplied items—that you provide to subcontractors while monitoring the service items received back. Understanding this workflow and how to manage supplied materials ensures accurate inventory valuation and prevents stock discrepancies during outsourced production.
Creating a Subcontracting Order from a Purchase Order
The workflow begins when you convert a subcontracted Purchase Order into a Subcontracting Order. In erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py, the validate_purchase_order_for_subcontracting() method ensures the selected PO has is_subcontracted = 1, is submitted, and not fully received.
The populate_items_table() method copies service items from the PO into the SCO's Items table, calculating conversion factors and default Bills of Materials (BOMs). Simultaneously, the set_missing_values() method runs helpers like calculate_service_costs and calculate_supplied_items_qty_and_amount to populate rates and totals.
Key fields initialized during creation include:
purchase_order– Link to the originating POsupplied_items– Child table of typeSubcontracting Order Supplied Itemreserve_stock– Boolean flag triggering automatic reservation
When you submit the SCO, the system calls reserve_raw_materials() (if reserve_stock is checked) and updates the PO's subcontracted_qty via update_subcontracted_quantity_in_po().
Reserving Raw Materials for Subcontractors
When Reserve Stock is enabled, submitting an SCO triggers the reserve_raw_materials() method in subcontracting_order.py. This creates Stock Reservation Entry records in tabStock Reservation Entry for each item in the Supplied Items table:
for item in self.supplied_items:
data = frappe._dict({
"voucher_no": self.name,
"voucher_type": self.doctype,
"voucher_detail_no": item.name,
"item_code": item.rm_item_code,
"warehouse": item.reserve_warehouse,
"stock_qty": item.required_qty,
})
reservation_items.append(data)
These reservations guarantee that the required raw materials remain available in your warehouse until physically transferred to the subcontractor. The reservation is later consumed by the Subcontracting Receipt to reflect actual material usage.
Receiving Finished Goods via Subcontracting Receipt
The Subcontracting Receipt (SCR) records the return of finished goods and consumes the supplied raw materials. Located in erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py, this document performs several critical functions on submission:
-
Validation: The
validate()method checks posting dates, inspection requirements, and ensures the linked SCO remains open. -
Consumption: The
set_consumed_qty_in_subcontract_order()method updates thesupplied_qtyfield in eachSubcontracting Order Supplied Itemrow, deducting the consumed amount from reservations. -
Ledger Updates: The
make_gl_entries()andupdate_stock_ledger()methods generate financial transactions and adjust stock balances for both the consumed raw materials and received finished goods. -
Status Propagation: The receipt triggers
update_status()on the parent SCO to re-evaluate whether the order is partially or fully received.
Automatic Status Management and Transitions
The SCO status updates automatically through the update_status() method in subcontracting_order.py. The logic evaluates both receipt percentages and material transfer status:
if self.docstatus == 1:
if self.per_received >= 100:
status = "Completed"
elif 0 < self.per_received < 100:
status = "Partially Received"
else:
# Check material transfer status
total_required_qty = sum(item.required_qty for item in self.supplied_items)
total_supplied_qty = sum(item.supplied_qty for item in self.supplied_items)
if total_supplied_qty >= total_required_qty:
status = "Material Transferred"
elif total_supplied_qty > 0:
status = "Partial Material Transferred"
else:
status = "Open"
Status flow: Draft → Open → (Partial Material Transferred / Material Transferred) → Partially Received → Completed.
This status controls whether the linked Purchase Order can be modified, protecting against changes that would invalidate material allocations through validations in accounts_controller.py.
Managing Supplied Materials: Validation and Reporting
Each row in the Supplied Items table represents a Subcontracting Order Supplied Item document requiring specific fields: rm_item_code, required_qty, reserve_warehouse, and supplier_warehouse.
The validate_supplied_items() method in subcontracting_order.py enforces that reserve and supplier warehouses differ:
def validate_supplied_items(self):
if self.supplier_warehouse:
for item in self.supplied_items:
if self.supplier_warehouse == item.reserve_warehouse:
frappe.throw(_("Reserve Warehouse must be different from Supplier Warehouse"))
For monitoring material balances, the Subcontract Order Summary report (erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py) displays required versus supplied quantities per item. Query status programmatically:
sco = frappe.get_doc("Subcontracting Order", "SCO-00015")
for si in sco.supplied_items:
print(f"{si.rm_item_code}: required {si.required_qty}, supplied {si.supplied_qty}")
Practical Implementation Examples
Create a Subcontracting Order via REST API
import frappe
sco = frappe.get_doc({
"doctype": "Subcontracting Order",
"purchase_order": "PO-00023",
"supplier": "Supplier XYZ",
"supplier_warehouse": "Supplier WH",
"reserve_stock": 1,
"supplied_items": [
{
"rm_item_code": "RAW-STEEL-01",
"required_qty": 500,
"reserve_warehouse": "Raw Materials WH"
},
{
"rm_item_code": "RAW-PAINT-02",
"required_qty": 200,
"reserve_warehouse": "Paint WH"
},
],
}).insert()
sco.submit() # Triggers reserve_raw_materials()
Record a Subcontracting Receipt
receipt = frappe.get_doc({
"doctype": "Subcontracting Receipt",
"subcontracting_order": "SCO-00015",
"supplier": "Supplier XYZ",
"posting_date": frappe.utils.today(),
"items": [
{
"item_code": "FINISHED-GOOD-01",
"qty": 250,
"rate": 150,
},
],
"supplied_items": [
{
"rm_item_code": "RAW-STEEL-01",
"supplied_qty": 250,
"warehouse": "Raw Materials WH",
},
],
}).insert()
receipt.submit() # Consumes reserved stock and updates SCO status
Manual Reservation When reserve_stock Was Unchecked
sco = frappe.get_doc("Subcontracting Order", "SCO-00015")
sco.reserve_raw_materials()
frappe.db.commit()
Summary
- Subcontracting Orders bridge Purchase Orders and external manufacturers, tracking both service items and raw material supplies through the
subcontracting_order.pycontroller. - The four-phase workflow comprises: Creation (from PO), Reservation (
reserve_raw_materialscreating Stock Reservation Entries), Receipt (Subcontracting Receipt consumption), and Status Closure (update_status). - Stock Reservation Entries created in
tabStock Reservation Entryensure raw materials remain allocated until physically consumed by receipts. - Status transitions depend on both finished goods receipt percentages (
per_received) and supplied material quantities (supplied_qtyvsrequired_qty), preventing premature closure. - Key implementation files include
erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py,subcontracting_receipt.py, anderpnext/controllers/subcontracting_controller.py.
Frequently Asked Questions
How do I link a Subcontracting Order to an existing Purchase Order?
Set the purchase_order field when creating the SCO. The system validates the link through validate_purchase_order_for_subcontracting(), ensuring the PO has is_subcontracted = 1 and is not fully received. Upon SCO submission, the method update_subcontracted_quantity_in_po() automatically updates the subcontracted quantity on the PO to maintain synchronization.
What happens if I don't enable Reserve Stock when creating an SCO?
If reserve_stock is unchecked, the system skips the automatic reserve_raw_materials() call during submission. You must manually trigger reservations later by calling sco.reserve_raw_materials() or manage material transfers through separate Stock Entry documents. Without reservations, no Stock Reservation Entry records are created in tabStock Reservation Entry, risking stock allocation conflicts.
How does ERPNext prevent over-consumption of supplied materials?
The set_consumed_qty_in_subcontract_order() method in subcontracting_receipt.py validates consumption against the required_qty specified in the SCO's Supplied Items table. Additionally, warehouse validation in validate_supplied_items() ensures you cannot reserve and consume from the same supplier warehouse, maintaining separation between your stock and the subcontractor's location.
Can I modify a Purchase Order after creating a Subcontracting Order?
No. The system blocks PO modifications through validations in accounts_controller.py once a Subcontracting Order exists. This prevents discrepancies between ordered quantities and reserved materials. To make changes, you must cancel or amend the SCO first, which restores the ability to edit the PO.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →