Merge branch 'develop' into stock-reservation

This commit is contained in:
Sagar Sharma 2023-05-26 15:28:54 +05:30 committed by GitHub
commit 8cc8af8204
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 752 additions and 565 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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,

View File

@ -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",

View File

@ -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
},
});

View File

@ -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": []
}

View File

@ -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()

View File

@ -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

View File

@ -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
}

View File

@ -596,7 +596,7 @@ def filter_items_with_no_transactions(
"warehouse",
"item_name",
"item_group",
"projecy",
"project",
"stock_uom",
"company",
"opening_fifo_queue",