diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f010f1a1be6..fbfb7d2094e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -331,3 +331,4 @@ execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_h # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger 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 00000000000..1aeb2e6cc3d --- /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 diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index ae56645b730..77545e0e1ad 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -185,11 +185,14 @@ 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')); + 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", + frm: me.frm + }) }, __('Create') + ); + } } if (!doc.__islocal && doc.docstatus==1) { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 16caceba09f..3a056500b54 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -86,6 +86,10 @@ class DeliveryNote(SellingController): ] ) + 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) @@ -299,20 +303,21 @@ class DeliveryNote(SellingController): ) 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) - ) - has_error = True - if has_error: - raise frappe.ValidationError + """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) + ) + ) def update_pick_list_status(self): from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status @@ -390,6 +395,23 @@ class DeliveryNote(SellingController): ) ) + 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]}, + pluck="name", + ) + def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum @@ -681,6 +703,12 @@ def make_installation_note(source_name, target_doc=None): @frappe.whitelist() 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, @@ -695,12 +723,34 @@ 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", + "stock_uom": "stock_uom", + "name": "dn_detail", }, + "postprocess": update_item, + "condition": lambda item: ( + not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}) + and flt(item.packed_qty) < flt(item.qty) + ), + }, + "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", + }, + "postprocess": update_item, + "condition": lambda item: (flt(item.packed_qty) < flt(item.qty)), }, }, target_doc, + set_missing_values, ) return doclist 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 e46cab05762..b97e42c2468 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", @@ -850,6 +851,16 @@ "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, @@ -866,4 +877,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index cb8eb30cb30..c5fb2411c28 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", diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js index 40d46852d03..95e5ea309f8 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.js +++ b/erpnext/stock/doctype/packing_slip/packing_slip.js @@ -1,113 +1,46 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// License: GNU General Public License v3. See license.txt +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt -cur_frm.fields_dict['delivery_note'].get_query = function(doc, cdt, cdn) { - return{ - filters:{ 'docstatus': 0} - } -} +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, + } + } + } + }); + }, -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} + refresh: (frm) => { + 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 ...'), + }); } - } -} - -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(); - } - }); -} - -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 || []; - - cur_frm.cscript.validate_duplicate_items(doc, ps_detail); - cur_frm.cscript.calc_net_total_pkg(doc, ps_detail); -} - - -// Do not allow duplicate items i.e. items with same item_code -// Also check for 0 qty -cur_frm.cscript.validate_duplicate_items = function(doc, ps_detail) { - for(var i=0; i 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() + self.validate_items() + validate_uom_is_integer(self, "stock_uom", "qty") validate_uom_is_integer(self, "weight_uom", "net_weight") - 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)) + self.set_missing_values() + self.calculate_net_total_pkg() - 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_submit(self): + self.update_prevdoc_status() + + def on_cancel(self): + self.update_prevdoc_status() + + def validate_delivery_note(self): + """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( + _("A Packing Slip can only be created for Draft Delivery Note.").format(self.delivery_note) + ) 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) + """Validate if case nos overlap. If they do, recommend next 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): - frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), raise_exception=1) + 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") + 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() - 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( + _("""Package No(s) already in use. Try from Package No {0}""").format( + self.get_recommended_case_no() + ) + ) - if res: - frappe.throw( - _("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no()) + 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 + ) + ) + + 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)"], ) - 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() + 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) + ) + ) - 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) - - 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 update_item_details(self): - """ - Fill empty columns in Packing Slip Item - """ + def set_missing_values(self): 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) + for item in self.items: + stock_uom, weight_per_unit, weight_uom = frappe.db.get_value( + "Item", item.item_code, ["stock_uom", "weight_per_unit", "weight_uom"] + ) - if res and len(res) > 0: - d.net_weight = res["weight_per_unit"] - d.weight_uom = res["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_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, + """Returns the next case no. for a new packing slip for a delivery note""" + + return ( + cint( + frappe.db.get_value( + "Packing Slip", {"delivery_note": self.delivery_note, "docstatus": 1}, ["max(to_case_no)"] + ) + ) + + 1 ) - return cint(recommended_case_no[0][0]) + 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 - @frappe.whitelist() - def get_items(self): - self.set("items", []) + 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." + ) + ) - custom_fields = frappe.get_meta("Delivery Note Item").get_custom_fields() + net_weight_pkg += flt(item.net_weight) * flt(item.qty) - 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) + self.net_weight_pkg = round(net_weight_pkg, 2) - # 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() + if not flt(self.gross_weight_pkg): + self.gross_weight_pkg = self.net_weight_pkg @frappe.whitelist() diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py index bc405b20995..96da23db4a8 100644 --- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py @@ -3,9 +3,118 @@ 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) + + # 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, "stock_uom": "Nos"}, + {"is_stock_item": 1, "stock_uom": "Box"}, + ] + + items = [] + for properties in items_properties: + items.append(make_item(properties=properties).name) + + return items 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 4270839bfdb..4bd90355acb 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,23 @@ "fieldtype": "Data", "hidden": 1, "in_list_view": 1, - "label": "DN Detail" + "label": "Delivery Note Item", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "pi_detail", + "fieldtype": "Data", + "hidden": 1, + "label": "Delivery Note Packed Item", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-12-14 01:22:00.715935", + "modified": "2023-04-28 15:00:14.079306", "modified_by": "Administrator", "module": "Stock", "name": "Packing Slip Item", @@ -136,5 +147,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file