From 77f1e8ce78742cfbf157b6fe4ab263ee75c20052 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 15:04:41 +0530 Subject: [PATCH] fix: update `Packed Qty` in DN on submit and cancel of `Packing Slip` --- .../doctype/packing_slip/packing_slip.py | 196 +++++++----------- 1 file changed, 72 insertions(+), 124 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index 415c9e86b5..d1c122d046 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -4,159 +4,107 @@ import frappe from frappe import _ -from frappe.model import no_value_fields -from frappe.model.document import Document -from frappe.utils import cint, flt +from frappe.utils import cint + +from erpnext.controllers.status_updater import StatusUpdater -class PackingSlip(Document): - def validate(self): - """ - * Validate existence of submitted Delivery Note - * Case nos do not overlap - * Check if packed qty doesn't exceed actual qty of delivery note - - It is necessary to validate case nos before checking quantity - """ - self.validate_delivery_note() - self.validate_items_mandatory() - self.validate_case_nos() - self.validate_qty() +class PackingSlip(StatusUpdater): + def __init__(self, *args, **kwargs) -> None: + super(PackingSlip, self).__init__(*args, **kwargs) + self.status_updater = [ + { + "target_dt": "Delivery Note Item", + "join_field": "dn_detail", + "target_field": "packed_qty", + "target_parent_dt": "Delivery Note", + "target_ref_field": "qty", + "source_dt": "Packing Slip Item", + "source_field": "qty", + }, + { + "target_dt": "Packed Item", + "join_field": "pi_detail", + "target_field": "packed_qty", + "target_parent_dt": "Delivery Note", + "target_ref_field": "qty", + "source_dt": "Packing Slip Item", + "source_field": "qty", + }, + ] + def validate(self) -> None: from erpnext.utilities.transaction_base import validate_uom_is_integer + self.validate_delivery_note() + self.validate_case_nos() + validate_uom_is_integer(self, "stock_uom", "qty") validate_uom_is_integer(self, "weight_uom", "net_weight") self.set_missing_values() + def on_submit(self): + self.update_prevdoc_status() + + def on_cancel(self): + self.update_prevdoc_status() + def validate_delivery_note(self): - """ - Validates if delivery note has status as draft - """ + """Raises an exception if the `Delivery Note` status is not Draft""" + if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0: - frappe.throw(_("Delivery Note {0} must not be submitted").format(self.delivery_note)) - - def validate_items_mandatory(self): - rows = [d.item_code for d in self.get("items")] - if not rows: - frappe.msgprint(_("No Items to pack"), raise_exception=1) - - def validate_case_nos(self): - """ - Validate if case nos overlap. If they do, recommend next case no. - """ - if not cint(self.from_case_no): - frappe.msgprint(_("Please specify a valid 'From Case No.'"), raise_exception=1) - elif not self.to_case_no: - self.to_case_no = self.from_case_no - elif cint(self.from_case_no) > cint(self.to_case_no): - frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), raise_exception=1) - - res = frappe.db.sql( - """SELECT name FROM `tabPacking Slip` - WHERE delivery_note = %(delivery_note)s AND docstatus = 1 AND - ((from_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s) - OR (to_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s) - OR (%(from_case_no)s BETWEEN from_case_no AND to_case_no)) - """, - { - "delivery_note": self.delivery_note, - "from_case_no": self.from_case_no, - "to_case_no": self.to_case_no, - }, - ) - - if res: frappe.throw( - _("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no()) + _("A Packing Slip can only be created for Draft Delivery Note.").format(self.delivery_note) ) - def validate_qty(self): - """Check packed qty across packing slips and delivery note""" - # Get Delivery Note Items, Item Quantity Dict and No. of Cases for this Packing slip - dn_details, ps_item_qty, no_of_cases = self.get_details_for_packing() + def validate_case_nos(self): + """Validate if case nos overlap. If they do, recommend next case no.""" - for item in dn_details: - new_packed_qty = (flt(ps_item_qty[item["item_code"]]) * no_of_cases) + flt(item["packed_qty"]) - if new_packed_qty > flt(item["qty"]) and no_of_cases: - self.recommend_new_qty(item, ps_item_qty, no_of_cases) + if not self.to_case_no: + self.to_case_no = self.from_case_no + elif cint(self.from_case_no) > cint(self.to_case_no): + frappe.throw(_("'To Package No.' cannot be less than 'From Package No.'")) + else: + ps = frappe.qb.DocType("Packing Slip") + res = ( + frappe.qb.from_(ps) + .select( + ps.name, + ) + .where( + (ps.delivery_note == self.delivery_note) + & (ps.docstatus == 1) + & ( + (ps.from_case_no.between(self.from_case_no, self.to_case_no)) + | (ps.to_case_no.between(self.from_case_no, self.to_case_no)) + | ((ps.from_case_no <= self.from_case_no) & (ps.to_case_no >= self.from_case_no)) + ) + ) + ).run() + + if res: + frappe.throw( + _("""Package No(s) already in use. Try from Package No {0}""").format( + self.get_recommended_case_no() + ) + ) def set_missing_values(self): if not self.from_case_no: self.from_case_no = self.get_recommended_case_no() for item in self.items: - weight_per_unit, weight_uom = frappe.db.get_value( - "Item", item.item_code, ["weight_per_unit", "weight_uom"] + stock_uom, weight_per_unit, weight_uom = frappe.db.get_value( + "Item", item.item_code, ["stock_uom", "weight_per_unit", "weight_uom"] ) + item.stock_uom = stock_uom if weight_per_unit and not item.net_weight: item.net_weight = weight_per_unit if weight_uom and not item.weight_uom: item.weight_uom = weight_uom - def get_details_for_packing(self): - """ - Returns - * 'Delivery Note Items' query result as a list of dict - * Item Quantity dict of current packing slip doc - * No. of Cases of this packing slip - """ - - rows = [d.item_code for d in self.get("items")] - - # also pick custom fields from delivery note - custom_fields = ", ".join( - "dni.`{0}`".format(d.fieldname) - for d in frappe.get_meta("Delivery Note Item").get_custom_fields() - if d.fieldtype not in no_value_fields - ) - - if custom_fields: - custom_fields = ", " + custom_fields - - condition = "" - if rows: - condition = " and item_code in (%s)" % (", ".join(["%s"] * len(rows))) - - # gets item code, qty per item code, latest packed qty per item code and stock uom - res = frappe.db.sql( - """select item_code, sum(qty) as qty, - (select sum(psi.qty * (abs(ps.to_case_no - ps.from_case_no) + 1)) - from `tabPacking Slip` ps, `tabPacking Slip Item` psi - where ps.name = psi.parent and ps.docstatus = 1 - and ps.delivery_note = dni.parent and psi.item_code=dni.item_code) as packed_qty, - stock_uom, item_name, description, dni.batch_no {custom_fields} - from `tabDelivery Note Item` dni - where parent=%s {condition} - group by item_code""".format( - condition=condition, custom_fields=custom_fields - ), - tuple([self.delivery_note] + rows), - as_dict=1, - ) - - ps_item_qty = dict([[d.item_code, d.qty] for d in self.get("items")]) - no_of_cases = cint(self.to_case_no) - cint(self.from_case_no) + 1 - - return res, ps_item_qty, no_of_cases - - def recommend_new_qty(self, item, ps_item_qty, no_of_cases): - """ - Recommend a new quantity and raise a validation exception - """ - item["recommended_qty"] = (flt(item["qty"]) - flt(item["packed_qty"])) / no_of_cases - item["specified_qty"] = flt(ps_item_qty[item["item_code"]]) - if not item["packed_qty"]: - item["packed_qty"] = 0 - - frappe.throw( - _("Quantity for Item {0} must be less than {1}").format( - item.get("item_code"), item.get("recommended_qty") - ) - ) - def get_recommended_case_no(self): """Returns the next case no. for a new packing slip for a delivery note"""