From 48d49cdcd2ebe780d12269f0152c624316dea49a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 07:47:54 +0530 Subject: [PATCH 01/10] fix(subcontracting): fix format placeholders in FG warehouse validation message `validate_manufacture` builds its "Target Warehouse for Finished Good must be same as Finished Good Warehouse ..." message with placeholders `{1}` and `{2}`, but only passes two positional args (indices 0 and 1). `str.format` raises `IndexError: Replacement index 2 out of range` instead of rendering the message, so a user who sets the wrong FG target warehouse gets an opaque traceback rather than the intended validation error. Renumber the placeholders to `{0}` and `{1}` to match the args. --- erpnext/controllers/subcontracting_inward_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index 4907f2d8484..5bdca5528e8 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -230,7 +230,7 @@ class SubcontractingInwardController: ): frappe.throw( _( - "Target Warehouse for Finished Good must be same as Finished Good Warehouse {1} in Work Order {2} linked to the Subcontracting Inward Order." + "Target Warehouse for Finished Good must be same as Finished Good Warehouse {0} in Work Order {1} linked to the Subcontracting Inward Order." ).format( get_link_to_form("Warehouse", fg_warehouse), get_link_to_form("Work Order", self.work_order), From 8fc7cb0117e6a7f4b31d7ab2d2788fbb3c5bb6a7 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 07:48:08 +0530 Subject: [PATCH 02/10] refactor(subcontracting): drop unused format arg in overconsumption message The "exceeds quantity available" throw in `validate_manufacture` passes a third positional arg (`item.transfer_qty`), but the message only has `{0}` and `{1}` placeholders, so `str.format` silently discards it. Remove the dead argument; no behaviour change. --- erpnext/controllers/subcontracting_inward_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index 5bdca5528e8..e4f1ffdacff 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -287,7 +287,7 @@ class SubcontractingInwardController: frappe.throw( _( "Row #{0}: Customer Provided Item {1} exceeds quantity available through Subcontracting Inward Order" - ).format(item.idx, get_link_to_form("Item", item.item_code), item.transfer_qty) + ).format(item.idx, get_link_to_form("Item", item.item_code)) ) elif item.s_warehouse != customer_warehouse: frappe.throw( From 57f5186dffdd9a027bc3d73769c62624e624bbce Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 07:48:33 +0530 Subject: [PATCH 03/10] refactor(subcontracting): hoist ValueWrapper import to module level `validate_delivery_on_save` imported `pypika.terms.ValueWrapper` inside its per-item loop, re-running the import on every iteration. Move it to the module-level imports. --- erpnext/controllers/subcontracting_inward_controller.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index e4f1ffdacff..92717975615 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -4,6 +4,7 @@ import frappe from frappe import _, bold from frappe.query_builder import Case from frappe.utils import flt, get_link_to_form +from pypika.terms import ValueWrapper from erpnext.stock.serial_batch_bundle import get_serial_batch_list_from_item @@ -500,8 +501,6 @@ class SubcontractingInwardController: ) ) - from pypika.terms import ValueWrapper - table = frappe.qb.DocType("Subcontracting Inward Order Item") query = ( frappe.qb.from_(table) From a342db38dea370df9f23af1bf126b3030db12909 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 07:48:46 +0530 Subject: [PATCH 04/10] refactor(subcontracting): drop redundant scio_item_name check In `update_inward_order_item`, the walrus assignment `scio_item_name :=` is already part of the truthy `if` condition, so the nested `if scio_item_name:` is always true. Remove it and dedent the body. --- erpnext/controllers/subcontracting_inward_controller.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index 92717975615..5c6ce75d8d3 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -645,10 +645,9 @@ class SubcontractingInwardController: "Work Order", self.work_order, "subcontracting_inward_order_item" ) ): - if scio_item_name: - frappe.get_doc( - "Subcontracting Inward Order Item", scio_item_name - ).update_manufacturing_qty_fields() + frappe.get_doc( + "Subcontracting Inward Order Item", scio_item_name + ).update_manufacturing_qty_fields() elif self.purpose in ["Subcontracting Delivery", "Subcontracting Return"]: fieldname = "delivered_qty" if self.purpose == "Subcontracting Delivery" else "returned_qty" qty_map = defaultdict(lambda: defaultdict(float)) From e422c4d2ab0191c787b3f4ecb9d2f8fefec24dab Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 07:49:30 +0530 Subject: [PATCH 05/10] perf(subcontracting): hoist Work Order Item lookup out of transfer loop `validate_material_transfer` ran the `Work Order Item` query and rebuilt `wo_item_dict` inside the per-item loop, even though both depend only on `self.work_order`. For an entry with N customer-provided rows that meant N identical queries. Build the lookup once before the loop. `validate_manufacture` already builds the analogous dict once up front, so this also aligns the two methods. --- .../subcontracting_inward_controller.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index 5c6ce75d8d3..ccc902b1d28 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -162,6 +162,23 @@ class SubcontractingInwardController: customer_warehouse = frappe.get_cached_value( "Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse" ) + work_order_items = frappe.get_all( + "Work Order Item", + {"parent": self.work_order, "docstatus": 1, "is_customer_provided_item": 1}, + ["item_code", "transferred_qty", "required_qty", "stock_reserved_qty"], + ) + wo_item_dict = frappe._dict( + { + wo_item.item_code: frappe._dict( + { + "transferred_qty": wo_item.transferred_qty, + "required_qty": wo_item.required_qty, + "stock_reserved_qty": wo_item.stock_reserved_qty, + } + ) + for wo_item in work_order_items + } + ) item_codes = [] for item in self.items: if not frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item"): @@ -184,23 +201,6 @@ class SubcontractingInwardController: ) ) else: - work_order_items = frappe.get_all( - "Work Order Item", - {"parent": self.work_order, "docstatus": 1, "is_customer_provided_item": 1}, - ["item_code", "transferred_qty", "required_qty", "stock_reserved_qty"], - ) - wo_item_dict = frappe._dict( - { - wo_item.item_code: frappe._dict( - { - "transferred_qty": wo_item.transferred_qty, - "required_qty": wo_item.required_qty, - "stock_reserved_qty": wo_item.stock_reserved_qty, - } - ) - for wo_item in work_order_items - } - ) if wo_item := wo_item_dict.get(item.item_code): if wo_item.transferred_qty + item.transfer_qty > max( wo_item.required_qty, wo_item.stock_reserved_qty From 82188757333a73bb9defeb8b31d51742f129806a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 07:49:57 +0530 Subject: [PATCH 06/10] refactor(subcontracting): fetch customer_warehouse only when needed In `validate_manufacture`, `customer_warehouse` is read only inside the `skip_transfer` branch but was fetched unconditionally, wasting a lookup on the non-skip-transfer path. Move it inside the branch that uses it. --- erpnext/controllers/subcontracting_inward_controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index ccc902b1d28..622a9d26eee 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -247,10 +247,10 @@ class SubcontractingInwardController: and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item") ] - customer_warehouse = frappe.get_cached_value( - "Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse" - ) if frappe.get_cached_value("Work Order", self.work_order, "skip_transfer"): + customer_warehouse = frappe.get_cached_value( + "Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse" + ) table = frappe.qb.DocType("Subcontracting Inward Order Received Item") query = ( frappe.qb.from_(table) From 63c5dccb4bfb906945c1354c4cf30b15165d00a2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 07:50:20 +0530 Subject: [PATCH 07/10] fix(subcontracting): guard empty raw-material list before strict zip `update_inward_order_received_items_for_manufacture` unpacks `zip(*item_code_wh.keys(), strict=True)`. When the manufacture entry has no raw-material rows (all rows are finished/secondary/scrap), `item_code_wh` is empty and the unpack raises `ValueError: not enough values to unpack`. Return early when there are no such rows, mirroring the `if secondary_items:` guard already present in `update_inward_order_secondary_items`. --- erpnext/controllers/subcontracting_inward_controller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index 622a9d26eee..3c2dcf8f4e0 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -793,6 +793,9 @@ class SubcontractingInwardController: for item in self.items if not item.is_finished_item and not item.secondary_item_type and not item.is_legacy_scrap_item ] + if not items: + return + item_code_wh = frappe._dict( { ( From 19466b24b04e1a22859a4e47df58f5128cf04bdd Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 07:53:15 +0530 Subject: [PATCH 08/10] perf(subcontracting): compute child idx once per insert loop Each new child row was given `idx=frappe.db.count(...) + 1`, issuing a count query per inserted row across three insert loops (received items on receipt, self-procured RM on manufacture, secondary items on manufacture). Compute the starting index once before each loop and increment a local counter, producing the same idx sequence with a single count query. --- .../subcontracting_inward_controller.py | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index 3c2dcf8f4e0..32eb7e46fd6 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -698,6 +698,13 @@ class SubcontractingInwardController: def update_inward_order_received_items_for_raw_materials_receipt(self): data = frappe._dict() + next_received_idx = ( + frappe.db.count( + "Subcontracting Inward Order Received Item", + {"parent": self.subcontracting_inward_order}, + ) + + 1 + ) for item in self.items: if item.scio_detail: data[item.scio_detail] = frappe._dict( @@ -709,11 +716,7 @@ class SubcontractingInwardController: parent=self.subcontracting_inward_order, parenttype="Subcontracting Inward Order", parentfield="received_items", - idx=frappe.db.count( - "Subcontracting Inward Order Received Item", - {"parent": self.subcontracting_inward_order}, - ) - + 1, + idx=next_received_idx, rm_item_code=item.item_code, stock_uom=item.stock_uom, warehouse=item.t_warehouse, @@ -732,6 +735,7 @@ class SubcontractingInwardController: scio_rm.flags.skip_docstatus_validation = True scio_rm.insert() scio_rm.submit() + next_received_idx += 1 item.db_set("scio_detail", scio_rm.name) if data: @@ -863,6 +867,13 @@ class SubcontractingInwardController: ) main_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code + next_received_idx = ( + frappe.db.count( + "Subcontracting Inward Order Received Item", + {"parent": self.subcontracting_inward_order}, + ) + + 1 + ) for extra_item in [ item for item in items @@ -875,11 +886,7 @@ class SubcontractingInwardController: parent=self.subcontracting_inward_order, parenttype="Subcontracting Inward Order", parentfield="received_items", - idx=frappe.db.count( - "Subcontracting Inward Order Received Item", - {"parent": self.subcontracting_inward_order}, - ) - + 1, + idx=next_received_idx, main_item_code=main_item_code, rm_item_code=extra_item.item_code, stock_uom=extra_item.stock_uom, @@ -894,6 +901,7 @@ class SubcontractingInwardController: doc.flags.skip_docstatus_validation = True doc.insert() doc.submit() + next_received_idx += 1 def update_inward_order_secondary_items(self): if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture": @@ -956,6 +964,9 @@ class SubcontractingInwardController: ) fg_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code + next_secondary_idx = ( + frappe.db.count("Subcontracting Inward Order Secondary Item", {"parent": scio}) + 1 + ) for secondary_item in [ item for item in secondary_items_list @@ -966,8 +977,7 @@ class SubcontractingInwardController: parent=scio, parenttype="Subcontracting Inward Order", parentfield="secondary_items", - idx=frappe.db.count("Subcontracting Inward Order Secondary Item", {"parent": scio}) - + 1, + idx=next_secondary_idx, item_code=secondary_item.item_code, fg_item_code=fg_item_code, stock_uom=secondary_item.stock_uom, @@ -982,6 +992,7 @@ class SubcontractingInwardController: doc.flags.skip_docstatus_validation = True doc.insert() doc.submit() + next_secondary_idx += 1 def cancel_stock_reservation_entries_for_inward(self): if self.purpose == "Receive from Customer": From 23f1fc62353293618675622fcfb34135f5fa142c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 07:53:28 +0530 Subject: [PATCH 09/10] refactor(subcontracting): use f-string for fg reference search filter `get_fg_reference_names` built its LIKE filter with old-style `"%%%s%%" % txt`. Use an f-string (`f"%{txt}%"`) for readability; the value is still passed as a parameterised filter, so behaviour is unchanged. --- erpnext/controllers/subcontracting_inward_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index 32eb7e46fd6..e6a9e06aed5 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -1163,7 +1163,7 @@ def get_fg_reference_names( "Subcontracting Inward Order Item", limit_start=start, limit_page_length=page_len, - filters={"parent": filters.get("parent"), "item_code": ("like", "%%%s%%" % txt), "docstatus": 1}, + filters={"parent": filters.get("parent"), "item_code": ("like", f"%{txt}%"), "docstatus": 1}, fields=["name", "item_code", "delivery_warehouse"], as_list=True, order_by="idx", From b129daedc84f06e19aada3e82a76b5d4cf1ad9b3 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 08:09:09 +0530 Subject: [PATCH 10/10] refactor(subcontracting): dedup validate_manufacture consumption checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `skip_transfer` and transfer branches of `validate_manufacture` ran the same per-item validation loop — look the row up or throw "not a part of", check overconsumption, guard against duplicates, record — differing only in the data source (SCIO Received Item vs Work Order Item), the available-qty basis, the source-warehouse check (skip_transfer only) and the message text. Split each branch into a small method that builds a normalised `{item_code: {consumed_qty, available_qty}}` lookup, and share the loop via `_validate_customer_provided_consumption`. Branch-specific throw messages are passed as callbacks so the user-facing strings (and their translations) are unchanged, and the order in which checks fire is preserved. Also drops the unused `name` column from the skip_transfer query. Adds a test for the non-skip-transfer manufacture flow (Material Transfer for Manufacture -> Manufacture), which exercises the Work Order branch that the existing suite — all of whose manufacture tests set skip_transfer=1 — never covered. Full subcontracting-inward suite passes on MariaDB and PostgreSQL. --- .../subcontracting_inward_controller.py | 227 +++++++++--------- .../test_subcontracting_inward_order.py | 25 ++ 2 files changed, 142 insertions(+), 110 deletions(-) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index e6a9e06aed5..892e5767adc 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -248,125 +248,132 @@ class SubcontractingInwardController: ] if frappe.get_cached_value("Work Order", self.work_order, "skip_transfer"): - customer_warehouse = frappe.get_cached_value( - "Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse" + self._validate_manufacture_consumption_against_scio(items) + else: + self._validate_manufacture_consumption_against_work_order(items) + + def _validate_manufacture_consumption_against_scio(self, items): + customer_warehouse = frappe.get_cached_value( + "Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse" + ) + table = frappe.qb.DocType("Subcontracting Inward Order Received Item") + query = ( + frappe.qb.from_(table) + .select( + table.rm_item_code, + table.consumed_qty, + (table.received_qty - table.returned_qty).as_("available_qty"), ) - table = frappe.qb.DocType("Subcontracting Inward Order Received Item") - query = ( - frappe.qb.from_(table) - .select( - table.rm_item_code, - (table.received_qty - table.returned_qty).as_("total_qty"), - table.consumed_qty, - table.name, - ) - .where( - (table.docstatus == 1) - & (table.parent == self.subcontracting_inward_order) - & ( - table.reference_name - == frappe.get_cached_value( - "Work Order", self.work_order, "subcontracting_inward_order_item" - ) + .where( + (table.docstatus == 1) + & (table.parent == self.subcontracting_inward_order) + & ( + table.reference_name + == frappe.get_cached_value( + "Work Order", self.work_order, "subcontracting_inward_order_item" ) - & (table.rm_item_code.isin([item.item_code for item in items])) ) + & (table.rm_item_code.isin([item.item_code for item in items])) ) - rm_item_dict = frappe._dict( - { - d.rm_item_code: frappe._dict( - {"name": d.name, "total_qty": d.total_qty, "qty": d.consumed_qty} - ) - for d in query.run(as_dict=True) - } + ) + lookup = { + d.rm_item_code: frappe._dict(consumed_qty=d.consumed_qty, available_qty=d.available_qty) + for d in query.run(as_dict=True) + } + + def on_missing(item): + frappe.throw( + _( + "Row #{0}: Customer Provided Item {1} is not a part of Subcontracting Inward Order {2}" + ).format( + item.idx, + get_link_to_form("Item", item.item_code), + get_link_to_form("Subcontracting Inward Order", self.subcontracting_inward_order), + ) ) - item_codes = [] - for item in items: - if rm := rm_item_dict.get(item.item_code): - if rm.qty + item.transfer_qty > rm.total_qty: - frappe.throw( - _( - "Row #{0}: Customer Provided Item {1} exceeds quantity available through Subcontracting Inward Order" - ).format(item.idx, get_link_to_form("Item", item.item_code)) - ) - elif item.s_warehouse != customer_warehouse: - frappe.throw( - _( - "Row #{0}: For Customer Provided Item {1}, Source Warehouse must be {2}" - ).format( - item.idx, - get_link_to_form("Item", item.item_code), - get_link_to_form("Warehouse", customer_warehouse), - ) - ) - elif item.item_code in item_codes: - frappe.throw( - _( - "Row #{0}: Customer Provided Item {1} cannot be added multiple times in the Subcontracting Inward process." - ).format( - item.idx, - get_link_to_form("Item", item.item_code), - ) - ) - else: - item_codes.append(item.item_code) - else: + def on_overconsumption(item): + frappe.throw( + _( + "Row #{0}: Customer Provided Item {1} exceeds quantity available through Subcontracting Inward Order" + ).format(item.idx, get_link_to_form("Item", item.item_code)) + ) + + def check_source_warehouse(item): + if item.s_warehouse != customer_warehouse: + frappe.throw( + _("Row #{0}: For Customer Provided Item {1}, Source Warehouse must be {2}").format( + item.idx, + get_link_to_form("Item", item.item_code), + get_link_to_form("Warehouse", customer_warehouse), + ) + ) + + self._validate_customer_provided_consumption( + items, lookup, on_missing, on_overconsumption, check_source_warehouse + ) + + def _validate_manufacture_consumption_against_work_order(self, items): + work_order_items = frappe.get_all( + "Work Order Item", + {"parent": self.work_order, "docstatus": 1, "is_customer_provided_item": 1}, + ["item_code", "transferred_qty", "consumed_qty"], + ) + lookup = { + wo_item.item_code: frappe._dict( + consumed_qty=wo_item.consumed_qty, available_qty=wo_item.transferred_qty + ) + for wo_item in work_order_items + } + + def on_missing(item): + frappe.throw( + _("Row #{0}: Customer Provided Item {1} is not a part of Work Order {2}").format( + item.idx, + get_link_to_form("Item", item.item_code), + get_link_to_form("Work Order", self.work_order), + ) + ) + + def on_overconsumption(item): + frappe.throw( + _( + "Row #{0}: Overconsumption of Customer Provided Item {1} against Work Order {2} is not allowed in the Subcontracting Inward process." + ).format( + item.idx, + get_link_to_form("Item", item.item_code), + get_link_to_form("Work Order", self.work_order), + ) + ) + + self._validate_customer_provided_consumption(items, lookup, on_missing, on_overconsumption) + + def _validate_customer_provided_consumption( + self, items, lookup, on_missing, on_overconsumption, extra_check=None + ): + """Shared per-item guard for the skip-transfer and transfer manufacture paths. + + `lookup` maps item_code -> {consumed_qty, available_qty}; the branch-specific + throw messages are supplied as callbacks. `extra_check` runs an extra per-item + validation (the source-warehouse check on the skip-transfer path). + """ + seen = [] + for item in items: + record = lookup.get(item.item_code) + if not record: + on_missing(item) + elif record.consumed_qty + item.transfer_qty > record.available_qty: + on_overconsumption(item) + else: + if extra_check: + extra_check(item) + if item.item_code in seen: frappe.throw( _( - "Row #{0}: Customer Provided Item {1} is not a part of Subcontracting Inward Order {2}" - ).format( - item.idx, - get_link_to_form("Item", item.item_code), - get_link_to_form("Subcontracting Inward Order", self.subcontracting_inward_order), - ) - ) - else: - work_order_items = frappe.get_all( - "Work Order Item", - {"parent": self.work_order, "docstatus": 1, "is_customer_provided_item": 1}, - ["item_code", "transferred_qty", "consumed_qty"], - ) - wo_item_dict = frappe._dict( - { - wo_item.item_code: frappe._dict( - {"transferred_qty": wo_item.transferred_qty, "consumed_qty": wo_item.consumed_qty} - ) - for wo_item in work_order_items - } - ) - item_codes = [] - for item in items: - if wo_item := wo_item_dict.get(item.item_code): - if wo_item.consumed_qty + item.transfer_qty > wo_item.transferred_qty: - frappe.throw( - _( - "Row #{0}: Overconsumption of Customer Provided Item {1} against Work Order {2} is not allowed in the Subcontracting Inward process." - ).format( - item.idx, - get_link_to_form("Item", item.item_code), - get_link_to_form("Work Order", self.work_order), - ) - ) - elif item.item_code in item_codes: - frappe.throw( - _( - "Row #{0}: Customer Provided Item {1} cannot be added multiple times in the Subcontracting Inward process." - ).format( - item.idx, - get_link_to_form("Item", item.item_code), - ) - ) - else: - item_codes.append(item.item_code) - else: - frappe.throw( - _("Row #{0}: Customer Provided Item {1} is not a part of Work Order {2}").format( - item.idx, - get_link_to_form("Item", item.item_code), - get_link_to_form("Work Order", self.work_order), - ) + "Row #{0}: Customer Provided Item {1} cannot be added multiple times in the Subcontracting Inward process." + ).format(item.idx, get_link_to_form("Item", item.item_code)) ) + seen.append(item.item_code) def set_allow_zero_valuation_rate(self): if self.subcontracting_inward_order: diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py b/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py index 4e8c62a62f4..e7c5da910a4 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py @@ -330,6 +330,31 @@ class IntegrationTestSubcontractingInwardOrder(ERPNextTestSuite): self.assertEqual(scio.items[0].delivered_qty, 2) self.assertEqual(scio.items[0].returned_qty, 1) + def test_manufacture_consumption_validates_against_work_order(self): + """Cover the non-skip-transfer manufacture path, where consumption is validated + against the Work Order's transferred quantity (the Work Order branch of + validate_manufacture).""" + so, scio = create_so_scio() + frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward()).submit() + + scio.reload() + wo = frappe.get_doc("Work Order", scio.make_work_order()[0]) + wo.wip_warehouse = "Work In Progress - _TC" + next( + item for item in wo.required_items if item.item_code == "Self RM" + ).source_warehouse = "Stores - _TC" + wo.submit() + + frappe.new_doc("Stock Entry").update( + make_stock_entry_from_wo(wo.name, "Material Transfer for Manufacture") + ).submit() + + manufacture = frappe.new_doc("Stock Entry").update(make_stock_entry_from_wo(wo.name, "Manufacture")) + manufacture.submit() + + scio.reload() + self.assertEqual(scio.items[0].produced_qty, 5) + @ERPNextTestSuite.change_settings("Selling Settings", {"allow_delivery_of_overproduced_qty": 1}) @ERPNextTestSuite.change_settings( "Manufacturing Settings", {"overproduction_percentage_for_work_order": 20}