From e0e3dcc8bf1fd82332a87b7f1b0309f41bb8b93a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sun, 7 Jun 2026 09:34:21 +0530 Subject: [PATCH] refactor(stock): extract Delivery Note PackingService Move packing slip / product bundle handling into stock/doctype/delivery_note/services/packing.py: - validate_packed_qty stays as a controller delegator (called via hasattr contract in accounts/utils.py); has_unpacked_items stays for onload/JS - get_product_bundle_list and cancel_packing_slips move into the service (internal-only; on_cancel repointed) Pre-existing raw SQL in cancel_packing_slips moved verbatim. No behaviour change. --- .../doctype/delivery_note/delivery_note.py | 50 ++------------- .../doctype/delivery_note/services/packing.py | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+), 46 deletions(-) create mode 100644 erpnext/stock/doctype/delivery_note/services/packing.py diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 5b9e44ae1f0..7bfd275ee2a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -8,6 +8,7 @@ from frappe.utils import cint, flt from erpnext.controllers.selling_controller import SellingController from erpnext.stock.doctype.delivery_note.services.billing_status import BillingStatusService +from erpnext.stock.doctype.delivery_note.services.packing import PackingService from erpnext.stock.doctype.packed_item.packed_item import make_packing_list form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -509,7 +510,7 @@ class DeliveryNote(SellingController): # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() - self.cancel_packing_slips() + PackingService(self).cancel_packing_slips() self.update_pick_list_status() self.make_gl_entries_on_cancel() @@ -600,20 +601,7 @@ class DeliveryNote(SellingController): def validate_packed_qty(self): """Validate that if packed qty exists, it should be equal to qty""" - - if frappe.db.exists("Packing Slip", {"docstatus": 1, "delivery_note": self.name}): - product_bundle_list = self.get_product_bundle_list() - for item in self.items + self.packed_items: - if ( - item.item_code not in product_bundle_list - and flt(item.packed_qty) - and flt(item.packed_qty) != flt(item.qty) - ): - frappe.throw( - _("Row {0}: Packed Qty must be equal to {1} Qty.").format( - item.idx, frappe.bold(item.doctype) - ) - ) + PackingService(self).validate_packed_qty() def check_next_docstatus(self): submit_rv = frappe.db.sql( @@ -634,22 +622,6 @@ class DeliveryNote(SellingController): if submit_in: frappe.throw(_("Installation Note {0} has already been submitted").format(submit_in[0][0])) - def cancel_packing_slips(self): - """ - Cancel submitted packing slips related to this delivery note - """ - res = frappe.db.sql( - """SELECT name FROM `tabPacking Slip` WHERE delivery_note = %s - AND docstatus = 1""", - self.name, - ) - - if res: - for r in res: - ps = frappe.get_doc("Packing Slip", r[0]) - ps.cancel() - frappe.msgprint(_("Packing Slip(s) cancelled")) - def update_status(self, status): BillingStatusService(self).update_status(status) @@ -657,21 +629,7 @@ class DeliveryNote(SellingController): BillingStatusService(self).update_billing_status(update_modified) def has_unpacked_items(self): - product_bundle_list = self.get_product_bundle_list() - - for item in self.items + self.packed_items: - if item.item_code not in product_bundle_list and flt(item.packed_qty) < flt(item.qty): - return True - - return False - - def get_product_bundle_list(self): - items_list = [item.item_code for item in self.items] - return frappe.db.get_all( - "Product Bundle", - filters={"new_item_code": ["in", items_list], "disabled": 0}, - pluck="name", - ) + return PackingService(self).has_unpacked_items() def get_list_context(context=None): diff --git a/erpnext/stock/doctype/delivery_note/services/packing.py b/erpnext/stock/doctype/delivery_note/services/packing.py new file mode 100644 index 00000000000..f26ba2897e9 --- /dev/null +++ b/erpnext/stock/doctype/delivery_note/services/packing.py @@ -0,0 +1,63 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +"""Packing slip and product bundle handling for Delivery Note.""" + +import frappe +from frappe import _ +from frappe.utils import flt + + +class PackingService: + def __init__(self, doc): + self.doc = doc + + def validate_packed_qty(self) -> None: + """Validate that if packed qty exists, it should be equal to qty""" + doc = self.doc + + if frappe.db.exists("Packing Slip", {"docstatus": 1, "delivery_note": doc.name}): + product_bundle_list = self.get_product_bundle_list() + for item in doc.items + doc.packed_items: + if ( + item.item_code not in product_bundle_list + and flt(item.packed_qty) + and flt(item.packed_qty) != flt(item.qty) + ): + frappe.throw( + _("Row {0}: Packed Qty must be equal to {1} Qty.").format( + item.idx, frappe.bold(item.doctype) + ) + ) + + def has_unpacked_items(self) -> bool: + doc = self.doc + product_bundle_list = self.get_product_bundle_list() + + for item in doc.items + doc.packed_items: + if item.item_code not in product_bundle_list and flt(item.packed_qty) < flt(item.qty): + return True + + return False + + def get_product_bundle_list(self) -> list[str]: + items_list = [item.item_code for item in self.doc.items] + return frappe.db.get_all( + "Product Bundle", + filters={"new_item_code": ["in", items_list], "disabled": 0}, + pluck="name", + ) + + def cancel_packing_slips(self) -> None: + """Cancel submitted packing slips related to this delivery note""" + res = frappe.db.sql( + """SELECT name FROM `tabPacking Slip` WHERE delivery_note = %s + AND docstatus = 1""", + self.doc.name, + ) + + if res: + for r in res: + ps = frappe.get_doc("Packing Slip", r[0]) + ps.cancel() + frappe.msgprint(_("Packing Slip(s) cancelled"))