From fe25746febc8731bbbc0880dcb18d528dcf08a63 Mon Sep 17 00:00:00 2001 From: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> Date: Sun, 13 Sep 2026 11:58:56 +0530 Subject: [PATCH] fix(subcontracting): validate project across the subcontracting flow (#58965) --- .../subcontracting_order.py | 30 +++++++++++- .../test_subcontracting_order.py | 47 +++++++++++++++++++ .../doctype/subcontracting_receipt/mapper.py | 2 + .../subcontracting_receipt.py | 19 ++++++++ .../test_subcontracting_receipt.py | 24 ++++++++++ 5 files changed, 120 insertions(+), 2 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index a8c6b7785e1..aeed852b01e 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -120,6 +120,7 @@ class SubcontractingOrder(SubcontractingController): self.validate_service_items() self.validate_supplied_items() self.set_missing_values() + self.validate_with_previous_doc() self.reset_default_field_value("set_warehouse", "items", "warehouse") def on_submit(self): @@ -131,6 +132,18 @@ class SubcontractingOrder(SubcontractingController): self.update_status() self.update_subcontracted_quantity_in_po(cancel=True) + def validate_with_previous_doc(self): + super().validate_with_previous_doc( + { + "Purchase Order Item": { + "ref_dn_field": "purchase_order_item", + "compare_fields": [["project", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + } + ) + def validate_purchase_order_for_subcontracting(self): if self.purchase_order: po = frappe.get_doc("Purchase Order", self.purchase_order) @@ -242,10 +255,22 @@ class SubcontractingOrder(SubcontractingController): if si.fg_item: item = frappe.get_doc("Item", si.fg_item) - qty, subcontracted_qty, fg_item_qty, production_plan_sub_assembly_item = frappe.db.get_value( + ( + qty, + subcontracted_qty, + fg_item_qty, + production_plan_sub_assembly_item, + project, + ) = frappe.db.get_value( "Purchase Order Item", si.purchase_order_item, - ["qty", "subcontracted_qty", "fg_item_qty", "production_plan_sub_assembly_item"], + [ + "qty", + "subcontracted_qty", + "fg_item_qty", + "production_plan_sub_assembly_item", + "project", + ], ) available_qty = flt(qty) - flt(subcontracted_qty) @@ -282,6 +307,7 @@ class SubcontractingOrder(SubcontractingController): "material_request": si.material_request, "material_request_item": si.material_request_item, "production_plan_sub_assembly_item": production_plan_sub_assembly_item, + "project": project, } ) else: diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 9c936c5aff4..0bd2ba23873 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -25,6 +25,7 @@ from erpnext.controllers.tests.test_subcontracting_controller import ( set_backflush_based_on, ) from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.projects.doctype.project.test_project import make_project from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( @@ -112,6 +113,24 @@ class TestSubcontractingOrder(ERPNextTestSuite): sco.load_from_db() self.assertEqual(sco.status, "Partially Received") + def test_project_is_carried_over_from_purchase_order(self): + project = make_project({"project_name": "_Test SCO Project"}).name + po = make_subcontracted_purchase_order(project) + + sco = get_mapped_subcontracting_order(source_name=po.name) + + self.assertEqual(sco.project, project) + self.assertEqual(sco.items[0].project, project) + + def test_project_cannot_differ_from_purchase_order(self): + project = make_project({"project_name": "_Test SCO Project"}).name + other_project = make_project({"project_name": "_Test SCO Project 2"}).name + po = make_subcontracted_purchase_order(project) + + sco = get_mapped_subcontracting_order(source_name=po.name) + sco.items[0].project = other_project + self.assertRaises(frappe.ValidationError, sco.save) + def test_sco_requires_a_subcontracting_purchase_order(self): sco = get_subcontracting_order(do_not_save=1) sco.purchase_order = None @@ -1057,3 +1076,31 @@ def create_subcontracting_order(**args): sco.submit() return sco + + +def make_subcontracted_purchase_order(project): + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 7", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA7", + "fg_item_qty": 10, + }, + ] + po = create_purchase_order( + rm_items=service_items, + is_subcontracted=1, + supplier_warehouse="_Test Warehouse 1 - _TC", + do_not_submit=1, + ) + po.project = project + for item in po.items: + item.project = project + po.save() + po.submit() + + return po diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py b/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py index 8d1f078db1f..ae1b7653d4f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py @@ -168,5 +168,7 @@ def add_po_items_to_pr(scr_doc, target_doc): "warehouse": item.warehouse, "purchase_order": item.parent, "purchase_order_item": item.name, + "project": item.project, + "cost_center": item.cost_center, }, ) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 248f959162c..81cc23a7ce4 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -148,6 +148,7 @@ class SubcontractingReceipt(SubcontractingController): super().validate() self.set_missing_values() + self.validate_with_previous_doc() # after set_missing_values, so the secondary rates are computed from the same # calculated per-qty costs the Get Secondary Items button uses @@ -167,6 +168,24 @@ class SubcontractingReceipt(SubcontractingController): self.set_supplied_items_cost_center() self.set_supplied_items_inventory_dimensions() + def validate_with_previous_doc(self): + super().validate_with_previous_doc( + { + "Subcontracting Order Item": { + "ref_dn_field": "subcontracting_order_item", + "compare_fields": [["project", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + "Purchase Order Item": { + "ref_dn_field": "purchase_order_item", + "compare_fields": [["project", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + } + ) + def on_submit(self): self.validate_closed_subcontracting_order() self.validate_bom_required_qty() diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index a86faa7e91c..705508de828 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -27,6 +27,7 @@ from erpnext.controllers.tests.test_subcontracting_controller import ( set_backflush_based_on, ) from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.projects.doctype.project.test_project import make_project from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( @@ -40,6 +41,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( make_subcontracting_receipt, ) +from erpnext.subcontracting.doctype.subcontracting_order.test_subcontracting_order import ( + make_subcontracted_purchase_order, +) from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import ( BOMQuantityError, ) @@ -53,6 +57,26 @@ class TestSubcontractingReceipt(ERPNextTestSuite): make_service_items() make_bom_for_subcontracted_items() + def test_project_is_carried_over_from_subcontracting_order(self): + project = make_project({"project_name": "_Test SCR Project"}).name + po = make_subcontracted_purchase_order(project) + sco = get_subcontracting_order(po_name=po.name) + + scr = make_subcontracting_receipt(sco.name) + + self.assertEqual(scr.project, project) + self.assertEqual(scr.items[0].project, project) + + def test_project_cannot_differ_from_subcontracting_order(self): + project = make_project({"project_name": "_Test SCR Project"}).name + other_project = make_project({"project_name": "_Test SCR Project 2"}).name + po = make_subcontracted_purchase_order(project) + sco = get_subcontracting_order(po_name=po.name) + + scr = make_subcontracting_receipt(sco.name) + scr.items[0].project = other_project + self.assertRaises(frappe.ValidationError, scr.save) + def test_subcontracting(self): set_backflush_based_on("BOM") make_stock_entry(item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100)