From 8f811728d97293744bb35f6a3bdba889708127c5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:18:01 +0530 Subject: [PATCH] feat(subcontracting): Added provision to create multiple Subcontracting Orders against a single Purchase Order (backport #44711) (#44782) * feat(subcontracting): Added provision to create multiple Subcontracting Orders against a single Purchase Order (#44711) * feat(subcontracting): Added provision to create multiple Subcontracting Orders from a single Subcontracted Purchase Order * refactor(new_sc_flow_2): Fixed error thrown by semgrep (cherry picked from commit 3eba6bf3ddcf911638681a83522e4e47196183ae) # Conflicts: # erpnext/buying/doctype/purchase_order/purchase_order.js # erpnext/buying/doctype/purchase_order/test_purchase_order.py # erpnext/buying/doctype/purchase_order_item/purchase_order_item.json # erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json # erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json * feat(subcontracting): Added provision to create multiple Subcontracting Orders against a single Purchase Order (#44711) * feat(subcontracting): Added provision to create multiple Subcontracting Orders from a single Subcontracted Purchase Order * refactor(new_sc_flow_2): Fixed error thrown by semgrep * fix: Resolved errors and removed code from develop branch merged by mistake --------- Co-authored-by: Mihir Kandoi --- .../doctype/purchase_order/purchase_order.js | 14 ++- .../doctype/purchase_order/purchase_order.py | 61 ++++----- .../purchase_order/test_purchase_order.py | 116 +++++++++++++++++- .../purchase_order_item.json | 13 +- .../purchase_order_item.py | 1 + .../controllers/subcontracting_controller.py | 19 +++ .../tests/test_subcontracting_controller.py | 3 +- .../subcontracting_order.js | 30 ++++- .../subcontracting_order.py | 50 ++++++-- .../test_subcontracting_order.py | 6 - .../subcontracting_order_item.json | 14 ++- .../subcontracting_order_item.py | 1 + .../subcontracting_order_service_item.json | 2 +- .../subcontracting_order_service_item.py | 2 + 14 files changed, 272 insertions(+), 60 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index e71f24187940..4cc383912764 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -400,11 +400,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( ); } } else { - cur_frm.add_custom_button( - __("Subcontracting Order"), - this.make_subcontracting_order, - __("Create") - ); + if (!doc.items.every((item) => item.qty == item.sco_qty)) { + this.frm.add_custom_button( + __("Subcontracting Order"), + () => { + me.make_subcontracting_order(); + }, + __("Create") + ); + } } } } diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 6ba9cb69f9fc..26c0101b49b6 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -867,27 +867,40 @@ def make_inter_company_sales_order(source_name, target_doc=None): @frappe.whitelist() def make_subcontracting_order(source_name, target_doc=None, save=False, submit=False, notify=False): - target_doc = get_mapped_subcontracting_order(source_name, target_doc) - - if (save or submit) and frappe.has_permission(target_doc.doctype, "create"): - target_doc.save() + if not is_po_fully_subcontracted(source_name): + target_doc = get_mapped_subcontracting_order(source_name, 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): + try: + target_doc.submit() + except Exception as e: + target_doc.add_comment("Comment", _("Submit Action Failed") + "

" + str(e)) + + if notify: + frappe.msgprint( + _("Subcontracting Order {0} created.").format( + get_link_to_form(target_doc.doctype, target_doc.name) + ), + indicator="green", + alert=True, + ) - if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc): - try: - target_doc.submit() - except Exception as e: - target_doc.add_comment("Comment", _("Submit Action Failed") + "

" + str(e)) + return target_doc + else: + frappe.throw(_("This PO has been fully subcontracted.")) - if notify: - frappe.msgprint( - _("Subcontracting Order {0} created.").format( - get_link_to_form(target_doc.doctype, target_doc.name) - ), - indicator="green", - alert=True, - ) - return target_doc +def is_po_fully_subcontracted(po_name): + table = frappe.qb.DocType("Purchase Order Item") + query = ( + frappe.qb.from_(table) + .select(table.name) + .where((table.parent == po_name) & (table.qty != table.sco_qty)) + ) + return not query.run(as_dict=True) def get_mapped_subcontracting_order(source_name, target_doc=None): @@ -931,7 +944,8 @@ def post_process(source_doc, target_doc): "material_request": "material_request", "material_request_item": "material_request_item", }, - "field_no_map": [], + "field_no_map": ["qty", "fg_item_qty", "amount"], + "condition": lambda item: item.qty != item.sco_qty, }, }, target_doc, @@ -939,12 +953,3 @@ def post_process(source_doc, target_doc): ) return target_doc - - -@frappe.whitelist() -def is_subcontracting_order_created(po_name) -> bool: - return ( - True - if frappe.db.exists("Subcontracting Order", {"purchase_order": po_name, "docstatus": ["=", 1]}) - else False - ) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index dbfcbf60b266..5d8ce73e8ab6 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1004,7 +1004,7 @@ def test_update_items_for_subcontracting_purchase_order(self): ) def update_items(po, qty): - trans_items = [po.items[0].as_dict()] + trans_items = [po.items[0].as_dict().update({"docname": po.items[0].name})] trans_items[0]["qty"] = qty trans_items[0]["fg_item_qty"] = qty trans_items = json.dumps(trans_items, default=str) @@ -1059,6 +1059,73 @@ def update_items(po, qty): self.assertEqual(po.items[0].qty, 30) self.assertEqual(po.items[0].fg_item_qty, 30) + def test_new_sc_flow(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_subcontracting_order + + po = create_po_for_sc_testing() + sco = make_subcontracting_order(po.name) + + sco.items[0].qty = 5 + sco.items.pop(1) + sco.items[1].qty = 25 + sco.save() + sco.submit() + + # Test - 1: Quantity of Service Items should change based on change in Quantity of its corresponding Finished Goods Item + self.assertEqual(sco.service_items[0].qty, 5) + + # Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly + po.reload() + self.assertEqual(po.items[0].sco_qty, 5) + self.assertEqual(po.items[1].sco_qty, 0) + self.assertEqual(po.items[2].sco_qty, 12.5) + + # Test - 3: Amount for both FG Item and its Service Item should be updated correctly based on change in Quantity + self.assertEqual(sco.items[0].amount, 2000) + self.assertEqual(sco.service_items[0].amount, 500) + + # Test - 4: Service Items should be removed if its corresponding Finished Good line item is deleted + self.assertEqual(len(sco.service_items), 2) + + # Test - 5: Service Item quantity calculation should be based upon conversion factor calculated from its corresponding PO Item + self.assertEqual(sco.service_items[1].qty, 12.5) + + sco = make_subcontracting_order(po.name) + + sco.items[0].qty = 6 + + # Test - 6: Saving document should not be allowed if Quantity exceeds available Subcontracting Quantity of any Purchase Order Item + self.assertRaises(frappe.ValidationError, sco.save) + + sco.items[0].qty = 5 + sco.items.pop() + sco.items.pop() + sco.save() + sco.submit() + + sco = make_subcontracting_order(po.name) + + # Test - 7: Since line item 1 is now fully subcontracted, new SCO should by default only have the remaining 2 line items + self.assertEqual(len(sco.items), 2) + + sco.items.pop(0) + sco.save() + sco.submit() + + # Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled + po.reload() + self.assertEqual(po.items[2].sco_qty, 25) + sco.cancel() + po.reload() + self.assertEqual(po.items[2].sco_qty, 12.5) + + sco = make_subcontracting_order(po.name) + sco.save() + sco.submit() + + # Test - 8: Since this PO is now fully subcontracted, creating a new SCO from it should throw error + self.assertRaises(frappe.ValidationError, make_subcontracting_order, po.name) + @change_settings("Buying Settings", {"auto_create_subcontracting_order": 1}) def test_auto_create_subcontracting_order(self): from erpnext.controllers.tests.test_subcontracting_controller import ( @@ -1124,6 +1191,53 @@ def test_po_billed_amount_against_return_entry(self): self.assertEqual(po.per_billed, 100) +def create_po_for_sc_testing(): + from erpnext.controllers.tests.test_subcontracting_controller import ( + make_bom_for_subcontracted_items, + make_raw_materials, + make_service_items, + make_subcontracted_items, + ) + + make_subcontracted_items() + make_raw_materials() + make_service_items() + make_bom_for_subcontracted_items() + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA1", + "fg_item_qty": 10, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 2", + "qty": 20, + "rate": 25, + "fg_item": "Subcontracted Item SA2", + "fg_item_qty": 15, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 3", + "qty": 25, + "rate": 10, + "fg_item": "Subcontracted Item SA3", + "fg_item_qty": 50, + }, + ] + + return create_purchase_order( + rm_items=service_items, + is_subcontracted=1, + supplier_warehouse="_Test Warehouse 1 - _TC", + ) + + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.selling.doctype.customer.test_customer import create_internal_customer diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index e3e8def7ffdf..4fc20594ffab 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "hash", - "creation": "2013-05-24 19:29:06", + "creation": "2024-12-09 12:54:24.652161", "doctype": "DocType", "document_type": "Document", "editable_grid": 1, @@ -26,6 +26,7 @@ "quantity_and_rate", "qty", "stock_uom", + "sco_qty", "col_break2", "uom", "conversion_factor", @@ -909,13 +910,21 @@ { "fieldname": "column_break_fyqr", "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "sco_qty", + "fieldtype": "Float", + "label": "Subcontracted Quantity", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-02-05 11:23:24.859435", + "modified": "2024-12-10 12:11:18.536089", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py index e9cc2b4eecfe..b80abda56c3a 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py @@ -80,6 +80,7 @@ class PurchaseOrderItem(Document): sales_order_item: DF.Data | None sales_order_packed_item: DF.Data | None schedule_date: DF.Date + sco_qty: DF.Float stock_qty: DF.Float stock_uom: DF.Link stock_uom_rate: DF.Currency diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 5fb1ee468cda..75cb5516348e 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -103,6 +103,19 @@ def validate_items(self): _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name) ) + if ( + self.doctype not in "Subcontracting Receipt" + and item.qty + > flt(get_pending_sco_qty(self.purchase_order).get(item.purchase_order_item)) + / item.sc_conversion_factor + ): + frappe.throw( + _( + "Row {0}: Item {1}'s quantity cannot be higher than the available quantity." + ).format(item.idx, item.item_name) + ) + item.amount = item.qty * item.rate + if item.bom: is_active, bom_item = frappe.get_value("BOM", item.bom, ["is_active", "item"]) @@ -1110,6 +1123,12 @@ def get_item_details(items): return item_details +def get_pending_sco_qty(po_name): + table = frappe.qb.DocType("Purchase Order Item") + query = frappe.qb.from_(table).select(table.name, table.qty, table.sco_qty).where(table.parent == po_name) + return {item.name: item.qty - item.sco_qty for item in query.run(as_dict=True)} + + @frappe.whitelist() def make_rm_stock_entry( subcontract_order, rm_items=None, order_doctype="Subcontracting Order", target_doc=None diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 84326bafef2e..0bc348e88761 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -1261,6 +1261,7 @@ def make_raw_materials(): for item, properties in raw_materials.items(): if not frappe.db.exists("Item", item): properties.update({"is_stock_item": 1}) + properties.update({"valuation_rate": 100}) make_item(item, properties) @@ -1311,7 +1312,7 @@ def make_bom_for_subcontracted_items(): for item_code, raw_materials in boms.items(): if not frappe.db.exists("BOM", {"item": item_code}): - make_bom(item=item_code, raw_materials=raw_materials, rate=100) + make_bom(item=item_code, raw_materials=raw_materials, rate=100, currency="INR") def set_backflush_based_on(based_on): diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index c6ee3a3e0e35..e9513a47597d 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -5,10 +5,38 @@ frappe.provide("erpnext.buying"); erpnext.landed_cost_taxes_and_charges.setup_triggers("Subcontracting Order"); +// client script for Subcontracting Order Item is not necessarily required as the server side code will do everything that is necessary. +// this is just so that the user does not get potentially confused +frappe.ui.form.on("Subcontracting Order Item", { + qty(frm, cdt, cdn) { + const row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, "amount", row.qty * row.rate); + const service_item = frm.doc.service_items[row.idx - 1]; + frappe.model.set_value( + service_item.doctype, + service_item.name, + "qty", + row.qty * row.sc_conversion_factor + ); + frappe.model.set_value(service_item.doctype, service_item.name, "fg_item_qty", row.qty); + frappe.model.set_value( + service_item.doctype, + service_item.name, + "amount", + row.qty * row.sc_conversion_factor * service_item.rate + ); + }, + before_items_remove(frm, cdt, cdn) { + const row = locals[cdt][cdn]; + frm.toggle_enable(["service_items"], true); + frm.get_field("service_items").grid.grid_rows[row.idx - 1].remove(); + frm.toggle_enable(["service_items"], false); + }, +}); + frappe.ui.form.on("Subcontracting Order", { setup: (frm) => { frm.get_field("items").grid.cannot_add_rows = true; - frm.get_field("items").grid.only_sortable(); frm.trigger("set_queries"); frm.set_indicator_formatter("item_code", (doc) => (doc.qty <= doc.received_qty ? "green" : "orange")); diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 8419c8e90935..f6d3fa041484 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -6,7 +6,6 @@ from frappe.model.mapper import get_mapped_doc from frappe.utils import flt -from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.stock.stock_balance import update_bin_qty @@ -120,20 +119,15 @@ def validate(self): def on_submit(self): self.update_prevdoc_status() self.update_status() + self.update_sco_qty_in_po() def on_cancel(self): self.update_prevdoc_status() self.update_status() + self.update_sco_qty_in_po(cancel=True) def validate_purchase_order_for_subcontracting(self): if self.purchase_order: - if is_subcontracting_order_created(self.purchase_order): - frappe.throw( - _( - "Only one Subcontracting Order can be created against a Purchase Order, cancel the existing Subcontracting Order to create a new one." - ) - ) - po = frappe.get_doc("Purchase Order", self.purchase_order) if not po.is_subcontracted: @@ -154,10 +148,23 @@ def validate_purchase_order_for_subcontracting(self): frappe.throw(_("Please select a Subcontracting Purchase Order.")) def validate_service_items(self): - for item in self.service_items: - if frappe.get_value("Item", item.item_code, "is_stock_item"): - msg = f"Service Item {item.item_name} must be a non-stock item." - frappe.throw(_(msg)) + purchase_order_items = [item.purchase_order_item for item in self.items] + self.service_items = [ + service_item + for service_item in self.service_items + if service_item.purchase_order_item in purchase_order_items + ] + + for service_item in self.service_items: + if frappe.get_value("Item", service_item.item_code, "is_stock_item"): + frappe.throw(_("Service Item {0} must be a non-stock item.").format(service_item.item_code)) + + item = next( + item for item in self.items if item.purchase_order_item == service_item.purchase_order_item + ) + service_item.qty = item.qty * item.sc_conversion_factor + service_item.fg_item_qty = item.qty + service_item.amount = service_item.qty * service_item.rate def validate_supplied_items(self): if self.supplier_warehouse: @@ -241,6 +248,18 @@ def populate_items_table(self): for si in self.service_items: if si.fg_item: item = frappe.get_doc("Item", si.fg_item) + + po_item = frappe.get_doc("Purchase Order Item", si.purchase_order_item) + available_qty = po_item.qty - po_item.sco_qty + + if available_qty == 0: + continue + + si.qty = available_qty + conversion_factor = po_item.qty / po_item.fg_item_qty + si.fg_item_qty = available_qty / conversion_factor + si.amount = available_qty * si.rate + bom = ( frappe.db.get_value( "Subcontracting BOM", @@ -257,6 +276,7 @@ def populate_items_table(self): "schedule_date": self.schedule_date, "description": item.description, "qty": si.fg_item_qty, + "sc_conversion_factor": conversion_factor, "stock_uom": item.stock_uom, "bom": bom, "purchase_order_item": si.purchase_order_item, @@ -310,6 +330,12 @@ def update_status(self, status=None, update_modified=True): self.update_ordered_qty_for_subcontracting() self.update_reserved_qty_for_subcontracting() + def update_sco_qty_in_po(self, cancel=False): + for service_item in self.service_items: + doc = frappe.get_doc("Purchase Order Item", service_item.purchase_order_item) + doc.sco_qty = (doc.sco_qty + service_item.qty) if not cancel else (doc.sco_qty - service_item.qty) + doc.save() + @frappe.whitelist() def make_subcontracting_receipt(source_name, target_doc=None): diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index ac87239e73ed..43592b628ad1 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -40,12 +40,6 @@ def setUp(self): make_service_items() make_bom_for_subcontracted_items() - def test_populate_items_table(self): - sco = get_subcontracting_order() - sco.items = None - sco.populate_items_table() - self.assertEqual(len(sco.service_items), len(sco.items)) - def test_set_missing_values(self): sco = get_subcontracting_order() before = {sco.total_qty, sco.total, sco.total_additional_costs} 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 502a28b3ec62..31616944fdac 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -51,7 +51,8 @@ "project", "section_break_34", "purchase_order_item", - "page_break" + "page_break", + "sc_conversion_factor" ], "fields": [ { @@ -144,8 +145,8 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Quantity", + "non_negative": 1, "print_width": "60px", - "read_only": 1, "reqd": 1, "width": "60px" }, @@ -381,13 +382,20 @@ "no_copy": 1, "read_only": 1, "search_index": 1 + }, + { + "fieldname": "sc_conversion_factor", + "fieldtype": "Float", + "hidden": 1, + "label": "SC Conversion Factor", + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-12-06 15:23:05.252346", + "modified": "2024-12-13 13:35:28.935898", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py index 7a426f91cb00..d8f2e5664e73 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py @@ -42,6 +42,7 @@ class SubcontractingOrderItem(Document): received_qty: DF.Float returned_qty: DF.Float rm_cost_per_qty: DF.Currency + sc_conversion_factor: DF.Float schedule_date: DF.Date | None service_cost_per_qty: DF.Currency stock_uom: DF.Link 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 f1e94e125a98..ad3dfe34c888 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 @@ -155,7 +155,7 @@ ], "istable": 1, "links": [], - "modified": "2023-11-30 13:29:31.017440", + "modified": "2024-12-05 17:33:46.099601", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Service Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py index cc4901baf45c..661a2b2702ce 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py @@ -19,6 +19,8 @@ class SubcontractingOrderServiceItem(Document): fg_item_qty: DF.Float item_code: DF.Link item_name: DF.Data + material_request: DF.Link | None + material_request_item: DF.Data | None parent: DF.Data parentfield: DF.Data parenttype: DF.Data