From eca77134ae46d487da9889657f44dfc7b77551c2 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 27 Apr 2023 15:48:02 +0530 Subject: [PATCH 01/21] feat: add field `pi_detail` in `Packing Slip` --- .../packing_slip_item/packing_slip_item.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json index 4270839bfdbe..4a566b6ff2cf 100644 --- a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json +++ b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json @@ -20,7 +20,8 @@ "stock_uom", "weight_uom", "page_break", - "dn_detail" + "dn_detail", + "pi_detail" ], "fields": [ { @@ -121,13 +122,21 @@ "fieldtype": "Data", "hidden": 1, "in_list_view": 1, - "label": "DN Detail" + "label": "Delivery Note Item", + "read_only": 1 + }, + { + "fieldname": "pi_detail", + "fieldtype": "Data", + "hidden": 1, + "label": "Delivery Note Packed Item", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-12-14 01:22:00.715935", + "modified": "2023-04-27 15:37:17.023153", "modified_by": "Administrator", "module": "Stock", "name": "Packing Slip Item", @@ -136,5 +145,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 380dd730650a8512c0d7ce485357ed9301ce9a6a Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 27 Apr 2023 18:37:32 +0530 Subject: [PATCH 02/21] fix: map `Packed Items` while creating `Packing Slip` --- .../doctype/delivery_note/delivery_note.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 9f6dd24fa6c0..f477e3529262 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -698,8 +698,27 @@ def make_packing_slip(source_name, target_doc=None): "field_map": { "item_code": "item_code", "item_name": "item_name", + "batch_no": "batch_no", + "description": "description", + "qty": "qty", + "total_weight": "net_weight", + "stock_uom": "stock_uom", + "weight_uom": "weight_uom", + "name": "dn_detail", + }, + "condition": lambda doc: not frappe.db.exists( + "Product Bundle", {"new_item_code": doc.item_code} + ), + }, + "Packed Item": { + "doctype": "Packing Slip Item", + "field_map": { + "item_code": "item_code", + "item_name": "item_name", + "batch_no": "batch_no", "description": "description", "qty": "qty", + "name": "pi_detail", }, }, }, From b62bf788146a689481598ed3009ec2509272193c Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 27 Apr 2023 19:43:37 +0530 Subject: [PATCH 03/21] refactor: `packing_slip.js` --- .../doctype/packing_slip/packing_slip.js | 209 +++++++++--------- 1 file changed, 109 insertions(+), 100 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js index 40d46852d038..f9cd2bf08c6d 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.js +++ b/erpnext/stock/doctype/packing_slip/packing_slip.js @@ -1,113 +1,122 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// License: GNU General Public License v3. See license.txt - -cur_frm.fields_dict['delivery_note'].get_query = function(doc, cdt, cdn) { - return{ - filters:{ 'docstatus': 0} - } -} - - -cur_frm.fields_dict['items'].grid.get_field('item_code').get_query = function(doc, cdt, cdn) { - if(!doc.delivery_note) { - frappe.throw(__("Please select a Delivery Note")); - } else { - return { - query: "erpnext.stock.doctype.packing_slip.packing_slip.item_details", - filters:{ 'delivery_note': doc.delivery_note} - } - } -} - -cur_frm.cscript.onload_post_render = function(doc, cdt, cdn) { - if(doc.delivery_note && doc.__islocal) { - cur_frm.cscript.get_items(doc, cdt, cdn); - } -} - -cur_frm.cscript.get_items = function(doc, cdt, cdn) { - return this.frm.call({ - doc: this.frm.doc, - method: "get_items", - callback: function(r) { - if(!r.exc) cur_frm.refresh(); +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Packing Slip", { + setup: (frm) => { + frm.set_query('delivery_note', () => { + return { + filters: { + docstatus: 0, + } + } + }); + + frm.set_query('item_code', 'items', (doc, cdt, cdn) => { + if (!doc.delivery_note) { + frappe.throw(__("Please select a Delivery Note")); + } else { + let d = locals[cdt][cdn]; + return { + query: 'erpnext.stock.doctype.packing_slip.packing_slip.item_details', + filters: { + delivery_note: doc.delivery_note, + } + } + } + }); + }, + + refresh: (frm) => { + frm.toggle_display("misc_details", frm.doc.amended_from); + }, + + validate: (frm) => { + frm.trigger("validate_case_nos"); + frm.trigger("validate_calculate_item_details"); + }, + + onload_post_render: (frm) => { + if(frm.doc.delivery_note && frm.doc.__islocal) { + frm.trigger("get_items"); } - }); -} - -cur_frm.cscript.refresh = function(doc, dt, dn) { - cur_frm.toggle_display("misc_details", doc.amended_from); -} - -cur_frm.cscript.validate = function(doc, cdt, cdn) { - cur_frm.cscript.validate_case_nos(doc); - cur_frm.cscript.validate_calculate_item_details(doc); -} - -// To Case No. cannot be less than From Case No. -cur_frm.cscript.validate_case_nos = function(doc) { - doc = locals[doc.doctype][doc.name]; - if(cint(doc.from_case_no)==0) { - frappe.msgprint(__("The 'From Package No.' field must neither be empty nor it's value less than 1.")); - frappe.validated = false; - } else if(!cint(doc.to_case_no)) { - doc.to_case_no = doc.from_case_no; - refresh_field('to_case_no'); - } else if(cint(doc.to_case_no) < cint(doc.from_case_no)) { - frappe.msgprint(__("'To Case No.' cannot be less than 'From Case No.'")); - frappe.validated = false; - } -} - - -cur_frm.cscript.validate_calculate_item_details = function(doc) { - doc = locals[doc.doctype][doc.name]; - var ps_detail = doc.items || []; + }, + + get_items: (frm) => { + return frm.call({ + doc: frm.doc, + method: "get_items", + callback: function(r) { + if(!r.exc) { + frm.refresh(); + } + } + }); + }, - cur_frm.cscript.validate_duplicate_items(doc, ps_detail); - cur_frm.cscript.calc_net_total_pkg(doc, ps_detail); -} + // To Case No. cannot be less than From Case No. + validate_case_nos: (frm) => { + doc = locals[frm.doc.doctype][frm.doc.name]; + if(cint(doc.from_case_no) == 0) { + frappe.msgprint(__("The 'From Package No.' field must neither be empty nor it's value less than 1.")); + frappe.validated = false; + } else if(!cint(doc.to_case_no)) { + doc.to_case_no = doc.from_case_no; + refresh_field('to_case_no'); + } else if(cint(doc.to_case_no) < cint(doc.from_case_no)) { + frappe.msgprint(__("'To Case No.' cannot be less than 'From Case No.'")); + frappe.validated = false; + } + }, + + validate_calculate_item_details: (frm) => { + doc = locals[frm.doc.doctype][frm.doc.name]; + var ps_detail = doc.items || []; + + frm.events.validate_duplicate_items(doc, ps_detail); + frm.events.calc_net_total_pkg(doc, ps_detail); + }, + + // Do not allow duplicate items i.e. items with same item_code + // Also check for 0 qty + validate_duplicate_items: (doc, ps_detail) => { + for(var i=0; i { + var net_weight_pkg = 0; + doc.net_weight_uom = (ps_detail && ps_detail.length) ? ps_detail[0].weight_uom : ''; + doc.gross_weight_uom = doc.net_weight_uom; + + for(var i=0; i Date: Fri, 28 Apr 2023 09:06:50 +0530 Subject: [PATCH 04/21] fix: remove duplicate items validation --- .../doctype/packing_slip/packing_slip.js | 48 +++++++------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js index f9cd2bf08c6d..fb91930273ce 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.js +++ b/erpnext/stock/doctype/packing_slip/packing_slip.js @@ -70,51 +70,37 @@ frappe.ui.form.on("Packing Slip", { }, validate_calculate_item_details: (frm) => { - doc = locals[frm.doc.doctype][frm.doc.name]; - var ps_detail = doc.items || []; - - frm.events.validate_duplicate_items(doc, ps_detail); - frm.events.calc_net_total_pkg(doc, ps_detail); + frm.trigger("validate_items_qty"); + frm.trigger("calc_net_total_pkg"); }, - // Do not allow duplicate items i.e. items with same item_code - // Also check for 0 qty - validate_duplicate_items: (doc, ps_detail) => { - for(var i=0; i { + frm.doc.items.forEach(item => { + if (item.qty <= 0) { + frappe.msgprint(__("Invalid quantity specified for item {0}. Quantity should be greater than 0.", [item.item_code])); frappe.validated = false; } - } + }); }, - // Calculate Net Weight of Package - calc_net_total_pkg: (doc, ps_detail) => { + calc_net_total_pkg: (frm) => { var net_weight_pkg = 0; - doc.net_weight_uom = (ps_detail && ps_detail.length) ? ps_detail[0].weight_uom : ''; - doc.gross_weight_uom = doc.net_weight_uom; + var items = frm.doc.items || []; + frm.doc.net_weight_uom = (items && items.length) ? items[0].weight_uom : ''; + frm.doc.gross_weight_uom = frm.doc.net_weight_uom; - for(var i=0; i { + if(item.weight_uom != frm.doc.net_weight_uom) { frappe.msgprint(__("Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM.")); frappe.validated = false; } net_weight_pkg += flt(item.net_weight) * flt(item.qty); - } + }); - doc.net_weight_pkg = roundNumber(net_weight_pkg, 2); + frm.doc.net_weight_pkg = roundNumber(net_weight_pkg, 2); - if(!flt(doc.gross_weight_pkg)) { - doc.gross_weight_pkg = doc.net_weight_pkg; + if(!flt(frm.doc.gross_weight_pkg)) { + frm.doc.gross_weight_pkg = frm.doc.net_weight_pkg; } refresh_many(['net_weight_pkg', 'net_weight_uom', 'gross_weight_uom', 'gross_weight_pkg']); From 75fe9dd3ea10342bbbcb29f5fa17a467a3fe7637 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 10:38:02 +0530 Subject: [PATCH 05/21] fix: don't map items twice * don't explicitly map Delivery Note Item custom fields to Packing Slip Item, get auto-mapped while mapping the doc. * call Packing List `set_missing_values` after mapping the doc. * refactor `get_recommended_case_no`, use `frappe.db.get_value` instead of `frappe.db.sql`. --- .../doctype/delivery_note/delivery_note.py | 6 +- .../doctype/packing_slip/packing_slip.js | 18 ----- .../doctype/packing_slip/packing_slip.py | 73 +++++++------------ 3 files changed, 29 insertions(+), 68 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index f477e3529262..44fb9ab2fa5e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -684,6 +684,9 @@ def update_item(obj, target, source_parent): @frappe.whitelist() def make_packing_slip(source_name, target_doc=None): + def set_missing_values(source, target): + target.run_method("set_missing_values") + doclist = get_mapped_doc( "Delivery Note", source_name, @@ -701,9 +704,7 @@ def make_packing_slip(source_name, target_doc=None): "batch_no": "batch_no", "description": "description", "qty": "qty", - "total_weight": "net_weight", "stock_uom": "stock_uom", - "weight_uom": "weight_uom", "name": "dn_detail", }, "condition": lambda doc: not frappe.db.exists( @@ -723,6 +724,7 @@ def make_packing_slip(source_name, target_doc=None): }, }, target_doc, + set_missing_values, ) return doclist diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js index fb91930273ce..85a611f2b1f8 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.js +++ b/erpnext/stock/doctype/packing_slip/packing_slip.js @@ -35,24 +35,6 @@ frappe.ui.form.on("Packing Slip", { frm.trigger("validate_calculate_item_details"); }, - onload_post_render: (frm) => { - if(frm.doc.delivery_note && frm.doc.__islocal) { - frm.trigger("get_items"); - } - }, - - get_items: (frm) => { - return frm.call({ - doc: frm.doc, - method: "get_items", - callback: function(r) { - if(!r.exc) { - frm.refresh(); - } - } - }); - }, - // To Case No. cannot be less than From Case No. validate_case_nos: (frm) => { doc = locals[frm.doc.doctype][frm.doc.name]; diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index e5b9de8789ff..415c9e86b56e 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -28,6 +28,8 @@ def validate(self): validate_uom_is_integer(self, "stock_uom", "qty") validate_uom_is_integer(self, "weight_uom", "net_weight") + self.set_missing_values() + def validate_delivery_note(self): """ Validates if delivery note has status as draft @@ -80,6 +82,20 @@ def validate_qty(self): if new_packed_qty > flt(item["qty"]) and no_of_cases: self.recommend_new_qty(item, ps_item_qty, no_of_cases) + 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"] + ) + + 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 @@ -141,56 +157,17 @@ def recommend_new_qty(self, item, ps_item_qty, no_of_cases): ) ) - def update_item_details(self): - """ - Fill empty columns in Packing Slip Item - """ - if not self.from_case_no: - self.from_case_no = self.get_recommended_case_no() - - for d in self.get("items"): - res = frappe.db.get_value("Item", d.item_code, ["weight_per_unit", "weight_uom"], as_dict=True) - - if res and len(res) > 0: - d.net_weight = res["weight_per_unit"] - d.weight_uom = res["weight_uom"] - def get_recommended_case_no(self): - """ - Returns the next case no. for a new packing slip for a delivery - note - """ - recommended_case_no = frappe.db.sql( - """SELECT MAX(to_case_no) FROM `tabPacking Slip` - WHERE delivery_note = %s AND docstatus=1""", - self.delivery_note, - ) - - return cint(recommended_case_no[0][0]) + 1 + """Returns the next case no. for a new packing slip for a delivery note""" - @frappe.whitelist() - def get_items(self): - self.set("items", []) - - custom_fields = frappe.get_meta("Delivery Note Item").get_custom_fields() - - dn_details = self.get_details_for_packing()[0] - for item in dn_details: - if flt(item.qty) > flt(item.packed_qty): - ch = self.append("items", {}) - ch.item_code = item.item_code - ch.item_name = item.item_name - ch.stock_uom = item.stock_uom - ch.description = item.description - ch.batch_no = item.batch_no - ch.qty = flt(item.qty) - flt(item.packed_qty) - - # copy custom fields - for d in custom_fields: - if item.get(d.fieldname): - ch.set(d.fieldname, item.get(d.fieldname)) - - self.update_item_details() + return ( + cint( + frappe.db.get_value( + "Packing Slip", {"delivery_note": self.delivery_note, "docstatus": 1}, ["max(to_case_no)"] + ) + ) + + 1 + ) @frappe.whitelist() From e6fc281acfd91b7107d6bf2c78c61a31b35c3529 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 13:20:47 +0530 Subject: [PATCH 06/21] feat: add field `Packed Qty` in `Delivery Note Item` and `Packed Item` --- .../delivery_note_item/delivery_note_item.json | 13 ++++++++++++- erpnext/stock/doctype/packed_item/packed_item.json | 13 ++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 180adee0cb08..a2e3cc11a72b 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -84,6 +84,7 @@ "installed_qty", "item_tax_rate", "column_break_atna", + "packed_qty", "received_qty", "accounting_details_section", "expense_account", @@ -848,13 +849,23 @@ "print_hide": 1, "read_only": 1, "report_hide": 1 + }, + { + "default": "0", + "depends_on": "eval: doc.packed_qty", + "fieldname": "packed_qty", + "fieldtype": "Float", + "label": "Packed Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-04-06 09:28:29.182053", + "modified": "2023-04-28 13:14:10.648655", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index cb8eb30cb301..c5fb2411c281 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -27,6 +27,7 @@ "actual_qty", "projected_qty", "ordered_qty", + "packed_qty", "column_break_16", "incoming_rate", "picked_qty", @@ -242,13 +243,23 @@ "label": "Picked Qty", "no_copy": 1, "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval: doc.packed_qty", + "fieldname": "packed_qty", + "fieldtype": "Float", + "label": "Packed Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-04-27 05:23:08.683245", + "modified": "2023-04-28 13:16:38.460806", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", From 77f1e8ce78742cfbf157b6fe4ab263ee75c20052 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 15:04:41 +0530 Subject: [PATCH 07/21] fix: update `Packed Qty` in DN on submit and cancel of `Packing Slip` --- .../doctype/packing_slip/packing_slip.py | 192 +++++++----------- 1 file changed, 70 insertions(+), 122 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index 415c9e86b56e..d1c122d046aa 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 validate_delivery_note(self): - """ - Validates if delivery note has status as 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 on_submit(self): + self.update_prevdoc_status() - 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 on_cancel(self): + self.update_prevdoc_status() - 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, - }, - ) + def validate_delivery_note(self): + """Raises an exception if the `Delivery Note` status is not Draft""" - if res: + if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0: 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""" From 0add90e7ec8b9bdad8500de381cb98990e0c513a Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 15:05:28 +0530 Subject: [PATCH 08/21] chore: enable `no_copy` for `dn_detail` and `pi_detail` in Packing Slip Item --- .../stock/doctype/packing_slip_item/packing_slip_item.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json index 4a566b6ff2cf..4bd90355acbe 100644 --- a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json +++ b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json @@ -123,6 +123,7 @@ "hidden": 1, "in_list_view": 1, "label": "Delivery Note Item", + "no_copy": 1, "read_only": 1 }, { @@ -130,13 +131,14 @@ "fieldtype": "Data", "hidden": 1, "label": "Delivery Note Packed Item", + "no_copy": 1, "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-04-27 15:37:17.023153", + "modified": "2023-04-28 15:00:14.079306", "modified_by": "Administrator", "module": "Stock", "name": "Packing Slip Item", From 372bce45675ac0232219097cc191bc662962a49f Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 15:24:23 +0530 Subject: [PATCH 09/21] fix: Packing Slip Item Qty --- erpnext/stock/doctype/delivery_note/delivery_note.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 44fb9ab2fa5e..1e34296c787c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -687,6 +687,9 @@ def make_packing_slip(source_name, target_doc=None): def set_missing_values(source, target): target.run_method("set_missing_values") + def update_item(obj, target, source_parent): + target.qty = flt(obj.qty) - flt(obj.packed_qty) + doclist = get_mapped_doc( "Delivery Note", source_name, @@ -707,8 +710,10 @@ def set_missing_values(source, target): "stock_uom": "stock_uom", "name": "dn_detail", }, - "condition": lambda doc: not frappe.db.exists( - "Product Bundle", {"new_item_code": doc.item_code} + "postprocess": update_item, + "condition": lambda doc: ( + not frappe.db.exists("Product Bundle", {"new_item_code": doc.item_code}) + and (doc.qty - doc.packed_qty) > 0 ), }, "Packed Item": { @@ -721,6 +726,8 @@ def set_missing_values(source, target): "qty": "qty", "name": "pi_detail", }, + "postprocess": update_item, + "condition": lambda doc: ((doc.qty - doc.packed_qty) > 0), }, }, target_doc, From 9e5b102768395601812fa4e09980a3fe0d6289b4 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 15:40:26 +0530 Subject: [PATCH 10/21] fix: make DN item reference mandatory for Packing Slip Item --- erpnext/stock/doctype/packing_slip/packing_slip.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index d1c122d046aa..b356a205963d 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -38,6 +38,7 @@ def validate(self) -> None: self.validate_delivery_note() self.validate_case_nos() + self.validate_mandatory() validate_uom_is_integer(self, "stock_uom", "qty") validate_uom_is_integer(self, "weight_uom", "net_weight") @@ -90,6 +91,13 @@ def validate_case_nos(self): ) ) + def validate_mandatory(self): + for item in self.items: + if not item.dn_detail and not item.pi_detail: + frappe.throw( + _("Row {0}: Either Delivery Note Item or Packed Item reference is mandatory").format(item.idx) + ) + def set_missing_values(self): if not self.from_case_no: self.from_case_no = self.get_recommended_case_no() From 90701c7ae9249918fa81c7605baa80b1eaf51c09 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 17:05:51 +0530 Subject: [PATCH 11/21] fix: validate Packing Slip Item Qty with DN Items --- .../doctype/packing_slip/packing_slip.py | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index b356a205963d..c5b928bd5c16 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -38,7 +38,7 @@ def validate(self) -> None: self.validate_delivery_note() self.validate_case_nos() - self.validate_mandatory() + self.validate_items() validate_uom_is_integer(self, "stock_uom", "qty") validate_uom_is_integer(self, "weight_uom", "net_weight") @@ -91,13 +91,38 @@ def validate_case_nos(self): ) ) - def validate_mandatory(self): + def validate_items(self): for item in self.items: if not item.dn_detail and not item.pi_detail: frappe.throw( _("Row {0}: Either Delivery Note Item or Packed Item reference is mandatory").format(item.idx) ) + remaining_qty = frappe.db.get_value( + "Delivery Note Item" if item.dn_detail else "Packed Item", + {"name": item.dn_detail or item.pi_detail, "docstatus": 0}, + ["sum(qty - packed_qty)"], + ) + + if remaining_qty is None: + frappe.throw( + _("Row {0}: Please provide a valid Delivery Note Item or Packed Item reference.").format( + item.idx + ) + ) + elif remaining_qty <= 0: + frappe.throw( + _("Row {0}: Packing Slip is already created for Item {1}.").format( + item.idx, frappe.bold(item.item_code) + ) + ) + elif item.qty > remaining_qty: + frappe.throw( + _("Row {0}: Qty cannot be greater than {1} for the Item {2}.").format( + item.idx, frappe.bold(remaining_qty), frappe.bold(item.item_code) + ) + ) + def set_missing_values(self): if not self.from_case_no: self.from_case_no = self.get_recommended_case_no() From 269cc96c412b0dacf5e1f0fcc3e9ead2f0cf3e95 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 17:47:36 +0530 Subject: [PATCH 12/21] refactor: move `js` validations to `py` --- .../doctype/packing_slip/packing_slip.js | 58 ------------------- .../doctype/packing_slip/packing_slip.py | 38 ++++++++++-- 2 files changed, 34 insertions(+), 62 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js index 85a611f2b1f8..ae3d9bae9318 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.js +++ b/erpnext/stock/doctype/packing_slip/packing_slip.js @@ -28,63 +28,5 @@ frappe.ui.form.on("Packing Slip", { refresh: (frm) => { frm.toggle_display("misc_details", frm.doc.amended_from); - }, - - validate: (frm) => { - frm.trigger("validate_case_nos"); - frm.trigger("validate_calculate_item_details"); - }, - - // To Case No. cannot be less than From Case No. - validate_case_nos: (frm) => { - doc = locals[frm.doc.doctype][frm.doc.name]; - - if(cint(doc.from_case_no) == 0) { - frappe.msgprint(__("The 'From Package No.' field must neither be empty nor it's value less than 1.")); - frappe.validated = false; - } else if(!cint(doc.to_case_no)) { - doc.to_case_no = doc.from_case_no; - refresh_field('to_case_no'); - } else if(cint(doc.to_case_no) < cint(doc.from_case_no)) { - frappe.msgprint(__("'To Case No.' cannot be less than 'From Case No.'")); - frappe.validated = false; - } - }, - - validate_calculate_item_details: (frm) => { - frm.trigger("validate_items_qty"); - frm.trigger("calc_net_total_pkg"); - }, - - validate_items_qty: (frm) => { - frm.doc.items.forEach(item => { - if (item.qty <= 0) { - frappe.msgprint(__("Invalid quantity specified for item {0}. Quantity should be greater than 0.", [item.item_code])); - frappe.validated = false; - } - }); - }, - - calc_net_total_pkg: (frm) => { - var net_weight_pkg = 0; - var items = frm.doc.items || []; - frm.doc.net_weight_uom = (items && items.length) ? items[0].weight_uom : ''; - frm.doc.gross_weight_uom = frm.doc.net_weight_uom; - - items.forEach(item => { - if(item.weight_uom != frm.doc.net_weight_uom) { - frappe.msgprint(__("Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM.")); - frappe.validated = false; - } - net_weight_pkg += flt(item.net_weight) * flt(item.qty); - }); - - frm.doc.net_weight_pkg = roundNumber(net_weight_pkg, 2); - - if(!flt(frm.doc.gross_weight_pkg)) { - frm.doc.gross_weight_pkg = frm.doc.net_weight_pkg; - } - - refresh_many(['net_weight_pkg', 'net_weight_uom', 'gross_weight_uom', 'gross_weight_pkg']); } }); diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index c5b928bd5c16..6ea5938917ab 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.utils import cint +from frappe.utils import cint, flt from erpnext.controllers.status_updater import StatusUpdater @@ -44,6 +44,7 @@ def validate(self) -> None: validate_uom_is_integer(self, "weight_uom", "net_weight") self.set_missing_values() + self.calculate_net_total_pkg() def on_submit(self): self.update_prevdoc_status() @@ -62,9 +63,13 @@ def validate_delivery_note(self): def validate_case_nos(self): """Validate if case nos overlap. If they do, recommend next case no.""" - if not self.to_case_no: + if cint(self.from_case_no) <= 0: + frappe.throw( + _("The 'From Package No.' field must neither be empty nor it's value less than 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): + elif cint(self.to_case_no) < cint(self.from_case_no): frappe.throw(_("'To Package No.' cannot be less than 'From Package No.'")) else: ps = frappe.qb.DocType("Packing Slip") @@ -93,9 +98,14 @@ def validate_case_nos(self): def validate_items(self): for item in self.items: + if item.qty <= 0: + frappe.throw(_("Row {0}: Qty must be greater than 0.").format(item.idx)) + if not item.dn_detail and not item.pi_detail: frappe.throw( - _("Row {0}: Either Delivery Note Item or Packed Item reference is mandatory").format(item.idx) + _("Row {0}: Either Delivery Note Item or Packed Item reference is mandatory.").format( + item.idx + ) ) remaining_qty = frappe.db.get_value( @@ -150,6 +160,26 @@ def get_recommended_case_no(self): + 1 ) + def calculate_net_total_pkg(self): + self.net_weight_uom = self.items[0].weight_uom if self.items else None + self.gross_weight_uom = self.net_weight_uom + + net_weight_pkg = 0 + for item in self.items: + if item.weight_uom != self.net_weight_uom: + frappe.throw( + _( + "Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM." + ) + ) + + net_weight_pkg += flt(item.net_weight) * flt(item.qty) + + self.net_weight_pkg = round(net_weight_pkg, 2) + + if not flt(self.gross_weight_pkg): + self.gross_weight_pkg = self.net_weight_pkg + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From e75aa4e291694c7722ef568be162916ccf3ebcc6 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 17:59:34 +0530 Subject: [PATCH 13/21] fix(ux): get items on selecting DN in Packing Slip --- .../doctype/packing_slip/packing_slip.js | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js index ae3d9bae9318..95e5ea309f81 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.js +++ b/erpnext/stock/doctype/packing_slip/packing_slip.js @@ -1,7 +1,7 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on("Packing Slip", { +frappe.ui.form.on('Packing Slip', { setup: (frm) => { frm.set_query('delivery_note', () => { return { @@ -13,7 +13,7 @@ frappe.ui.form.on("Packing Slip", { frm.set_query('item_code', 'items', (doc, cdt, cdn) => { if (!doc.delivery_note) { - frappe.throw(__("Please select a Delivery Note")); + frappe.throw(__('Please select a Delivery Note')); } else { let d = locals[cdt][cdn]; return { @@ -27,6 +27,20 @@ frappe.ui.form.on("Packing Slip", { }, refresh: (frm) => { - frm.toggle_display("misc_details", frm.doc.amended_from); - } + frm.toggle_display('misc_details', frm.doc.amended_from); + }, + + delivery_note: (frm) => { + frm.set_value('items', null); + + if (frm.doc.delivery_note) { + erpnext.utils.map_current_doc({ + method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip', + source_name: frm.doc.delivery_note, + target_doc: frm, + freeze: true, + freeze_message: __('Creating Packing Slip ...'), + }); + } + }, }); From 8d1bccada467338f870226d9aba79a9bf7a74c90 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 18:02:24 +0530 Subject: [PATCH 14/21] fix(ux): remove `Get Items` button from `Packing Slip` --- .../doctype/packing_slip/packing_slip.json | 522 +++++++++--------- 1 file changed, 260 insertions(+), 262 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.json b/erpnext/stock/doctype/packing_slip/packing_slip.json index ec8d57c96522..86ed794c6209 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.json +++ b/erpnext/stock/doctype/packing_slip/packing_slip.json @@ -1,264 +1,262 @@ { - "allow_import": 1, - "autoname": "MAT-PAC-.YYYY.-.#####", - "creation": "2013-04-11 15:32:24", - "description": "Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.", - "doctype": "DocType", - "document_type": "Document", - "engine": "InnoDB", - "field_order": [ - "packing_slip_details", - "column_break0", - "delivery_note", - "column_break1", - "naming_series", - "section_break0", - "column_break2", - "from_case_no", - "column_break3", - "to_case_no", - "package_item_details", - "get_items", - "items", - "package_weight_details", - "net_weight_pkg", - "net_weight_uom", - "column_break4", - "gross_weight_pkg", - "gross_weight_uom", - "letter_head_details", - "letter_head", - "misc_details", - "amended_from" - ], - "fields": [ - { - "fieldname": "packing_slip_details", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break0", - "fieldtype": "Column Break" - }, - { - "description": "Indicates that the package is a part of this delivery (Only Draft)", - "fieldname": "delivery_note", - "fieldtype": "Link", - "in_global_search": 1, - "in_list_view": 1, - "label": "Delivery Note", - "options": "Delivery Note", - "reqd": 1 - }, - { - "fieldname": "column_break1", - "fieldtype": "Column Break" - }, - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "options": "MAT-PAC-.YYYY.-", - "print_hide": 1, - "reqd": 1, - "set_only_once": 1 - }, - { - "fieldname": "section_break0", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break2", - "fieldtype": "Column Break" - }, - { - "description": "Identification of the package for the delivery (for print)", - "fieldname": "from_case_no", - "fieldtype": "Int", - "in_list_view": 1, - "label": "From Package No.", - "no_copy": 1, - "reqd": 1, - "width": "50px" - }, - { - "fieldname": "column_break3", - "fieldtype": "Column Break" - }, - { - "description": "If more than one package of the same type (for print)", - "fieldname": "to_case_no", - "fieldtype": "Int", - "in_list_view": 1, - "label": "To Package No.", - "no_copy": 1, - "width": "50px" - }, - { - "fieldname": "package_item_details", - "fieldtype": "Section Break" - }, - { - "fieldname": "get_items", - "fieldtype": "Button", - "label": "Get Items" - }, - { - "fieldname": "items", - "fieldtype": "Table", - "label": "Items", - "options": "Packing Slip Item", - "reqd": 1 - }, - { - "fieldname": "package_weight_details", - "fieldtype": "Section Break", - "label": "Package Weight Details" - }, - { - "description": "The net weight of this package. (calculated automatically as sum of net weight of items)", - "fieldname": "net_weight_pkg", - "fieldtype": "Float", - "label": "Net Weight", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "net_weight_uom", - "fieldtype": "Link", - "label": "Net Weight UOM", - "no_copy": 1, - "options": "UOM", - "read_only": 1 - }, - { - "fieldname": "column_break4", - "fieldtype": "Column Break" - }, - { - "description": "The gross weight of the package. Usually net weight + packaging material weight. (for print)", - "fieldname": "gross_weight_pkg", - "fieldtype": "Float", - "label": "Gross Weight", - "no_copy": 1 - }, - { - "fieldname": "gross_weight_uom", - "fieldtype": "Link", - "label": "Gross Weight UOM", - "no_copy": 1, - "options": "UOM" - }, - { - "fieldname": "letter_head_details", - "fieldtype": "Section Break", - "label": "Letter Head" - }, - { - "allow_on_submit": 1, - "fieldname": "letter_head", - "fieldtype": "Link", - "label": "Letter Head", - "options": "Letter Head", - "print_hide": 1 - }, - { - "fieldname": "misc_details", - "fieldtype": "Section Break" - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Amended From", - "no_copy": 1, - "options": "Packing Slip", - "print_hide": 1, - "read_only": 1 - } - ], - "icon": "fa fa-suitcase", - "idx": 1, - "is_submittable": 1, - "modified": "2019-09-09 04:45:08.082862", - "modified_by": "Administrator", - "module": "Stock", - "name": "Packing Slip", - "owner": "Administrator", - "permissions": [ - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock User", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Item Manager", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock Manager", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "share": 1, - "submit": 1, - "write": 1 - } - ], - "search_fields": "delivery_note", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC" + "actions": [], + "allow_import": 1, + "autoname": "MAT-PAC-.YYYY.-.#####", + "creation": "2013-04-11 15:32:24", + "description": "Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "packing_slip_details", + "column_break0", + "delivery_note", + "column_break1", + "naming_series", + "section_break0", + "column_break2", + "from_case_no", + "column_break3", + "to_case_no", + "package_item_details", + "items", + "package_weight_details", + "net_weight_pkg", + "net_weight_uom", + "column_break4", + "gross_weight_pkg", + "gross_weight_uom", + "letter_head_details", + "letter_head", + "misc_details", + "amended_from" + ], + "fields": [ + { + "fieldname": "packing_slip_details", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break0", + "fieldtype": "Column Break" + }, + { + "description": "Indicates that the package is a part of this delivery (Only Draft)", + "fieldname": "delivery_note", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Delivery Note", + "options": "Delivery Note", + "reqd": 1 + }, + { + "fieldname": "column_break1", + "fieldtype": "Column Break" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "MAT-PAC-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "section_break0", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break2", + "fieldtype": "Column Break" + }, + { + "description": "Identification of the package for the delivery (for print)", + "fieldname": "from_case_no", + "fieldtype": "Int", + "in_list_view": 1, + "label": "From Package No.", + "no_copy": 1, + "reqd": 1, + "width": "50px" + }, + { + "fieldname": "column_break3", + "fieldtype": "Column Break" + }, + { + "description": "If more than one package of the same type (for print)", + "fieldname": "to_case_no", + "fieldtype": "Int", + "in_list_view": 1, + "label": "To Package No.", + "no_copy": 1, + "width": "50px" + }, + { + "fieldname": "package_item_details", + "fieldtype": "Section Break" + }, + { + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Packing Slip Item", + "reqd": 1 + }, + { + "fieldname": "package_weight_details", + "fieldtype": "Section Break", + "label": "Package Weight Details" + }, + { + "description": "The net weight of this package. (calculated automatically as sum of net weight of items)", + "fieldname": "net_weight_pkg", + "fieldtype": "Float", + "label": "Net Weight", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "net_weight_uom", + "fieldtype": "Link", + "label": "Net Weight UOM", + "no_copy": 1, + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "column_break4", + "fieldtype": "Column Break" + }, + { + "description": "The gross weight of the package. Usually net weight + packaging material weight. (for print)", + "fieldname": "gross_weight_pkg", + "fieldtype": "Float", + "label": "Gross Weight", + "no_copy": 1 + }, + { + "fieldname": "gross_weight_uom", + "fieldtype": "Link", + "label": "Gross Weight UOM", + "no_copy": 1, + "options": "UOM" + }, + { + "fieldname": "letter_head_details", + "fieldtype": "Section Break", + "label": "Letter Head" + }, + { + "allow_on_submit": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head", + "print_hide": 1 + }, + { + "fieldname": "misc_details", + "fieldtype": "Section Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "options": "Packing Slip", + "print_hide": 1, + "read_only": 1 } + ], + "icon": "fa fa-suitcase", + "idx": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-04-28 18:01:37.341619", + "modified_by": "Administrator", + "module": "Stock", + "name": "Packing Slip", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Item Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "search_fields": "delivery_note", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file From da00fc0f16bd980243548cd1560078aa1891a323 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 18:28:28 +0530 Subject: [PATCH 15/21] fix(ux): don't show `Create > Packing Slip` button if items are already packed --- .../doctype/delivery_note/delivery_note.js | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index ae56645b7306..08419c26ed60 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -185,11 +185,30 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends erpn } if(doc.docstatus==0 && !doc.__islocal) { - this.frm.add_custom_button(__('Packing Slip'), function() { - frappe.model.open_mapped_doc({ - method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip", - frm: me.frm - }) }, __('Create')); + var remaining_qty = 0; + + doc.items.forEach(item => { + frappe.db.exists("Product Bundle", item.item_code).then(exists => { + if (!exists) { + remaining_qty += (item.qty - item.packed_qty); + } + }); + }); + + if (!remaining_qty) { + doc.packed_items.forEach(item => { + remaining_qty += (item.qty - item.packed_qty); + }); + } + + if (remaining_qty > 0) { + this.frm.add_custom_button(__('Packing Slip'), function() { + frappe.model.open_mapped_doc({ + method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip", + frm: me.frm + }) }, __('Create') + ); + } } if (!doc.__islocal && doc.docstatus==1) { From 7742c592c5528002813e0247240cb0cb3f67985a Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 21:51:15 +0530 Subject: [PATCH 16/21] test: add test cases for `Packing Slip` --- .../doctype/packing_slip/test_packing_slip.py | 106 +++++++++++++++++- 1 file changed, 103 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py index bc405b209951..e8873e382120 100644 --- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py @@ -3,9 +3,109 @@ import unittest -# test_records = frappe.get_test_records('Packing Slip') +import frappe from frappe.tests.utils import FrappeTestCase +from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle +from erpnext.stock.doctype.delivery_note.delivery_note import make_packing_slip +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.item.test_item import make_item -class TestPackingSlip(unittest.TestCase): - pass + +class TestPackingSlip(FrappeTestCase): + def test_packing_slip(self): + # Step - 1: Create a Product Bundle + items = create_items() + make_product_bundle(items[0], items[1:], 5) + + # Step - 2: Create a Delivery Note (Draft) with Product Bundle + dn = create_delivery_note( + item_code=items[0], + qty=2, + do_not_save=True, + ) + dn.append( + "items", + { + "item_code": items[1], + "warehouse": "_Test Warehouse - _TC", + "qty": 10, + }, + ) + dn.save() + + # Step - 3: Make a Packing Slip from Delivery Note for 4 Qty + ps1 = make_packing_slip(dn.name) + for item in ps1.items: + item.qty = 4 + ps1.save() + ps1.submit() + + # Test - 1: `Packed Qty` should be updated to 4 in Delivery Note Items and Packed Items. + dn.load_from_db() + for item in dn.items: + if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): + self.assertEqual(item.packed_qty, 4) + + for item in dn.packed_items: + self.assertEqual(item.packed_qty, 4) + + # Step - 4: Make another Packing Slip from Delivery Note for 6 Qty + ps2 = make_packing_slip(dn.name) + ps2.save() + ps2.submit() + + # Test - 2: `Packed Qty` should be updated to 10 in Delivery Note Items and Packed Items. + dn.load_from_db() + for item in dn.items: + if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): + self.assertEqual(item.packed_qty, 10) + + for item in dn.packed_items: + self.assertEqual(item.packed_qty, 10) + + # Step - 5: Cancel Packing Slip [1] + ps1.cancel() + + # Test - 3: `Packed Qty` should be updated to 4 in Delivery Note Items and Packed Items. + dn.load_from_db() + for item in dn.items: + if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): + self.assertEqual(item.packed_qty, 6) + + for item in dn.packed_items: + self.assertEqual(item.packed_qty, 6) + + # Step - 6: Cancel Packing Slip [2] + ps2.cancel() + + # Test - 4: `Packed Qty` should be updated to 0 in Delivery Note Items and Packed Items. + dn.load_from_db() + for item in dn.items: + if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): + self.assertEqual(item.packed_qty, 0) + + for item in dn.packed_items: + self.assertEqual(item.packed_qty, 0) + + # Step - 7: Make Packing Slip for more Qty than Delivery Note + ps3 = make_packing_slip(dn.name) + ps3.items[0].qty = 20 + + # Test - 5: Should throw an ValidationError, as Packing Slip Qty is more than Delivery Note Qty + self.assertRaises(frappe.exceptions.ValidationError, ps3.save) + + +def create_items(): + items_properties = [ + {"is_stock_item": 0}, + {"is_stock_item": 1}, + {"is_stock_item": 1}, + {"is_stock_item": 1}, + ] + + items = [] + for properties in items_properties: + items.append(make_item(properties=properties).name) + + return items From b0eb9ea7bde7329e41012c468ff2bc76c9330cf3 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 22:47:27 +0530 Subject: [PATCH 17/21] refactor(minor): use `set_onload` to get unpacked items details --- .../doctype/delivery_note/delivery_note.js | 18 +------------ .../doctype/delivery_note/delivery_note.py | 26 ++++++++++++++++--- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 08419c26ed60..77545e0e1ad4 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -185,23 +185,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends erpn } if(doc.docstatus==0 && !doc.__islocal) { - var remaining_qty = 0; - - doc.items.forEach(item => { - frappe.db.exists("Product Bundle", item.item_code).then(exists => { - if (!exists) { - remaining_qty += (item.qty - item.packed_qty); - } - }); - }); - - if (!remaining_qty) { - doc.packed_items.forEach(item => { - remaining_qty += (item.qty - item.packed_qty); - }); - } - - if (remaining_qty > 0) { + if (doc.__onload && doc.__onload.has_unpacked_items) { this.frm.add_custom_button(__('Packing Slip'), function() { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 1e34296c787c..cb13833d1ece 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -86,6 +86,10 @@ def __init__(self, *args, **kwargs): ] ) + def onload(self): + if self.docstatus == 0: + self.set_onload("has_unpacked_items", self.has_unpacked_items()) + def before_print(self, settings=None): def toggle_print_hide(meta, fieldname): df = meta.get_field(fieldname) @@ -393,6 +397,20 @@ def make_return_invoice(self): ) ) + def has_unpacked_items(self): + for item in self.items: + if ( + not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}) + and item.packed_qty < item.qty + ): + return True + + for item in self.packed_items: + if item.packed_qty < item.qty: + return True + + return False + def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum @@ -711,9 +729,9 @@ def update_item(obj, target, source_parent): "name": "dn_detail", }, "postprocess": update_item, - "condition": lambda doc: ( - not frappe.db.exists("Product Bundle", {"new_item_code": doc.item_code}) - and (doc.qty - doc.packed_qty) > 0 + "condition": lambda item: ( + not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}) + and item.packed_qty < item.qty ), }, "Packed Item": { @@ -727,7 +745,7 @@ def update_item(obj, target, source_parent): "name": "pi_detail", }, "postprocess": update_item, - "condition": lambda doc: ((doc.qty - doc.packed_qty) > 0), + "condition": lambda item: (item.packed_qty < item.qty), }, }, target_doc, From 699532647d826ef0a561d57d7aee817c7e0380e4 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 23:03:33 +0530 Subject: [PATCH 18/21] refactor: validate_packed_qty() --- .../doctype/delivery_note/delivery_note.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index cb13833d1ece..2a3f9fcc0bc9 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -306,20 +306,13 @@ def check_credit_limit(self): ) def validate_packed_qty(self): - """ - Validate that if packed qty exists, it should be equal to qty - """ - if not any(flt(d.get("packed_qty")) for d in self.get("items")): - return - has_error = False - for d in self.get("items"): - if flt(d.get("qty")) != flt(d.get("packed_qty")): - frappe.msgprint( - _("Packed quantity must equal quantity for Item {0} in row {1}").format(d.item_code, d.idx) + """Validate that if packed qty exists, it should be equal to qty""" + + for item in self.items + self.packed_items: + if item.packed_qty and item.packed_qty != item.qty: + frappe.throw( + _("Row {0}: Packed Qty must be equal to {1} Qty.").format(item.idx, frappe.bold(item.doctype)) ) - has_error = True - if has_error: - raise frappe.ValidationError def update_pick_list_status(self): from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status From ba61292dfc0d767ab8c88bd05cadff2ad7ed1237 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 29 Apr 2023 07:25:35 +0530 Subject: [PATCH 19/21] test: add test case for packed qty validation on DN submit --- .../doctype/packing_slip/test_packing_slip.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py index e8873e382120..96da23db4a8d 100644 --- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py @@ -95,13 +95,22 @@ def test_packing_slip(self): # Test - 5: Should throw an ValidationError, as Packing Slip Qty is more than Delivery Note Qty self.assertRaises(frappe.exceptions.ValidationError, ps3.save) + # Step - 8: Make Packing Slip for less Qty than Delivery Note + ps4 = make_packing_slip(dn.name) + ps4.items[0].qty = 5 + ps4.save() + ps4.submit() + + # Test - 6: Delivery Note should throw a ValidationError on Submit, as Packed Qty and Delivery Note Qty are not the same + dn.load_from_db() + self.assertRaises(frappe.exceptions.ValidationError, dn.submit) + def create_items(): items_properties = [ {"is_stock_item": 0}, - {"is_stock_item": 1}, - {"is_stock_item": 1}, - {"is_stock_item": 1}, + {"is_stock_item": 1, "stock_uom": "Nos"}, + {"is_stock_item": 1, "stock_uom": "Box"}, ] items = [] From bbcb65894b4d957e45b03bb7d170a71e021c967c Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sun, 30 Apr 2023 08:04:02 +0530 Subject: [PATCH 20/21] refactor: use `get_product_bundle_list()` to get all product bundle at once --- .../doctype/delivery_note/delivery_note.py | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 2a3f9fcc0bc9..ce33d26faf51 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -308,11 +308,19 @@ def check_credit_limit(self): def validate_packed_qty(self): """Validate that if packed qty exists, it should be equal to qty""" - for item in self.items + self.packed_items: - if item.packed_qty and item.packed_qty != item.qty: - frappe.throw( - _("Row {0}: Packed Qty must be equal to {1} Qty.").format(item.idx, frappe.bold(item.doctype)) - ) + 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) + ) + ) def update_pick_list_status(self): from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status @@ -391,19 +399,22 @@ def make_return_invoice(self): ) def has_unpacked_items(self): - for item in self.items: - if ( - not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}) - and item.packed_qty < item.qty - ): - return True + product_bundle_list = self.get_product_bundle_list() - for item in self.packed_items: - if item.packed_qty < item.qty: + 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]}, + pluck="name", + ) + def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum @@ -724,7 +735,7 @@ def update_item(obj, target, source_parent): "postprocess": update_item, "condition": lambda item: ( not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}) - and item.packed_qty < item.qty + and flt(item.packed_qty) < flt(item.qty) ), }, "Packed Item": { @@ -738,7 +749,7 @@ def update_item(obj, target, source_parent): "name": "pi_detail", }, "postprocess": update_item, - "condition": lambda item: (item.packed_qty < item.qty), + "condition": lambda item: (flt(item.packed_qty) < flt(item.qty)), }, }, target_doc, From 196e18187f3c33bb9f9e272a8e2cdb14002c5a9c Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 25 May 2023 14:39:36 +0530 Subject: [PATCH 21/21] fix(patch): add patch to set `packed_qty` in draft DN --- erpnext/patches.txt | 1 + .../set_packed_qty_in_draft_delivery_notes.py | 60 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7e68ec1fcb8d..3a59d3c8b2e6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -333,3 +333,4 @@ erpnext.patches.v14_0.migrate_gl_to_payment_ledger execute:frappe.delete_doc_if_exists("Report", "Tax Detail") erpnext.patches.v15_0.enable_all_leads erpnext.patches.v14_0.update_company_in_ldc +erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes diff --git a/erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py b/erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py new file mode 100644 index 000000000000..1aeb2e6cc3d4 --- /dev/null +++ b/erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py @@ -0,0 +1,60 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.query_builder.functions import Sum + + +def execute(): + ps = frappe.qb.DocType("Packing Slip") + dn = frappe.qb.DocType("Delivery Note") + ps_item = frappe.qb.DocType("Packing Slip Item") + + ps_details = ( + frappe.qb.from_(ps) + .join(ps_item) + .on(ps.name == ps_item.parent) + .join(dn) + .on(ps.delivery_note == dn.name) + .select( + dn.name.as_("delivery_note"), + ps_item.item_code.as_("item_code"), + Sum(ps_item.qty).as_("packed_qty"), + ) + .where((ps.docstatus == 1) & (dn.docstatus == 0)) + .groupby(dn.name, ps_item.item_code) + ).run(as_dict=True) + + if ps_details: + dn_list = set() + item_code_list = set() + for ps_detail in ps_details: + dn_list.add(ps_detail.delivery_note) + item_code_list.add(ps_detail.item_code) + + dn_item = frappe.qb.DocType("Delivery Note Item") + dn_item_query = ( + frappe.qb.from_(dn_item) + .select( + dn.parent.as_("delivery_note"), + dn_item.name, + dn_item.item_code, + dn_item.qty, + ) + .where((dn_item.parent.isin(dn_list)) & (dn_item.item_code.isin(item_code_list))) + ) + + dn_details = frappe._dict() + for r in dn_item_query.run(as_dict=True): + dn_details.setdefault((r.delivery_note, r.item_code), frappe._dict()).setdefault(r.name, r.qty) + + for ps_detail in ps_details: + dn_items = dn_details.get((ps_detail.delivery_note, ps_detail.item_code)) + + if dn_items: + remaining_qty = ps_detail.packed_qty + for name, qty in dn_items.items(): + if remaining_qty > 0: + row_packed_qty = min(qty, remaining_qty) + frappe.db.set_value("Delivery Note Item", name, "packed_qty", row_packed_qty) + remaining_qty -= row_packed_qty