From 2a37cfffcdea0a1026ac60f0080533d0b76465f1 Mon Sep 17 00:00:00 2001 From: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:39:39 +0530 Subject: [PATCH] feat: Adding requested qty in packed item (#53486) * feat: Adding requested qty in packed item * fix: correct import path --------- Co-authored-by: Nishka Gosalia Co-authored-by: Mihir Kandoi (cherry picked from commit 953f089c063980502d1735dbfca7ca17b1d115c5) # Conflicts: # erpnext/controllers/status_updater.py # erpnext/patches.txt # erpnext/selling/doctype/sales_order/sales_order.py --- erpnext/controllers/status_updater.py | 7 +++ erpnext/patches.txt | 7 +++ .../v16_0/update_requested_qty_packed_item.py | 24 ++++++++++ .../doctype/sales_order/sales_order.py | 46 +++++++++++++++++-- .../doctype/sales_order/test_sales_order.py | 26 +++++++++++ .../material_request/material_request.py | 11 ++++- .../doctype/packed_item/packed_item.json | 12 ++++- .../stock/doctype/packed_item/packed_item.py | 1 + 8 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 erpnext/patches/v16_0/update_requested_qty_packed_item.py diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 290d8eb5d4b..1abdbc36a34 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -342,7 +342,14 @@ class StatusUpdater(Document): ): return +<<<<<<< HEAD if args["target_dt"] != "Quotation Item": +======= + if args["source_dt"] != "Pick List Item" and args["target_dt"] not in [ + "Quotation Item", + "Packed Item", + ]: +>>>>>>> 953f089c06 (feat: Adding requested qty in packed item (#53486)) if qty_or_amount == "qty": action_msg = _( 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7d2c1757bda..5d57fe6c6ad 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -432,3 +432,10 @@ erpnext.patches.v16_0.set_ordered_qty_in_quotation_item erpnext.patches.v15_0.replace_http_with_https_in_sales_partner erpnext.patches.v16_0.add_portal_redirects erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po +<<<<<<< HEAD +======= +erpnext.patches.v16_0.complete_onboarding_steps_for_older_sites #2 +erpnext.patches.v16_0.enable_serial_batch_setting +erpnext.patches.v16_0.co_by_product_patch +erpnext.patches.v16_0.update_requested_qty_packed_item +>>>>>>> 953f089c06 (feat: Adding requested qty in packed item (#53486)) diff --git a/erpnext/patches/v16_0/update_requested_qty_packed_item.py b/erpnext/patches/v16_0/update_requested_qty_packed_item.py new file mode 100644 index 00000000000..82a636d79bf --- /dev/null +++ b/erpnext/patches/v16_0/update_requested_qty_packed_item.py @@ -0,0 +1,24 @@ +import frappe +from frappe.query_builder.functions import Sum + + +def execute(): + MaterialRequestItem = frappe.qb.DocType("Material Request Item") + + mri_query = ( + frappe.qb.from_(MaterialRequestItem) + .select(MaterialRequestItem.packed_item, Sum(MaterialRequestItem.qty)) + .where((MaterialRequestItem.packed_item.isnotnull()) & (MaterialRequestItem.docstatus == 1)) + .groupby(MaterialRequestItem.packed_item) + ) + + mri_data = mri_query.run() + + if not mri_data: + return + + updates_against_mr = {data[0]: {"requested_qty": data[1]} for data in mri_data} + + frappe.db.auto_commit_on_many_writes = True + frappe.db.bulk_update("Packed Item", updates_against_mr) + frappe.db.auto_commit_on_many_writes = False diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index dbd7f406432..b91b2a4b3d9 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -838,6 +838,7 @@ def close_or_unclose_sales_orders(names, status): def get_requested_item_qty(sales_order): result = {} +<<<<<<< HEAD for d in frappe.db.get_all( "Material Request Item", filters={"docstatus": 1, "sales_order": sales_order}, @@ -845,6 +846,21 @@ def get_requested_item_qty(sales_order): group_by="sales_order_item", ): result[d.sales_order_item] = frappe._dict({"qty": d.qty, "received_qty": d.received_qty}) +======= + + so = frappe.get_doc("Sales Order", sales_order) + + for item in so.items: + if is_product_bundle(item.item_code): + for packed_item in so.get("packed_items"): + if ( + packed_item.parent_item == item.item_code + and packed_item.parent_detail_docname == item.name + ): + result[packed_item.name] = frappe._dict({"qty": packed_item.requested_qty}) + else: + result[item.name] = frappe._dict({"qty": item.requested_qty}) +>>>>>>> 953f089c06 (feat: Adding requested qty in packed item (#53486)) return result @@ -863,12 +879,32 @@ def make_material_request(source_name, target_doc=None): flt(so_item.qty) - flt(requested_item_qty.get(so_item.name, {}).get("qty")) - max( - flt(so_item.get("delivered_qty")) - - flt(requested_item_qty.get(so_item.name, {}).get("received_qty")), + flt(so_item.get("delivered_qty")), 0, ) ) +<<<<<<< HEAD +======= + def get_remaining_packed_item_qty(so_item): + delivered_qty = frappe.db.get_value( + "Sales Order Item", {"name": so_item.parent_detail_docname}, ["delivered_qty"] + ) + + bundle_item_qty = frappe.db.get_value( + "Product Bundle Item", {"parent": so_item.parent_item, "item_code": so_item.item_code}, ["qty"] + ) + + return flt( + flt(so_item.qty) + - flt(requested_item_qty.get(so_item.name, {}).get("qty")) + - max( + flt(delivered_qty) * flt(bundle_item_qty), + 0, + ) + ) + +>>>>>>> 953f089c06 (feat: Adding requested qty in packed item (#53486)) def update_item(source, target, source_parent): # qty is for packed items, because packed items don't have stock_qty field target.project = source_parent.project @@ -923,8 +959,10 @@ def make_material_request(source_name, target_doc=None): target_doc, postprocess, ) - - return doc + if doc and doc.items: + return doc + else: + frappe.throw(_("Material Request already created for the ordered quantity")) @frappe.whitelist() diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 4fffd1f801e..12a3cb549a6 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -57,6 +57,32 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): def tearDown(self): frappe.set_user("Administrator") + def test_sales_order_with_product_bundle_for_partial_material_request(self): + product_bundle = make_product_bundle( + "_Test Product Bundle Item", ["_Test Item", "_Test Item Home Desktop 100"] + ) + so = make_sales_order(item_code=product_bundle.name, qty=2) + mr = make_material_request(so.name) + mr.items[0].qty = 4 + mr.items[1].qty = 2 + mr.items[0].schedule_date = today() + mr.items[1].schedule_date = today() + mr.save() + mr.submit() + mr.reload() + self.assertEqual(mr.items[0].qty, 4) + mr = make_material_request(so.name) + self.assertEqual(mr.items[0].qty, 6) + + def test_sales_order_with_full_material_request(self): + so = make_sales_order(item_code="_Test Item", qty=5, do_not_submit=True) + so.submit() + mr = make_material_request(so.name) + mr.save() + mr.submit() + mr.reload() + self.assertRaises(frappe.ValidationError, make_material_request, so.name) + def test_sales_order_skip_delivery_note(self): so = make_sales_order(do_not_submit=True) so.order_type = "Maintenance" diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 068daeae4f1..195076d54ec 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -87,7 +87,16 @@ class MaterialRequest(BuyingController): "join_field": "sales_order_item", "target_ref_field": "stock_qty", "source_field": "stock_qty", - } + }, + { + "source_dt": "Material Request Item", + "target_dt": "Packed Item", + "target_field": "requested_qty", + "target_parent_dt": "Sales Order", + "join_field": "packed_item", + "target_ref_field": "qty", + "source_field": "qty", + }, ] def check_if_already_pulled(self): diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 31726dff277..726bf4eb1be 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -34,6 +34,7 @@ "projected_qty", "ordered_qty", "packed_qty", + "requested_qty", "column_break_16", "incoming_rate", "picked_qty", @@ -298,13 +299,22 @@ "fieldtype": "Check", "label": "Supplier delivers to Customer", "read_only": 1 + }, + { + "default": "0", + "fieldname": "requested_qty", + "fieldtype": "Float", + "label": "Requested Qty", + "non_negative": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-07-09 19:12:45.850219", + "modified": "2026-03-16 18:10:47.511381", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index d8412dd9dbf..df53537216d 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -45,6 +45,7 @@ class PackedItem(Document): projected_qty: DF.Float qty: DF.Float rate: DF.Currency + requested_qty: DF.Float serial_and_batch_bundle: DF.Link | None serial_no: DF.Text | None target_warehouse: DF.Link | None