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 PO
  • supplied_items – Child table of type Subcontracting Order Supplied Item
  • reserve_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:

  1. Validation: The validate() method checks posting dates, inspection requirements, and ensures the linked SCO remains open.

  2. Consumption: The set_consumed_qty_in_subcontract_order() method updates the supplied_qty field in each Subcontracting Order Supplied Item row, deducting the consumed amount from reservations.

  3. Ledger Updates: The make_gl_entries() and update_stock_ledger() methods generate financial transactions and adjust stock balances for both the consumed raw materials and received finished goods.

  4. 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: DraftOpen → (Partial Material Transferred / Material Transferred) → Partially ReceivedCompleted.

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.py controller.
  • The four-phase workflow comprises: Creation (from PO), Reservation (reserve_raw_materials creating Stock Reservation Entries), Receipt (Subcontracting Receipt consumption), and Status Closure (update_status).
  • Stock Reservation Entries created in tabStock Reservation Entry ensure 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_qty vs required_qty), preventing premature closure.
  • Key implementation files include erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py, subcontracting_receipt.py, and erpnext/controllers/subcontracting_controller.py.

Frequently Asked Questions

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →