diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 2d7c0b3229eb7bdeed41db8e0652b3c102a753ae..62740e6e6e1c9c81370ec7b4292bdcb2664476e7 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -90,6 +90,10 @@ class PurchaseOrder(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): + mri_compare_fields = [["project", "="], ["item_code", "="]] + if self.is_subcontracted: + mri_compare_fields = [["project", "="]] + super(PurchaseOrder, self).validate_with_previous_doc( { "Supplier Quotation": { @@ -112,7 +116,7 @@ class PurchaseOrder(BuyingController): }, "Material Request Item": { "ref_dn_field": "material_request_item", - "compare_fields": [["project", "="], ["item_code", "="]], + "compare_fields": mri_compare_fields, "is_child_table": True, }, } @@ -286,23 +290,6 @@ class PurchaseOrder(BuyingController): check_list.append(d.material_request) check_on_hold_or_closed_status("Material Request", d.material_request) - def update_requested_qty(self): - material_request_map = {} - for d in self.get("items"): - if d.material_request_item: - material_request_map.setdefault(d.material_request, []).append(d.material_request_item) - - for mr, mr_item_rows in material_request_map.items(): - if mr and mr_item_rows: - mr_obj = frappe.get_doc("Material Request", mr) - - if mr_obj.status in ["Stopped", "Cancelled"]: - frappe.throw( - _("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError - ) - - mr_obj.update_requested_qty(mr_item_rows) - def update_ordered_qty(self, po_item_rows=None): """update requested qty (before ordered_qty is updated)""" item_wh_list = [] @@ -345,7 +332,9 @@ class PurchaseOrder(BuyingController): self.update_status_updater() self.update_prevdoc_status() - self.update_requested_qty() + if not self.is_subcontracted or self.is_old_subcontracting_flow: + self.update_requested_qty() + self.update_ordered_qty() self.validate_budget() self.update_reserved_qty_for_subcontract() @@ -380,7 +369,9 @@ class PurchaseOrder(BuyingController): # Must be called after updating ordered qty in Material Request # bin uses Material Request Items to recalculate & update - self.update_requested_qty() + if not self.is_subcontracted or self.is_old_subcontracting_flow: + self.update_requested_qty() + self.update_ordered_qty() self.update_blanket_order() @@ -682,21 +673,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions return doc -def get_item_details(items): - item_details = {} - for d in frappe.db.sql( - """select item_code, description, allow_alternative_item from `tabItem` - where name in ({0})""".format( - ", ".join(["%s"] * len(items)) - ), - items, - as_dict=1, - ): - item_details[d.item_code] = d - - return item_details - - def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context @@ -778,6 +754,8 @@ def get_mapped_subcontracting_order(source_name, target_doc=None): "doctype": "Subcontracting Order Service Item", "field_map": { "name": "purchase_order_item", + "material_request": "material_request", + "material_request_item": "material_request_item", }, "field_no_map": [], }, diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index f88e3ebf534efc94497e7511f475e6fbcb5b9fc0..9555902a74cd9defa8f4c5ee2d0aa2fd72a86aa6 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -953,6 +953,23 @@ class SubcontractingController(StockController): return self._sub_contracted_items + def update_requested_qty(self): + material_request_map = {} + for d in self.get("items"): + if d.material_request_item: + material_request_map.setdefault(d.material_request, []).append(d.material_request_item) + + for mr, mr_item_rows in material_request_map.items(): + if mr and mr_item_rows: + mr_obj = frappe.get_doc("Material Request", mr) + + if mr_obj.status in ["Stopped", "Cancelled"]: + frappe.throw( + _("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError + ) + + mr_obj.update_requested_qty(mr_item_rows) + def get_item_details(items): item = frappe.qb.DocType("Item") diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index dca1f58ebd3dcc1db2e42e6a3c8bc9c182a8ce52..8ed554dfc1dd0a821594e658eb2b1978d57b91c4 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -336,7 +336,7 @@ class BOM(WebsiteGenerator): ) args.update(item) - rate = self.get_rm_rate(args) + rate = self.get_rm_rate(args) if self.rm_cost_as_per != "Manual" else (args.get("rate") or 0.0) ret_item = { "item_name": item and args["item_name"] or "", "description": item and args["description"] or "", @@ -732,7 +732,7 @@ class BOM(WebsiteGenerator): def calculate_exploded_cost(self): "Set exploded row cost from it's parent BOM." - rm_rate_map = self.get_rm_rate_map() + rm_rate_map = self.get_rm_rate_map() if self.rm_cost_as_per != "Manual" else {} for row in self.get("exploded_items"): old_rate = flt(row.rate) diff --git a/erpnext/manufacturing/doctype/bom/test_records.json b/erpnext/manufacturing/doctype/bom/test_records.json index 38ea414b184d09da720733e1e2e37ac73588b548..86707a327569ecb8a1f879a3b60805b4e95a9701 100644 --- a/erpnext/manufacturing/doctype/bom/test_records.json +++ b/erpnext/manufacturing/doctype/bom/test_records.json @@ -36,16 +36,16 @@ "quantity": 1.0 }, { - "scrap_items":[ - { - "amount": 2000.0, - "doctype": "BOM Scrap Item", - "item_code": "_Test Item Home Desktop 100", - "parentfield": "scrap_items", - "stock_qty": 1.0, - "rate": 2000.0, - "stock_uom": "_Test UOM" - } + "scrap_items": [ + { + "amount": 2000.0, + "doctype": "BOM Scrap Item", + "item_code": "_Test Item Home Desktop 100", + "parentfield": "scrap_items", + "stock_qty": 1.0, + "rate": 2000.0, + "stock_uom": "_Test UOM" + } ], "items": [ { @@ -80,7 +80,8 @@ "currency": "USD", "company": "_Test Company", "item": "_Test FG Item", - "quantity": 1.0 + "quantity": 1.0, + "rm_cost_as_per": "Manual" }, { "operations": [ @@ -92,7 +93,7 @@ "time_in_mins": 60, "operating_cost": 100 } - ], + ], "items": [ { "amount": 5000.0, @@ -140,7 +141,7 @@ "time_in_mins": 60, "operating_cost": 140 } - ], + ], "items": [ { "amount": 5000.0, @@ -164,31 +165,31 @@ "item": "_Test Variant Item", "quantity": 1.0, "with_operations": 1 -}, -{ - "items": [ - { - "amount": 5000.0, - "doctype": "BOM Item", - "item_code": "_Test Item", - "parentfield": "items", - "qty": 2.0, - "rate": 3000.0, - "uom": "_Test UOM", - "stock_uom": "_Test UOM", - "source_warehouse": "_Test Warehouse - _TC", - "include_item_in_manufacturing": 1 - } - ], - "docstatus": 1, - "doctype": "BOM", - "is_active": 1, - "is_default": 1, - "currency": "USD", - "item": "_Test Variant Item", - "quantity": 1.0, - "with_operations": 0, - "fg_based_operating_cost": 1, - "operating_cost_per_bom_quantity": 140 + }, + { + "items": [ + { + "amount": 5000.0, + "doctype": "BOM Item", + "item_code": "_Test Item", + "parentfield": "items", + "qty": 2.0, + "rate": 3000.0, + "uom": "_Test UOM", + "stock_uom": "_Test UOM", + "source_warehouse": "_Test Warehouse - _TC", + "include_item_in_manufacturing": 1 + } + ], + "docstatus": 1, + "doctype": "BOM", + "is_active": 1, + "is_default": 1, + "currency": "USD", + "item": "_Test Variant Item", + "quantity": 1.0, + "with_operations": 0, + "fg_based_operating_cost": 1, + "operating_cost_per_bom_quantity": 140 } ] diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 68c084547cb87fa52ff964c0c779df9b45d730dc..0fe8c13efbdf6da427d41bf328722061e1f0c14b 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -14,6 +14,90 @@ from erpnext.stock.utils import get_bin class SubcontractingOrder(SubcontractingController): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.stock.doctype.landed_cost_taxes_and_charges.landed_cost_taxes_and_charges import ( + LandedCostTaxesandCharges, + ) + from erpnext.subcontracting.doctype.subcontracting_order_item.subcontracting_order_item import ( + SubcontractingOrderItem, + ) + from erpnext.subcontracting.doctype.subcontracting_order_service_item.subcontracting_order_service_item import ( + SubcontractingOrderServiceItem, + ) + from erpnext.subcontracting.doctype.subcontracting_order_supplied_item.subcontracting_order_supplied_item import ( + SubcontractingOrderSuppliedItem, + ) + + additional_costs: DF.Table[LandedCostTaxesandCharges] + address_display: DF.SmallText | None + amended_from: DF.Link | None + billing_address: DF.Link | None + billing_address_display: DF.SmallText | None + company: DF.Link + contact_display: DF.SmallText | None + contact_email: DF.SmallText | None + contact_mobile: DF.SmallText | None + contact_person: DF.Link | None + cost_center: DF.Link | None + distribute_additional_costs_based_on: DF.Literal["Qty", "Amount"] + items: DF.Table[SubcontractingOrderItem] + letter_head: DF.Link | None + naming_series: DF.Literal["SC-ORD-.YYYY.-"] + per_received: DF.Percent + project: DF.Link | None + purchase_order: DF.Link + schedule_date: DF.Date | None + select_print_heading: DF.Link | None + service_items: DF.Table[SubcontractingOrderServiceItem] + set_reserve_warehouse: DF.Link | None + set_warehouse: DF.Link | None + shipping_address: DF.Link | None + shipping_address_display: DF.SmallText | None + status: DF.Literal[ + "Draft", + "Open", + "Partially Received", + "Completed", + "Material Transferred", + "Partial Material Transferred", + "Cancelled", + ] + supplied_items: DF.Table[SubcontractingOrderSuppliedItem] + supplier: DF.Link + supplier_address: DF.Link | None + supplier_name: DF.Data + supplier_warehouse: DF.Link + title: DF.Data | None + total: DF.Currency + total_additional_costs: DF.Currency + total_qty: DF.Float + transaction_date: DF.Date + # end: auto-generated types + + def __init__(self, *args, **kwargs): + super(SubcontractingOrder, self).__init__(*args, **kwargs) + + self.status_updater = [ + { + "source_dt": "Subcontracting Order Item", + "target_dt": "Material Request Item", + "join_field": "material_request_item", + "target_field": "ordered_qty", + "target_parent_dt": "Material Request", + "target_parent_field": "per_ordered", + "target_ref_field": "stock_qty", + "source_field": "qty", + "percent_join_field": "material_request", + } + ] + def before_validate(self): super(SubcontractingOrder, self).before_validate() @@ -27,11 +111,15 @@ class SubcontractingOrder(SubcontractingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") def on_submit(self): + self.update_prevdoc_status() + self.update_requested_qty() self.update_ordered_qty_for_subcontracting() self.update_reserved_qty_for_subcontracting() self.update_status() def on_cancel(self): + self.update_prevdoc_status() + self.update_requested_qty() self.update_ordered_qty_for_subcontracting() self.update_reserved_qty_for_subcontracting() self.update_status() @@ -171,6 +259,8 @@ class SubcontractingOrder(SubcontractingController): "stock_uom": item.stock_uom, "bom": bom, "purchase_order_item": si.purchase_order_item, + "material_request": si.material_request, + "material_request_item": si.material_request_item, } ) else: diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 3557858935134065568e5bb1d850d7e3f716d381..37dabf1bfbe71102fa815fadd93a9b4a4b536f10 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -628,6 +628,62 @@ class TestSubcontractingOrder(FrappeTestCase): self.assertEqual(ordered_qty + 10, new_ordered_qty) + def test_requested_qty_for_subcontracting_order(self): + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + from erpnext.stock.doctype.material_request.test_material_request import make_material_request + + requested_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="indented_qty", + ) + requested_qty = flt(requested_qty) + + mr = make_material_request( + item_code="Subcontracted Item SA8", + material_request_type="Purchase", + qty=10, + ) + + self.assertTrue(mr.docstatus == 1) + + new_requested_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="indented_qty", + ) + new_requested_qty = flt(new_requested_qty) + + self.assertEqual(requested_qty + 10, new_requested_qty) + + po = make_purchase_order(mr.name) + po.is_subcontracted = 1 + po.supplier = "_Test Supplier" + po.items[0].fg_item = "Subcontracted Item SA8" + po.items[0].fg_item_qty = 10 + po.items[0].item_code = "Subcontracted Service Item 8" + po.items[0].item_name = "Subcontracted Service Item 8" + po.items[0].qty = 10 + po.supplier_warehouse = "_Test Warehouse 1 - _TC" + po.save() + po.submit() + + self.assertTrue(po.items[0].material_request) + self.assertTrue(po.items[0].material_request_item) + + sco = create_subcontracting_order(po_name=po.name) + self.assertTrue(sco.items[0].material_request) + self.assertTrue(sco.items[0].material_request_item) + + new_requested_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="indented_qty", + ) + new_requested_qty = flt(new_requested_qty) + + self.assertEqual(requested_qty, new_requested_qty) + def create_subcontracting_order(**args): args = frappe._dict(args) diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json index d1f21ba7c96a87d102a47265fbd114a9dc962c8e..9ba595f830c5962671a24570861f8fe5beef9b43 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -40,13 +40,18 @@ "manufacture_section", "manufacturer", "manufacturer_part_no", + "column_break_impp", + "reference_section", + "material_request", + "column_break_fpyl", + "material_request_item", "accounting_dimensions_section", "cost_center", "dimension_col_break", "project", "section_break_34", - "page_break", - "purchase_order_item" + "purchase_order_item", + "page_break" ], "fields": [ { @@ -334,6 +339,37 @@ "label": "Project", "options": "Project" }, + { + "fieldname": "column_break_impp", + "fieldtype": "Column Break" + }, + { + "fieldname": "material_request", + "fieldtype": "Link", + "label": "Material Request", + "no_copy": 1, + "options": "Material Request", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "material_request_item", + "fieldtype": "Data", + "label": "Material Request Item", + "no_copy": 1, + "read_only": 1, + "search_index": 1 + }, + { + "collapsible": 1, + "fieldname": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "column_break_fpyl", + "fieldtype": "Column Break" + }, { "fieldname": "purchase_order_item", "fieldtype": "Data", @@ -348,7 +384,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-23 16:56:22.182698", + "modified": "2023-11-30 15:29:43.744618", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json index 2ca41da71e26cb4c3626a07516bd0fb4958705db..6ae310cf756818f182dba268c5fdafbe9f34b75b 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json @@ -19,7 +19,11 @@ "fg_item", "column_break_12", "fg_item_qty", - "purchase_order_item" + "purchase_order_item", + "section_break_kphn", + "material_request", + "column_break_piqi", + "material_request_item" ], "fields": [ { @@ -122,11 +126,36 @@ "no_copy": 1, "read_only": 1, "search_index": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_kphn", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "material_request", + "fieldtype": "Link", + "label": "Material Request", + "no_copy": 1, + "options": "Material Request", + "read_only": 1 + }, + { + "fieldname": "column_break_piqi", + "fieldtype": "Column Break" + }, + { + "fieldname": "material_request_item", + "fieldtype": "Data", + "label": "Material Request Item", + "no_copy": 1, + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2023-11-23 17:05:04.561948", + "modified": "2023-11-30 13:29:31.017440", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Service Item",