Merge branch 'develop' into PACKING-SLIP-FOR-DN-PACKED-ITEMS
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index ae56645..77545e0 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -185,11 +185,14 @@
}
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 c18e851..e404d0b 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -86,6 +86,10 @@
]
)
+ 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)
@@ -302,20 +306,21 @@
)
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
@@ -393,6 +398,23 @@
)
)
+ 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
@@ -684,6 +706,12 @@
@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,
@@ -698,12 +726,34 @@
"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 e46cab0..3853bd1 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,
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index cb8eb30..c5fb241 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 40d4685..95e5ea3 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<ps_detail.length; i++) {
- for(var j=0; j<ps_detail.length; j++) {
- if(i!=j && ps_detail[i].item_code && ps_detail[i].item_code==ps_detail[j].item_code) {
- frappe.msgprint(__("You have entered duplicate items. Please rectify and try again."));
- frappe.validated = false;
- return;
- }
- }
- if(flt(ps_detail[i].qty)<=0) {
- frappe.msgprint(__("Invalid quantity specified for item {0}. Quantity should be greater than 0.", [ps_detail[i].item_code]));
- frappe.validated = false;
- }
- }
-}
-
-
-// Calculate Net Weight of Package
-cur_frm.cscript.calc_net_total_pkg = function(doc, ps_detail) {
- 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<ps_detail.length; i++) {
- var item = ps_detail[i];
- if(item.weight_uom != 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);
- if(!flt(doc.gross_weight_pkg)) {
- doc.gross_weight_pkg = doc.net_weight_pkg;
- }
- refresh_many(['net_weight_pkg', 'net_weight_uom', 'gross_weight_uom', 'gross_weight_pkg']);
-}
-
-// TODO: validate gross weight field
+ },
+});
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.json b/erpnext/stock/doctype/packing_slip/packing_slip.json
index ec8d57c..86ed794 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
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py
index e5b9de8..6ea5938 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.py
@@ -4,193 +4,181 @@
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 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()
+ 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 bc405b2..96da23d 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 4270839..4bd9035 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