diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 4ec5e739143..fe0f8a831aa 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -444,7 +444,10 @@ class StatusUpdater(Document): ): return - if args["source_dt"] != "Pick List Item" and args["target_dt"] != "Quotation Item": + if args["source_dt"] != "Pick List Item" and args["target_dt"] not in [ + "Quotation Item", + "Packed Item", + ]: 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 c80d2a2969b..40ff506d3e8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -470,3 +470,4 @@ erpnext.patches.v16_0.complete_onboarding_steps_for_older_sites #2 erpnext.patches.v16_0.migrate_asset_type_checkboxes_to_select erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po erpnext.patches.v16_0.enable_serial_batch_setting +erpnext.patches.v16_0.update_requested_qty_packed_item 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 5c6f64278af..7918dced389 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1003,20 +1003,19 @@ def close_or_unclose_sales_orders(names, status): def get_requested_item_qty(sales_order): result = {} - for d in frappe.db.get_all( - "Material Request Item", - filters={"docstatus": 1, "sales_order": sales_order}, - fields=[ - "sales_order_item", - "packed_item", - {"SUM": "qty", "as": "qty"}, - {"SUM": "received_qty", "as": "received_qty"}, - ], - group_by="sales_order_item, packed_item", - ): - result[d.sales_order_item or d.packed_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}) return result @@ -1035,8 +1034,7 @@ 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, ) ) @@ -1051,16 +1049,12 @@ def make_material_request(source_name, target_doc=None): ) return flt( - ( - flt(so_item.qty) - - flt(requested_item_qty.get(so_item.name, {}).get("qty")) - - max( - flt(delivered_qty) * flt(bundle_item_qty) - - flt(requested_item_qty.get(so_item.name, {}).get("received_qty")), - 0, - ) + flt(so_item.qty) + - flt(requested_item_qty.get(so_item.name, {}).get("qty")) + - max( + flt(delivered_qty) * flt(bundle_item_qty), + 0, ) - * bundle_item_qty ) def update_item(source, target, source_parent): @@ -1122,8 +1116,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 6c7ae9c0660..405583aeca0 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, IntegrationTestCase): frappe.db.rollback() 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 09d33a36463..24e8ac4b1a9 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -95,7 +95,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 8938d503356..1614bfe2ab7 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 f8bb88f6151..5a6531f1526 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