feat: Adding requested qty in packed item (backport #53486) (#53521)

* feat: Adding requested qty in packed item (#53486)

* feat: Adding requested qty in packed item

* fix: correct import path

---------

Co-authored-by: Nishka Gosalia <nishkagosalia@Nishkas-MacBook-Air.local>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit 953f089c06)

# Conflicts:
#	erpnext/patches.txt

* chore: resolve conflicts

---------

Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
This commit is contained in:
mergify[bot]
2026-03-16 18:51:35 +00:00
committed by GitHub
parent e72f398b7c
commit 8753ed9992
8 changed files with 100 additions and 30 deletions

View File

@@ -444,7 +444,10 @@ class StatusUpdater(Document):
): ):
return 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": if qty_or_amount == "qty":
action_msg = _( action_msg = _(
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'

View File

@@ -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.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.update_order_qty_and_requested_qty_based_on_mr_and_po
erpnext.patches.v16_0.enable_serial_batch_setting erpnext.patches.v16_0.enable_serial_batch_setting
erpnext.patches.v16_0.update_requested_qty_packed_item

View File

@@ -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

View File

@@ -1003,20 +1003,19 @@ def close_or_unclose_sales_orders(names, status):
def get_requested_item_qty(sales_order): def get_requested_item_qty(sales_order):
result = {} result = {}
for d in frappe.db.get_all(
"Material Request Item", so = frappe.get_doc("Sales Order", sales_order)
filters={"docstatus": 1, "sales_order": sales_order},
fields=[ for item in so.items:
"sales_order_item", if is_product_bundle(item.item_code):
"packed_item", for packed_item in so.get("packed_items"):
{"SUM": "qty", "as": "qty"}, if (
{"SUM": "received_qty", "as": "received_qty"}, packed_item.parent_item == item.item_code
], and packed_item.parent_detail_docname == item.name
group_by="sales_order_item, packed_item", ):
): result[packed_item.name] = frappe._dict({"qty": packed_item.requested_qty})
result[d.sales_order_item or d.packed_item] = frappe._dict( else:
{"qty": d.qty, "received_qty": d.received_qty} result[item.name] = frappe._dict({"qty": item.requested_qty})
)
return result return result
@@ -1035,8 +1034,7 @@ def make_material_request(source_name, target_doc=None):
flt(so_item.qty) flt(so_item.qty)
- flt(requested_item_qty.get(so_item.name, {}).get("qty")) - flt(requested_item_qty.get(so_item.name, {}).get("qty"))
- max( - max(
flt(so_item.get("delivered_qty")) flt(so_item.get("delivered_qty")),
- flt(requested_item_qty.get(so_item.name, {}).get("received_qty")),
0, 0,
) )
) )
@@ -1051,16 +1049,12 @@ def make_material_request(source_name, target_doc=None):
) )
return flt( return flt(
( flt(so_item.qty)
flt(so_item.qty) - flt(requested_item_qty.get(so_item.name, {}).get("qty"))
- flt(requested_item_qty.get(so_item.name, {}).get("qty")) - max(
- max( flt(delivered_qty) * flt(bundle_item_qty),
flt(delivered_qty) * flt(bundle_item_qty) 0,
- flt(requested_item_qty.get(so_item.name, {}).get("received_qty")),
0,
)
) )
* bundle_item_qty
) )
def update_item(source, target, source_parent): def update_item(source, target, source_parent):
@@ -1122,8 +1116,10 @@ def make_material_request(source_name, target_doc=None):
target_doc, target_doc,
postprocess, postprocess,
) )
if doc and doc.items:
return doc return doc
else:
frappe.throw(_("Material Request already created for the ordered quantity"))
@frappe.whitelist() @frappe.whitelist()

View File

@@ -57,6 +57,32 @@ class TestSalesOrder(AccountsTestMixin, IntegrationTestCase):
frappe.db.rollback() frappe.db.rollback()
frappe.set_user("Administrator") 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): def test_sales_order_skip_delivery_note(self):
so = make_sales_order(do_not_submit=True) so = make_sales_order(do_not_submit=True)
so.order_type = "Maintenance" so.order_type = "Maintenance"

View File

@@ -95,7 +95,16 @@ class MaterialRequest(BuyingController):
"join_field": "sales_order_item", "join_field": "sales_order_item",
"target_ref_field": "stock_qty", "target_ref_field": "stock_qty",
"source_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): def check_if_already_pulled(self):

View File

@@ -34,6 +34,7 @@
"projected_qty", "projected_qty",
"ordered_qty", "ordered_qty",
"packed_qty", "packed_qty",
"requested_qty",
"column_break_16", "column_break_16",
"incoming_rate", "incoming_rate",
"picked_qty", "picked_qty",
@@ -298,13 +299,22 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Supplier delivers to Customer", "label": "Supplier delivers to Customer",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fieldname": "requested_qty",
"fieldtype": "Float",
"label": "Requested Qty",
"non_negative": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-07-09 19:12:45.850219", "modified": "2026-03-16 18:10:47.511381",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Packed Item", "name": "Packed Item",

View File

@@ -45,6 +45,7 @@ class PackedItem(Document):
projected_qty: DF.Float projected_qty: DF.Float
qty: DF.Float qty: DF.Float
rate: DF.Currency rate: DF.Currency
requested_qty: DF.Float
serial_and_batch_bundle: DF.Link | None serial_and_batch_bundle: DF.Link | None
serial_no: DF.Text | None serial_no: DF.Text | None
target_warehouse: DF.Link | None target_warehouse: DF.Link | None