Merge branch 'develop' into stock-reservation
This commit is contained in:
commit
8cc8af8204
@ -125,12 +125,14 @@ def get_revenue(data, period_list, include_in_gross=1):
|
||||
|
||||
data_to_be_removed = True
|
||||
while data_to_be_removed:
|
||||
revenue, data_to_be_removed = remove_parent_with_no_child(revenue, period_list)
|
||||
revenue = adjust_account(revenue, period_list)
|
||||
revenue, data_to_be_removed = remove_parent_with_no_child(revenue)
|
||||
|
||||
adjust_account_totals(revenue, period_list)
|
||||
|
||||
return copy.deepcopy(revenue)
|
||||
|
||||
|
||||
def remove_parent_with_no_child(data, period_list):
|
||||
def remove_parent_with_no_child(data):
|
||||
data_to_be_removed = False
|
||||
for parent in data:
|
||||
if "is_group" in parent and parent.get("is_group") == 1:
|
||||
@ -147,16 +149,19 @@ def remove_parent_with_no_child(data, period_list):
|
||||
return data, data_to_be_removed
|
||||
|
||||
|
||||
def adjust_account(data, period_list, consolidated=False):
|
||||
leaf_nodes = [item for item in data if item["is_group"] == 0]
|
||||
def adjust_account_totals(data, period_list):
|
||||
totals = {}
|
||||
for node in leaf_nodes:
|
||||
set_total(node, node["total"], data, totals)
|
||||
for d in data:
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
d["total"] = totals[d["account"]]
|
||||
return data
|
||||
for d in reversed(data):
|
||||
if d.get("is_group"):
|
||||
for period in period_list:
|
||||
# reset totals for group accounts as totals set by get_data doesn't consider include_in_gross check
|
||||
d[period.key] = sum(
|
||||
item[period.key] for item in data if item.get("parent_account") == d.get("account")
|
||||
)
|
||||
else:
|
||||
set_total(d, d["total"], data, totals)
|
||||
|
||||
d["total"] = totals[d["account"]]
|
||||
|
||||
|
||||
def set_total(node, value, complete_list, totals):
|
||||
@ -191,6 +196,9 @@ def get_profit(
|
||||
|
||||
if profit_loss[key]:
|
||||
has_value = True
|
||||
if not profit_loss.get("total"):
|
||||
profit_loss["total"] = 0
|
||||
profit_loss["total"] += profit_loss[key]
|
||||
|
||||
if has_value:
|
||||
return profit_loss
|
||||
@ -229,6 +237,9 @@ def get_net_profit(
|
||||
|
||||
if profit_loss[key]:
|
||||
has_value = True
|
||||
if not profit_loss.get("total"):
|
||||
profit_loss["total"] = 0
|
||||
profit_loss["total"] += profit_loss[key]
|
||||
|
||||
if has_value:
|
||||
return profit_loss
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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) {
|
||||
|
@ -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)
|
||||
@ -390,20 +394,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
|
||||
@ -481,6 +486,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
|
||||
@ -772,6 +794,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,
|
||||
@ -786,12 +814,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
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
},
|
||||
});
|
||||
|
@ -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": []
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -596,7 +596,7 @@ def filter_items_with_no_transactions(
|
||||
"warehouse",
|
||||
"item_name",
|
||||
"item_group",
|
||||
"projecy",
|
||||
"project",
|
||||
"stock_uom",
|
||||
"company",
|
||||
"opening_fifo_queue",
|
||||
|
Loading…
x
Reference in New Issue
Block a user