From 0095560dc7aefc115513c3c7599e9f3ce7c282f6 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 1/2] fix(subcontracting): validate project across the subcontracting flow (#58965) (cherry picked from commit fe25746febc8731bbbc0880dcb18d528dcf08a63) # Conflicts: # erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py # erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py # erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py --- .../subcontracting_order.py | 30 ++- .../test_subcontracting_order.py | 108 +++++++++++ .../doctype/subcontracting_receipt/mapper.py | 174 ++++++++++++++++++ .../subcontracting_receipt.py | 21 +++ .../test_subcontracting_receipt.py | 24 +++ 5 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 043391145d2..e6a5a19cdb0 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) @@ -234,10 +247,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) @@ -274,6 +299,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 7992bfdd546..98e1d776b32 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,85 @@ class TestSubcontractingOrder(ERPNextTestSuite): sco.load_from_db() self.assertEqual(sco.status, "Partially Received") +<<<<<<< HEAD +======= + 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 + self.assertRaises(frappe.ValidationError, sco.validate_purchase_order_for_subcontracting) + + def test_service_item_must_be_non_stock(self): + sco = get_subcontracting_order(do_not_submit=1) + sco.service_items[0].item_code = "_Test Item" # a stock item + self.assertRaises(frappe.ValidationError, sco.validate_service_items) + + def test_reserve_warehouse_must_differ_from_supplier_warehouse(self): + sco = get_subcontracting_order(do_not_submit=1) + sco.supplied_items[0].reserve_warehouse = sco.supplier_warehouse + self.assertRaises(frappe.ValidationError, sco.validate_supplied_items) + + def test_subcontracting_receipt_applies_bom_process_loss(self): + sco = get_subcontracting_order() + frappe.db.set_value("BOM", sco.items[0].bom, "process_loss_percentage", 10) + + scr = make_subcontracting_receipt(sco.name) + + # 10% of the ordered 10 qty is lost in processing + self.assertEqual(scr.items[0].received_qty, 10) + self.assertEqual(scr.items[0].process_loss_qty, 1) + self.assertEqual(scr.items[0].qty, 9) + + def test_service_cost_is_matched_by_purchase_order_item(self): + 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, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 200, + "fg_item": "Subcontracted Item SA1", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + expected = {item.purchase_order_item: item.service_cost_per_qty for item in sco.items} + + # The two finished goods have distinct service costs, so a position-based pairing would swap them + self.assertEqual(len(set(expected.values())), 2) + + # Service costs must follow purchase_order_item, not list position + sco.service_items.reverse() + sco.calculate_service_costs() + + for item in sco.items: + self.assertEqual(item.service_cost_per_qty, expected[item.purchase_order_item]) + +>>>>>>> fe25746 (fix(subcontracting): validate project across the subcontracting flow (#58965)) def test_make_rm_stock_entry(self): sco = get_subcontracting_order() rm_items = get_rm_items(sco.supplied_items) @@ -1000,3 +1080,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 new file mode 100644 index 00000000000..ae1b7653d4f --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py @@ -0,0 +1,174 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc +from frappe.utils import flt, get_link_to_form + + +@frappe.whitelist() +def make_subcontract_return_against_rejected_warehouse(source_name: str): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + return make_return_doc("Subcontracting Receipt", source_name, return_against_rejected_qty=True) + + +@frappe.whitelist() +def make_subcontract_return(source_name: str, target_doc: str | dict | Document | None = None): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + return make_return_doc("Subcontracting Receipt", source_name, target_doc) + + +@frappe.whitelist(methods=["POST"]) +def make_purchase_receipt( + source_name: Document | str, + target_doc: str | dict | Document | None = None, + save: bool = False, + submit: bool = False, + notify: bool = False, +): + if isinstance(source_name, str): + source_doc = frappe.get_doc("Subcontracting Receipt", source_name) + else: + source_doc = source_name + + if source_doc.is_return: + return + + po_sr_item_dict = {} + po_name = None + for item in source_doc.items: + if not item.purchase_order: + continue + + if not po_name: + po_name = item.purchase_order + + po_sr_item_dict[item.purchase_order_item] = { + "qty": flt(item.qty), + "rejected_qty": flt(item.rejected_qty), + "warehouse": item.warehouse, + "rejected_warehouse": item.rejected_warehouse, + "subcontracting_receipt_item": item.name, + } + + if not po_name: + frappe.throw( + _("Purchase Order Item reference is missing in Subcontracting Receipt {0}").format( + source_doc.name + ) + ) + + def update_item(obj, target, source_parent): + sr_item_details = po_sr_item_dict.get(obj.name) + ratio = flt(obj.qty) / flt(obj.fg_item_qty) + + target.update( + { + "qty": ratio * sr_item_details["qty"], + "rejected_qty": ratio * sr_item_details["rejected_qty"], + "warehouse": sr_item_details["warehouse"], + "rejected_warehouse": sr_item_details["rejected_warehouse"], + "subcontracting_receipt_item": sr_item_details["subcontracting_receipt_item"], + } + ) + + def post_process(source, target): + target.set_missing_values() + target.update( + { + "posting_date": source_doc.posting_date, + "posting_time": source_doc.posting_time, + "subcontracting_receipt": source_doc.name, + "supplier_warehouse": source_doc.supplier_warehouse, + "is_subcontracted": 1, + "currency": frappe.get_cached_value("Company", target.company, "default_currency"), + } + ) + + target_doc = get_mapped_doc( + "Purchase Order", + po_name, + { + "Purchase Order": { + "doctype": "Purchase Receipt", + "field_map": {"supplier_warehouse": "supplier_warehouse"}, + "validation": { + "docstatus": ["=", 1], + }, + }, + "Purchase Order Item": { + "doctype": "Purchase Receipt Item", + "field_map": { + "name": "purchase_order_item", + "parent": "purchase_order", + "bom": "bom", + }, + "postprocess": update_item, + "condition": lambda doc: doc.name in po_sr_item_dict, + }, + "Purchase Taxes and Charges": { + "doctype": "Purchase Taxes and Charges", + "reset_value": True, + # for POs created in earlier version with tax_withholding_row + "condition": lambda doc: not doc.is_tax_withholding_account, + }, + }, + postprocess=post_process, + ) + + if not target_doc.get("items"): + add_po_items_to_pr(source_doc, target_doc) + + if (save or submit) and frappe.has_permission(target_doc.doctype, "create"): + target_doc.save() + + if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc): + frappe.db.savepoint("submit_subcontracting_receipt") + try: + target_doc.submit() + except Exception as e: + frappe.db.rollback(save_point="submit_subcontracting_receipt") + target_doc.add_comment("Comment", _("Submit Action Failed") + "

" + str(e)) + + if notify: + frappe.msgprint( + _("Purchase Receipt {0} created.").format( + get_link_to_form(target_doc.doctype, target_doc.name) + ), + indicator="green", + alert=True, + ) + + return target_doc + + +def add_po_items_to_pr(scr_doc, target_doc): + fg_items = {(item.item_code, item.purchase_order): item.qty for item in scr_doc.items} + + for (item_code, po_name), fg_qty in fg_items.items(): + po_doc = frappe.get_doc("Purchase Order", po_name) + for item in po_doc.items: + if item.fg_item != item_code: + continue + + qty = (item.stock_qty - item.received_qty) * fg_qty / item.fg_item_qty + if qty: + target_doc.append( + "items", + { + "item_code": item.item_code, + "item_name": item.item_name, + "description": item.description, + "qty": qty, + "rate": item.rate, + "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 d962e6119e1..7911e4ca987 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -145,6 +145,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 @@ -164,11 +165,31 @@ class SubcontractingReceipt(SubcontractingController): self.set_supplied_items_cost_center() self.set_supplied_items_inventory_dimensions() +<<<<<<< HEAD # SubcontractingController.validate() does not call super() for Subcontracting Receipt, so # the shared mandatory inventory dimension check must be invoked explicitly here. It runs # last so auto-populated supplied-item dimensions (set_supplied_items_inventory_dimensions) # are already in place. self.validate_inventory_dimension_mandatory() +======= + 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, + }, + } + ) +>>>>>>> fe25746 (fix(subcontracting): validate project across the subcontracting flow (#58965)) def on_submit(self): self.validate_closed_subcontracting_order() diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 5c4a95ee033..cc9b434f827 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -24,6 +24,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 ( @@ -37,6 +38,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, ) @@ -50,6 +54,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) From 2add72ba3675ac507f87bed4054b636a7af5d805 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Sun, 13 Sep 2026 12:12:10 +0530 Subject: [PATCH 2/2] fix: resolve conflicts --- .../test_subcontracting_order.py | 61 ------ .../doctype/subcontracting_receipt/mapper.py | 174 ------------------ .../subcontracting_receipt.py | 6 +- 3 files changed, 3 insertions(+), 238 deletions(-) delete mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 98e1d776b32..1b84cf75a1d 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -113,8 +113,6 @@ class TestSubcontractingOrder(ERPNextTestSuite): sco.load_from_db() self.assertEqual(sco.status, "Partially Received") -<<<<<<< HEAD -======= 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) @@ -133,65 +131,6 @@ class TestSubcontractingOrder(ERPNextTestSuite): 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 - self.assertRaises(frappe.ValidationError, sco.validate_purchase_order_for_subcontracting) - - def test_service_item_must_be_non_stock(self): - sco = get_subcontracting_order(do_not_submit=1) - sco.service_items[0].item_code = "_Test Item" # a stock item - self.assertRaises(frappe.ValidationError, sco.validate_service_items) - - def test_reserve_warehouse_must_differ_from_supplier_warehouse(self): - sco = get_subcontracting_order(do_not_submit=1) - sco.supplied_items[0].reserve_warehouse = sco.supplier_warehouse - self.assertRaises(frappe.ValidationError, sco.validate_supplied_items) - - def test_subcontracting_receipt_applies_bom_process_loss(self): - sco = get_subcontracting_order() - frappe.db.set_value("BOM", sco.items[0].bom, "process_loss_percentage", 10) - - scr = make_subcontracting_receipt(sco.name) - - # 10% of the ordered 10 qty is lost in processing - self.assertEqual(scr.items[0].received_qty, 10) - self.assertEqual(scr.items[0].process_loss_qty, 1) - self.assertEqual(scr.items[0].qty, 9) - - def test_service_cost_is_matched_by_purchase_order_item(self): - 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, - }, - { - "warehouse": "_Test Warehouse - _TC", - "item_code": "Subcontracted Service Item 1", - "qty": 10, - "rate": 200, - "fg_item": "Subcontracted Item SA1", - "fg_item_qty": 10, - }, - ] - sco = get_subcontracting_order(service_items=service_items) - expected = {item.purchase_order_item: item.service_cost_per_qty for item in sco.items} - - # The two finished goods have distinct service costs, so a position-based pairing would swap them - self.assertEqual(len(set(expected.values())), 2) - - # Service costs must follow purchase_order_item, not list position - sco.service_items.reverse() - sco.calculate_service_costs() - - for item in sco.items: - self.assertEqual(item.service_cost_per_qty, expected[item.purchase_order_item]) - ->>>>>>> fe25746 (fix(subcontracting): validate project across the subcontracting flow (#58965)) def test_make_rm_stock_entry(self): sco = get_subcontracting_order() rm_items = get_rm_items(sco.supplied_items) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py b/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py deleted file mode 100644 index ae1b7653d4f..00000000000 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe import _ -from frappe.model.document import Document -from frappe.model.mapper import get_mapped_doc -from frappe.utils import flt, get_link_to_form - - -@frappe.whitelist() -def make_subcontract_return_against_rejected_warehouse(source_name: str): - from erpnext.controllers.sales_and_purchase_return import make_return_doc - - return make_return_doc("Subcontracting Receipt", source_name, return_against_rejected_qty=True) - - -@frappe.whitelist() -def make_subcontract_return(source_name: str, target_doc: str | dict | Document | None = None): - from erpnext.controllers.sales_and_purchase_return import make_return_doc - - return make_return_doc("Subcontracting Receipt", source_name, target_doc) - - -@frappe.whitelist(methods=["POST"]) -def make_purchase_receipt( - source_name: Document | str, - target_doc: str | dict | Document | None = None, - save: bool = False, - submit: bool = False, - notify: bool = False, -): - if isinstance(source_name, str): - source_doc = frappe.get_doc("Subcontracting Receipt", source_name) - else: - source_doc = source_name - - if source_doc.is_return: - return - - po_sr_item_dict = {} - po_name = None - for item in source_doc.items: - if not item.purchase_order: - continue - - if not po_name: - po_name = item.purchase_order - - po_sr_item_dict[item.purchase_order_item] = { - "qty": flt(item.qty), - "rejected_qty": flt(item.rejected_qty), - "warehouse": item.warehouse, - "rejected_warehouse": item.rejected_warehouse, - "subcontracting_receipt_item": item.name, - } - - if not po_name: - frappe.throw( - _("Purchase Order Item reference is missing in Subcontracting Receipt {0}").format( - source_doc.name - ) - ) - - def update_item(obj, target, source_parent): - sr_item_details = po_sr_item_dict.get(obj.name) - ratio = flt(obj.qty) / flt(obj.fg_item_qty) - - target.update( - { - "qty": ratio * sr_item_details["qty"], - "rejected_qty": ratio * sr_item_details["rejected_qty"], - "warehouse": sr_item_details["warehouse"], - "rejected_warehouse": sr_item_details["rejected_warehouse"], - "subcontracting_receipt_item": sr_item_details["subcontracting_receipt_item"], - } - ) - - def post_process(source, target): - target.set_missing_values() - target.update( - { - "posting_date": source_doc.posting_date, - "posting_time": source_doc.posting_time, - "subcontracting_receipt": source_doc.name, - "supplier_warehouse": source_doc.supplier_warehouse, - "is_subcontracted": 1, - "currency": frappe.get_cached_value("Company", target.company, "default_currency"), - } - ) - - target_doc = get_mapped_doc( - "Purchase Order", - po_name, - { - "Purchase Order": { - "doctype": "Purchase Receipt", - "field_map": {"supplier_warehouse": "supplier_warehouse"}, - "validation": { - "docstatus": ["=", 1], - }, - }, - "Purchase Order Item": { - "doctype": "Purchase Receipt Item", - "field_map": { - "name": "purchase_order_item", - "parent": "purchase_order", - "bom": "bom", - }, - "postprocess": update_item, - "condition": lambda doc: doc.name in po_sr_item_dict, - }, - "Purchase Taxes and Charges": { - "doctype": "Purchase Taxes and Charges", - "reset_value": True, - # for POs created in earlier version with tax_withholding_row - "condition": lambda doc: not doc.is_tax_withholding_account, - }, - }, - postprocess=post_process, - ) - - if not target_doc.get("items"): - add_po_items_to_pr(source_doc, target_doc) - - if (save or submit) and frappe.has_permission(target_doc.doctype, "create"): - target_doc.save() - - if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc): - frappe.db.savepoint("submit_subcontracting_receipt") - try: - target_doc.submit() - except Exception as e: - frappe.db.rollback(save_point="submit_subcontracting_receipt") - target_doc.add_comment("Comment", _("Submit Action Failed") + "

" + str(e)) - - if notify: - frappe.msgprint( - _("Purchase Receipt {0} created.").format( - get_link_to_form(target_doc.doctype, target_doc.name) - ), - indicator="green", - alert=True, - ) - - return target_doc - - -def add_po_items_to_pr(scr_doc, target_doc): - fg_items = {(item.item_code, item.purchase_order): item.qty for item in scr_doc.items} - - for (item_code, po_name), fg_qty in fg_items.items(): - po_doc = frappe.get_doc("Purchase Order", po_name) - for item in po_doc.items: - if item.fg_item != item_code: - continue - - qty = (item.stock_qty - item.received_qty) * fg_qty / item.fg_item_qty - if qty: - target_doc.append( - "items", - { - "item_code": item.item_code, - "item_name": item.item_name, - "description": item.description, - "qty": qty, - "rate": item.rate, - "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 7911e4ca987..b681dd777fe 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -165,13 +165,12 @@ class SubcontractingReceipt(SubcontractingController): self.set_supplied_items_cost_center() self.set_supplied_items_inventory_dimensions() -<<<<<<< HEAD # SubcontractingController.validate() does not call super() for Subcontracting Receipt, so # the shared mandatory inventory dimension check must be invoked explicitly here. It runs # last so auto-populated supplied-item dimensions (set_supplied_items_inventory_dimensions) # are already in place. self.validate_inventory_dimension_mandatory() -======= + def validate_with_previous_doc(self): super().validate_with_previous_doc( { @@ -189,7 +188,6 @@ class SubcontractingReceipt(SubcontractingController): }, } ) ->>>>>>> fe25746 (fix(subcontracting): validate project across the subcontracting flow (#58965)) def on_submit(self): self.validate_closed_subcontracting_order() @@ -1232,5 +1230,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, }, )