From 8dcb9302b417618505ea24e5566c017eff451c1e Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Mon, 8 May 2023 20:41:38 +0530 Subject: [PATCH 01/46] fix: account group totals calculation to consider include_in_gross --- .../gross_and_net_profit_report.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index cd5f3667071..7f2877989bd 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -152,10 +152,17 @@ def adjust_account(data, period_list, consolidated=False): totals = {} for node in leaf_nodes: set_total(node, node["total"], data, totals) - for d in data: + + for d in reversed(data): for period in period_list: - key = period if consolidated else period.key + if d.get("is_group"): + # 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") + ) + d["total"] = totals[d["account"]] + return data @@ -170,7 +177,6 @@ def set_total(node, value, complete_list, totals): next(item for item in complete_list if item["account"] == parent), value, complete_list, totals ) - def get_profit( gross_income, gross_expense, period_list, company, profit_type, currency=None, consolidated=False ): From 50822f207ec5272d4d71a2b6579693da2088105d Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Mon, 8 May 2023 20:48:43 +0530 Subject: [PATCH 02/46] refactor: remove unused parameters --- .../gross_and_net_profit_report.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index 7f2877989bd..f2412819c4e 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -125,12 +125,12 @@ 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, data_to_be_removed = remove_parent_with_no_child(revenue) revenue = adjust_account(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,7 +147,7 @@ def remove_parent_with_no_child(data, period_list): return data, data_to_be_removed -def adjust_account(data, period_list, consolidated=False): +def adjust_account(data, period_list): leaf_nodes = [item for item in data if item["is_group"] == 0] totals = {} for node in leaf_nodes: From 1a3b9c5bdfb9fac04a3a7d8724e6b3c3b593ec19 Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Tue, 9 May 2023 09:16:10 +0530 Subject: [PATCH 03/46] refactor: merge separate loops for calculating group / leaf node totals rename function remove return statement as the list is mutated --- .../gross_and_net_profit_report.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index f2412819c4e..a7b7f270cfd 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -126,7 +126,9 @@ 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) - revenue = adjust_account(revenue, period_list) + + adjust_account_totals(revenue, period_list) + return copy.deepcopy(revenue) @@ -147,23 +149,19 @@ def remove_parent_with_no_child(data): return data, data_to_be_removed -def adjust_account(data, period_list): - 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 reversed(data): - for period in period_list: - if d.get("is_group"): + 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"]] - - return data + d["total"] = totals[d["account"]] def set_total(node, value, complete_list, totals): @@ -177,6 +175,7 @@ def set_total(node, value, complete_list, totals): next(item for item in complete_list if item["account"] == parent), value, complete_list, totals ) + def get_profit( gross_income, gross_expense, period_list, company, profit_type, currency=None, consolidated=False ): From cb9b4fbb91f4b73916416167932064ef5965eed1 Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Wed, 10 May 2023 12:34:15 +0530 Subject: [PATCH 04/46] fix: add total col for gross and net profit --- .../gross_and_net_profit_report.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index a7b7f270cfd..f0ca405401d 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -196,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 @@ -234,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 From 989052c0753dab2ce181623cf43b2fac538117e4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 12:47:38 +0530 Subject: [PATCH 05/46] fix: tab-uniformity (backport #35400) (#35402) fix: tab-uniformity (#35400) Made Doctype tabs uniform Co-authored-by: Sagar Sharma (cherry picked from commit c1f1a033c97bc690e5857a964db9a8f2d2f152ef) Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> --- .../doctype/purchase_order/purchase_order.json | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index c51c6edf1da..645abf25a8f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -157,7 +157,7 @@ "party_account_currency", "inter_company_order_reference", "is_old_subcontracting_flow", - "dashboard" + "connections_tab" ], "fields": [ { @@ -1171,7 +1171,6 @@ "depends_on": "is_internal_supplier", "fieldname": "set_from_warehouse", "fieldtype": "Link", - "ignore_user_permissions": 1, "label": "Set From Warehouse", "options": "Warehouse" }, @@ -1185,12 +1184,6 @@ "fieldtype": "Tab Break", "label": "More Info" }, - { - "fieldname": "dashboard", - "fieldtype": "Tab Break", - "label": "Dashboard", - "show_dashboard": 1 - }, { "fieldname": "column_break_7", "fieldtype": "Column Break" @@ -1266,13 +1259,19 @@ "fieldname": "shipping_address_section", "fieldtype": "Section Break", "label": "Shipping Address" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-05-07 20:18:09.196799", + "modified": "2023-05-24 11:16:41.195340", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", From 746a7342575a1702248e00997ae8e8c893e6bb8f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 24 May 2023 14:12:58 +0530 Subject: [PATCH 06/46] fix: available qty not fetching for raw material in PP (cherry picked from commit 8e3463c4ef4ac649fe12b91ee3ab7de11a680426) --- .../doctype/production_plan/production_plan.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index ab7aa52bb7a..45a59cf7325 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -451,10 +451,14 @@ frappe.ui.form.on("Material Request Plan Item", { for_warehouse: row.warehouse }, callback: function(r) { - let {projected_qty, actual_qty} = r.message; + if (r.message) { + let {projected_qty, actual_qty} = r.message[0]; - frappe.model.set_value(cdt, cdn, 'projected_qty', projected_qty); - frappe.model.set_value(cdt, cdn, 'actual_qty', actual_qty); + frappe.model.set_value(cdt, cdn, { + 'projected_qty': projected_qty, + 'actual_qty': actual_qty + }); + } } }) } From 6fe42c937c5b3e81ebacb4fcab3336c0277db8f0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 24 May 2023 15:24:05 +0530 Subject: [PATCH 07/46] fix: Negative value in Reserved Qty for Production Plan (cherry picked from commit a37608a36cad289770767f7b54d7289cd1ca7247) --- .../manufacturing/doctype/production_plan/production_plan.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index e40539acf34..0800bdd2af9 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1534,7 +1534,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0))) + .select(Sum(child.required_bom_qty * IfNull(child.conversion_factor, 1.0))) .where( (table.docstatus == 1) & (child.item_code == item_code) @@ -1552,6 +1552,9 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True) ) + if reserved_qty_for_production > reserved_qty_for_production_plan: + return 0.0 + return reserved_qty_for_production_plan - reserved_qty_for_production From 9fcfab219c0a77da5cc98f52922d9ea3af0b173a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 17 May 2023 23:52:03 +0530 Subject: [PATCH 08/46] refactor: stock balance report (cherry picked from commit d9979b2ffbbf2341978099edc135e1962a161260) --- .../doctype/closing_stock_balance/__init__.py | 0 .../closing_stock_balance.js | 23 + .../closing_stock_balance.json | 148 +++ .../closing_stock_balance.py | 118 +++ .../test_closing_stock_balance.py | 9 + .../report/stock_balance/stock_balance.js | 5 + .../report/stock_balance/stock_balance.py | 899 ++++++++++-------- 7 files changed, 779 insertions(+), 423 deletions(-) create mode 100644 erpnext/stock/doctype/closing_stock_balance/__init__.py create mode 100644 erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js create mode 100644 erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.json create mode 100644 erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py create mode 100644 erpnext/stock/doctype/closing_stock_balance/test_closing_stock_balance.py diff --git a/erpnext/stock/doctype/closing_stock_balance/__init__.py b/erpnext/stock/doctype/closing_stock_balance/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js new file mode 100644 index 00000000000..6e90884f56c --- /dev/null +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js @@ -0,0 +1,23 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Closing Stock Balance", { + refresh(frm) { + frm.trigger("generate_closing_balance"); + }, + + generate_closing_balance(frm) { + if (in_list(["Queued", "Failed"], frm.doc.status)) { + frm.add_custom_button(__("Generate Closing Stock Balance"), () => { + frm.call({ + method: "enqueue_job", + doc: frm.doc, + freeze: true, + callback: () => { + frm.reload_doc(); + } + }) + }) + } + } +}); diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.json b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.json new file mode 100644 index 00000000000..225da6d15ec --- /dev/null +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.json @@ -0,0 +1,148 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2023-05-17 09:58:42.086911", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "company", + "status", + "column_break_p0s0", + "from_date", + "to_date", + "filters_section", + "item_code", + "item_group", + "include_uom", + "column_break_rm5w", + "warehouse", + "warehouse_type", + "amended_from" + ], + "fields": [ + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "CBAL-.#####" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_preview": 1, + "label": "Status", + "options": "Draft\nQueued\nIn Progress\nCompleted\nFailed\nCanceled", + "read_only": 1 + }, + { + "fieldname": "column_break_p0s0", + "fieldtype": "Column Break" + }, + { + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date" + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date" + }, + { + "collapsible": 1, + "fieldname": "filters_section", + "fieldtype": "Section Break", + "label": "Filters" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "label": "Item Code", + "options": "Item" + }, + { + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" + }, + { + "fieldname": "column_break_rm5w", + "fieldtype": "Column Break" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "warehouse_type", + "fieldtype": "Link", + "label": "Warehouse Type", + "options": "Warehouse Type" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Closing Stock Balance", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Closing Stock Balance", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "include_uom", + "fieldtype": "Link", + "label": "Include UOM", + "options": "UOM" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-05-17 11:46:04.448220", + "modified_by": "Administrator", + "module": "Stock", + "name": "Closing Stock Balance", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py new file mode 100644 index 00000000000..544c9f3e1d2 --- /dev/null +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py @@ -0,0 +1,118 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import json + +import frappe +from frappe.core.doctype.prepared_report.prepared_report import create_json_gz_file +from frappe.desk.form.load import get_attachments +from frappe.model.document import Document +from frappe.utils import get_link_to_form, gzip_decompress, parse_json +from frappe.utils.background_jobs import enqueue + +from erpnext.stock.report.stock_balance.stock_balance import execute + + +class ClosingStockBalance(Document): + def before_save(self): + self.set_status() + + def set_status(self, save=False): + self.status = "Queued" + if self.docstatus == 2: + self.status = "Canceled" + + if self.docstatus == 0: + self.status = "Draft" + + if save: + self.db_set("status", self.status) + + def validate(self): + self.validate_duplicate() + + def validate_duplicate(self): + table = frappe.qb.DocType("Closing Stock Balance") + + query = ( + frappe.qb.from_(table) + .select(table.name) + .where( + (table.docstatus == 1) + & (table.company == self.company) + & ( + (table.from_date.between(self.from_date, self.to_date)) + | (table.to_date.between(self.from_date, self.to_date)) + | (table.from_date >= self.from_date and table.to_date <= self.to_date) + ) + ) + ) + + for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]: + if self.get(fieldname): + query = query.where(table.get(fieldname) == self.get(fieldname)) + + query = query.run(as_dict=True) + + if query and query[0].name: + name = get_link_to_form("Closing Stock Balance", query[0].name) + msg = f"Closing Stock Balance {name} already exists for the selected date range" + frappe.throw(msg, title="Duplicate Closing Stock Balance") + + def on_submit(self): + self.set_status(save=True) + self.enqueue_job() + + def on_cancel(self): + self.set_status(save=True) + + @frappe.whitelist() + def enqueue_job(self): + enqueue(prepare_closing_stock_balance, name=self.name, queue="long", timeout=1500) + + def create_closing_stock_balance_entries(self): + columns, data = execute( + filters=frappe._dict( + { + "company": self.company, + "from_date": self.from_date, + "to_date": self.to_date, + "warehouse": self.warehouse, + "item_code": self.item_code, + "item_group": self.item_group, + "warehouse_type": self.warehouse_type, + "include_uom": self.include_uom, + "ignore_closing_balance": 1, + } + ) + ) + + create_json_gz_file({"columns": columns, "data": data}, self.doctype, self.name) + + def get_prepared_data(self): + if attachments := get_attachments(self.doctype, self.name): + attachment = attachments[0] + attached_file = frappe.get_doc("File", attachment.name) + + data = gzip_decompress(attached_file.get_content()) + if data := json.loads(data.decode("utf-8")): + data = data + + return parse_json(data) + + return frappe._dict({}) + + +def prepare_closing_stock_balance(name): + doc = frappe.get_doc("Closing Stock Balance", name) + + doc.db_set("status", "In Progress") + + try: + doc.create_closing_stock_balance_entries() + doc.db_set("status", "Completed") + except Exception as e: + doc.db_set("status", "Failed") + traceback = frappe.get_traceback() + + frappe.log_error("Closing Stock Balance Failed", traceback, doc.doctype, doc.name) diff --git a/erpnext/stock/doctype/closing_stock_balance/test_closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/test_closing_stock_balance.py new file mode 100644 index 00000000000..7d61f5c9c63 --- /dev/null +++ b/erpnext/stock/doctype/closing_stock_balance/test_closing_stock_balance.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestClosingStockBalance(FrappeTestCase): + pass diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index 9b3965d0d69..a2b3b91f11e 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -87,6 +87,11 @@ frappe.query_reports["Stock Balance"] = { "label": __('Show Stock Ageing Data'), "fieldtype": 'Check' }, + { + "fieldname": 'ignore_closing_balance', + "label": __('Ignore Closing Balance'), + "fieldtype": 'Check' + }, ], "formatter": function (value, row, column, data, default_formatter) { diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 66991a907fd..f24e232b7d0 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -7,15 +7,16 @@ from typing import Any, Dict, List, Optional, TypedDict import frappe from frappe import _ +from frappe.query_builder import Order from frappe.query_builder.functions import Coalesce, CombineDatetime -from frappe.utils import cint, date_diff, flt, getdate +from frappe.utils import add_days, cint, date_diff, flt, getdate from frappe.utils.nestedset import get_descendants_of import erpnext from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age -from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_valuation_in_progress +from erpnext.stock.utils import add_additional_uom_columns class StockBalanceFilter(TypedDict): @@ -35,68 +36,83 @@ SLEntry = Dict[str, Any] def execute(filters: Optional[StockBalanceFilter] = None): - is_reposting_item_valuation_in_progress() - if not filters: - filters = {} + return StockBalanceReport(filters).run() - if filters.get("company"): - company_currency = erpnext.get_company_currency(filters.get("company")) - else: - company_currency = frappe.db.get_single_value("Global Defaults", "default_currency") - include_uom = filters.get("include_uom") - columns = get_columns(filters) - items = get_items(filters) - sle = get_stock_ledger_entries(filters, items) +class StockBalanceReport(object): + def __init__(self, filters: Optional[StockBalanceFilter]) -> None: + self.filters = filters + self.from_date = getdate(filters.get("from_date")) + self.to_date = getdate(filters.get("to_date")) - if filters.get("show_stock_ageing_data"): - filters["show_warehouse_wise_stock"] = True - item_wise_fifo_queue = FIFOSlots(filters, sle).generate() + self.start_from = None + self.data = [] + self.columns = [] + self.sle_entries: List[SLEntry] = [] + self.set_company_currency() - # if no stock ledger entry found return - if not sle: - return columns, [] + def set_company_currency(self) -> None: + if self.filters.get("company"): + self.company_currency = erpnext.get_company_currency(self.filters.get("company")) + else: + self.company_currency = frappe.db.get_single_value("Global Defaults", "default_currency") - iwb_map = get_item_warehouse_map(filters, sle) - item_map = get_item_details(items, sle, filters) - item_reorder_detail_map = get_item_reorder_details(item_map.keys()) + def run(self): + self.float_precision = cint(frappe.db.get_default("float_precision")) or 3 - data = [] - conversion_factors = {} + self.inventory_dimensions = get_inventory_dimension_fields() + self.prepare_opening_data_from_closing_balance() + self.prepare_stock_ledger_entries() + self.prepare_new_data() - _func = itemgetter(1) + if not self.columns: + self.columns = self.get_columns() - to_date = filters.get("to_date") + self.add_additional_uom_columns() - for group_by_key in iwb_map: - item = group_by_key[1] - warehouse = group_by_key[2] - company = group_by_key[0] + return self.columns, self.data - if item_map.get(item): - qty_dict = iwb_map[group_by_key] - item_reorder_level = 0 - item_reorder_qty = 0 - if item + warehouse in item_reorder_detail_map: - item_reorder_level = item_reorder_detail_map[item + warehouse]["warehouse_reorder_level"] - item_reorder_qty = item_reorder_detail_map[item + warehouse]["warehouse_reorder_qty"] + def prepare_opening_data_from_closing_balance(self) -> None: + self.opening_data = frappe._dict({}) - report_data = { - "currency": company_currency, - "item_code": item, - "warehouse": warehouse, - "company": company, - "reorder_level": item_reorder_level, - "reorder_qty": item_reorder_qty, - } - report_data.update(item_map[item]) - report_data.update(qty_dict) + closing_balance = self.get_closing_balance() + if not closing_balance: + return - if include_uom: - conversion_factors.setdefault(item, item_map[item].conversion_factor) + self.start_from = add_days(closing_balance[0].to_date, 1) + res = frappe.get_doc("Closing Stock Balance", closing_balance[0].name).get_prepared_data() - if filters.get("show_stock_ageing_data"): - fifo_queue = item_wise_fifo_queue[(item, warehouse)].get("fifo_queue") + for entry in res.data: + entry = frappe._dict(entry) + + group_by_key = self.get_group_by_key(entry) + if group_by_key not in self.opening_data: + self.opening_data.setdefault(group_by_key, entry) + + def prepare_new_data(self): + if not self.sle_entries: + return + + if self.filters.get("show_stock_ageing_data"): + self.filters["show_warehouse_wise_stock"] = True + item_wise_fifo_queue = FIFOSlots(self.filters, self.sle_entries).generate() + + _func = itemgetter(1) + + self.item_warehouse_map = self.get_item_warehouse_map() + + variant_values = {} + if self.filters.get("show_variant_attributes"): + variant_values = self.get_variant_values_for() + + for key, report_data in self.item_warehouse_map.items(): + if variant_data := variant_values.get(report_data.item_code): + report_data.update(variant_data) + + if self.filters.get("show_stock_ageing_data"): + fifo_queue = item_wise_fifo_queue[(report_data.item_code, report_data.warehouse)].get( + "fifo_queue" + ) stock_ageing_data = {"average_age": 0, "earliest_age": 0, "latest_age": 0} if fifo_queue: @@ -104,321 +120,445 @@ def execute(filters: Optional[StockBalanceFilter] = None): if not fifo_queue: continue + to_date = self.to_date stock_ageing_data["average_age"] = get_average_age(fifo_queue, to_date) stock_ageing_data["earliest_age"] = date_diff(to_date, fifo_queue[0][1]) stock_ageing_data["latest_age"] = date_diff(to_date, fifo_queue[-1][1]) report_data.update(stock_ageing_data) - data.append(report_data) + self.data.append(report_data) - add_additional_uom_columns(columns, data, include_uom, conversion_factors) - return columns, data + def get_item_warehouse_map(self): + item_warehouse_map = {} + for entry in self.sle_entries: + group_by_key = self.get_group_by_key(entry) + if group_by_key not in item_warehouse_map: + self.initialize_data(item_warehouse_map, group_by_key, entry) -def get_columns(filters: StockBalanceFilter): - """return columns""" - columns = [ - { - "label": _("Item"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 100, - }, - {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, - { - "label": _("Item Group"), - "fieldname": "item_group", - "fieldtype": "Link", - "options": "Item Group", - "width": 100, - }, - { - "label": _("Warehouse"), - "fieldname": "warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "width": 100, - }, - ] + self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key) - for dimension in get_inventory_dimensions(): - columns.append( - { - "label": _(dimension.doctype), - "fieldname": dimension.fieldname, - "fieldtype": "Link", - "options": dimension.doctype, - "width": 110, - } + if self.opening_data.get(group_by_key): + del self.opening_data[group_by_key] + + for group_by_key, entry in self.opening_data.items(): + if group_by_key not in item_warehouse_map: + self.initialize_data(item_warehouse_map, group_by_key, entry) + + qty_dict = item_warehouse_map[group_by_key] + qty_dict.val_rate = entry.valuation_rate + qty_dict.bal_qty += flt(qty_dict.opening_qty) + qty_dict.bal_val += flt(qty_dict.opening_val) + + item_warehouse_map = filter_items_with_no_transactions( + item_warehouse_map, self.float_precision, self.inventory_dimensions ) - columns.extend( - [ - { - "label": _("Stock UOM"), - "fieldname": "stock_uom", - "fieldtype": "Link", - "options": "UOM", - "width": 90, - }, - { - "label": _("Balance Qty"), - "fieldname": "bal_qty", - "fieldtype": "Float", - "width": 100, - "convertible": "qty", - }, - { - "label": _("Balance Value"), - "fieldname": "bal_val", - "fieldtype": "Currency", - "width": 100, - "options": "currency", - }, - { - "label": _("Opening Qty"), - "fieldname": "opening_qty", - "fieldtype": "Float", - "width": 100, - "convertible": "qty", - }, - { - "label": _("Opening Value"), - "fieldname": "opening_val", - "fieldtype": "Currency", - "width": 110, - "options": "currency", - }, - { - "label": _("In Qty"), - "fieldname": "in_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty", - }, - {"label": _("In Value"), "fieldname": "in_val", "fieldtype": "Float", "width": 80}, - { - "label": _("Out Qty"), - "fieldname": "out_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty", - }, - {"label": _("Out Value"), "fieldname": "out_val", "fieldtype": "Float", "width": 80}, - { - "label": _("Valuation Rate"), - "fieldname": "val_rate", - "fieldtype": "Currency", - "width": 90, - "convertible": "rate", - "options": "currency", - }, - { - "label": _("Reorder Level"), - "fieldname": "reorder_level", - "fieldtype": "Float", - "width": 80, - "convertible": "qty", - }, - { - "label": _("Reorder Qty"), - "fieldname": "reorder_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty", - }, - { - "label": _("Company"), - "fieldname": "company", - "fieldtype": "Link", - "options": "Company", - "width": 100, - }, - ] - ) + return item_warehouse_map - if filters.get("show_stock_ageing_data"): - columns += [ - {"label": _("Average Age"), "fieldname": "average_age", "width": 100}, - {"label": _("Earliest Age"), "fieldname": "earliest_age", "width": 100}, - {"label": _("Latest Age"), "fieldname": "latest_age", "width": 100}, - ] + def prepare_item_warehouse_map(self, item_warehouse_map, entry, group_by_key): + opening_vouchers = self.get_opening_vouchers() - if filters.get("show_variant_attributes"): - columns += [ - {"label": att_name, "fieldname": att_name, "width": 100} - for att_name in get_variants_attributes() - ] + qty_dict = item_warehouse_map[group_by_key] + for field in self.inventory_dimensions: + qty_dict[field] = entry.get(field) - return columns - - -def apply_conditions(query, filters): - sle = frappe.qb.DocType("Stock Ledger Entry") - warehouse_table = frappe.qb.DocType("Warehouse") - - if not filters.get("from_date"): - frappe.throw(_("'From Date' is required")) - - if to_date := filters.get("to_date"): - query = query.where(sle.posting_date <= to_date) - else: - frappe.throw(_("'To Date' is required")) - - if company := filters.get("company"): - query = query.where(sle.company == company) - - if filters.get("warehouse"): - query = apply_warehouse_filter(query, sle, filters) - elif warehouse_type := filters.get("warehouse_type"): - query = ( - query.join(warehouse_table) - .on(warehouse_table.name == sle.warehouse) - .where(warehouse_table.warehouse_type == warehouse_type) - ) - - return query - - -def get_stock_ledger_entries(filters: StockBalanceFilter, items: List[str]) -> List[SLEntry]: - sle = frappe.qb.DocType("Stock Ledger Entry") - - query = ( - frappe.qb.from_(sle) - .select( - sle.item_code, - sle.warehouse, - sle.posting_date, - sle.actual_qty, - sle.valuation_rate, - sle.company, - sle.voucher_type, - sle.qty_after_transaction, - sle.stock_value_difference, - sle.item_code.as_("name"), - sle.voucher_no, - sle.stock_value, - sle.batch_no, - ) - .where((sle.docstatus < 2) & (sle.is_cancelled == 0)) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) - .orderby(sle.creation) - .orderby(sle.actual_qty) - ) - - inventory_dimension_fields = get_inventory_dimension_fields() - if inventory_dimension_fields: - for fieldname in inventory_dimension_fields: - query = query.select(fieldname) - if fieldname in filters and filters.get(fieldname): - query = query.where(sle[fieldname].isin(filters.get(fieldname))) - - if items: - query = query.where(sle.item_code.isin(items)) - - query = apply_conditions(query, filters) - return query.run(as_dict=True) - - -def get_opening_vouchers(to_date): - opening_vouchers = {"Stock Entry": [], "Stock Reconciliation": []} - - se = frappe.qb.DocType("Stock Entry") - sr = frappe.qb.DocType("Stock Reconciliation") - - vouchers_data = ( - frappe.qb.from_( - ( - frappe.qb.from_(se) - .select(se.name, Coalesce("Stock Entry").as_("voucher_type")) - .where((se.docstatus == 1) & (se.posting_date <= to_date) & (se.is_opening == "Yes")) - ) - + ( - frappe.qb.from_(sr) - .select(sr.name, Coalesce("Stock Reconciliation").as_("voucher_type")) - .where((sr.docstatus == 1) & (sr.posting_date <= to_date) & (sr.purpose == "Opening Stock")) - ) - ).select("voucher_type", "name") - ).run(as_dict=True) - - if vouchers_data: - for d in vouchers_data: - opening_vouchers[d.voucher_type].append(d.name) - - return opening_vouchers - - -def get_inventory_dimension_fields(): - return [dimension.fieldname for dimension in get_inventory_dimensions()] - - -def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]): - iwb_map = {} - from_date = getdate(filters.get("from_date")) - to_date = getdate(filters.get("to_date")) - opening_vouchers = get_opening_vouchers(to_date) - float_precision = cint(frappe.db.get_default("float_precision")) or 3 - inventory_dimensions = get_inventory_dimension_fields() - - for d in sle: - group_by_key = get_group_by_key(d, filters, inventory_dimensions) - if group_by_key not in iwb_map: - iwb_map[group_by_key] = frappe._dict( - { - "opening_qty": 0.0, - "opening_val": 0.0, - "in_qty": 0.0, - "in_val": 0.0, - "out_qty": 0.0, - "out_val": 0.0, - "bal_qty": 0.0, - "bal_val": 0.0, - "val_rate": 0.0, - } - ) - - qty_dict = iwb_map[group_by_key] - for field in inventory_dimensions: - qty_dict[field] = d.get(field) - - if d.voucher_type == "Stock Reconciliation" and not d.batch_no: - qty_diff = flt(d.qty_after_transaction) - flt(qty_dict.bal_qty) + if entry.voucher_type == "Stock Reconciliation" and not entry.batch_no: + qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty) else: - qty_diff = flt(d.actual_qty) + qty_diff = flt(entry.actual_qty) - value_diff = flt(d.stock_value_difference) + value_diff = flt(entry.stock_value_difference) - if d.posting_date < from_date or d.voucher_no in opening_vouchers.get(d.voucher_type, []): + if entry.posting_date < self.from_date or entry.voucher_no in opening_vouchers.get( + entry.voucher_type, [] + ): qty_dict.opening_qty += qty_diff qty_dict.opening_val += value_diff - elif d.posting_date >= from_date and d.posting_date <= to_date: - if flt(qty_diff, float_precision) >= 0: + elif entry.posting_date >= self.from_date and entry.posting_date <= self.to_date: + + if flt(qty_diff, self.float_precision) >= 0: qty_dict.in_qty += qty_diff qty_dict.in_val += value_diff else: qty_dict.out_qty += abs(qty_diff) qty_dict.out_val += abs(value_diff) - qty_dict.val_rate = d.valuation_rate + # if self.opening_data and group_by_key in self.opening_data: + # qty_diff += flt(qty_dict.opening_qty) + # value_diff += flt(qty_dict.opening_val) + + qty_dict.val_rate = entry.valuation_rate qty_dict.bal_qty += qty_diff qty_dict.bal_val += value_diff - iwb_map = filter_items_with_no_transactions(iwb_map, float_precision, inventory_dimensions) + def initialize_data(self, item_warehouse_map, group_by_key, entry): + opening_data = self.opening_data.get(group_by_key, {}) - return iwb_map + item_warehouse_map[group_by_key] = frappe._dict( + { + "item_code": entry.item_code, + "warehouse": entry.warehouse, + "item_group": entry.item_group, + "company": entry.company, + "currency": self.company_currency, + "stock_uom": entry.stock_uom, + "item_name": entry.item_name, + "opening_qty": opening_data.get("bal_qty") or 0.0, + "opening_val": opening_data.get("bal_val") or 0.0, + "in_qty": 0.0, + "in_val": 0.0, + "out_qty": 0.0, + "out_val": 0.0, + "bal_qty": 0.0, + "bal_val": 0.0, + "val_rate": 0.0, + } + ) + + def get_group_by_key(self, row) -> tuple: + group_by_key = [row.company, row.item_code, row.warehouse] + + for fieldname in self.inventory_dimensions: + if self.filters.get(fieldname): + group_by_key.append(row.get(fieldname)) + + return tuple(group_by_key) + + def get_closing_balance(self) -> List[Dict[str, Any]]: + if self.filters.get("ignore_closing_balance"): + return [] + + table = frappe.qb.DocType("Closing Stock Balance") + + query = ( + frappe.qb.from_(table) + .select(table.name, table.to_date) + .where( + (table.docstatus == 1) + & (table.company == self.filters.company) + & ((table.to_date <= self.from_date)) + ) + .orderby(table.to_date, order=Order.desc) + .limit(1) + ) + + for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]: + if self.filters.get(fieldname): + query = query.where(table[fieldname] == self.filters.get(fieldname)) + + return query.run(as_dict=True) + + def prepare_stock_ledger_entries(self): + sle = frappe.qb.DocType("Stock Ledger Entry") + item_table = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(sle) + .inner_join(item_table) + .on(sle.item_code == item_table.name) + .select( + sle.item_code, + sle.warehouse, + sle.posting_date, + sle.actual_qty, + sle.valuation_rate, + sle.company, + sle.voucher_type, + sle.qty_after_transaction, + sle.stock_value_difference, + sle.item_code.as_("name"), + sle.voucher_no, + sle.stock_value, + sle.batch_no, + item_table.item_group, + item_table.stock_uom, + item_table.item_name, + ) + .where((sle.docstatus < 2) & (sle.is_cancelled == 0)) + .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.creation) + .orderby(sle.actual_qty) + ) + + query = self.apply_inventory_dimensions_filters(query, sle) + query = self.apply_warehouse_filters(query, sle) + query = self.apply_items_filters(query, item_table) + query = self.apply_date_filters(query, sle) + + if self.filters.get("company"): + query = query.where(sle.company == self.filters.get("company")) + + self.sle_entries = query.run(as_dict=True) + + def apply_inventory_dimensions_filters(self, query, sle) -> str: + inventory_dimension_fields = get_inventory_dimension_fields() + if inventory_dimension_fields: + for fieldname in inventory_dimension_fields: + query = query.select(fieldname) + if self.filters.get(fieldname): + query = query.where(sle[fieldname].isin(self.filters.get(fieldname))) + + return query + + def apply_warehouse_filters(self, query, sle) -> str: + warehouse_table = frappe.qb.DocType("Warehouse") + + if self.filters.get("warehouse"): + query = apply_warehouse_filter(query, sle, self.filters) + elif warehouse_type := self.filters.get("warehouse_type"): + query = ( + query.join(warehouse_table) + .on(warehouse_table.name == sle.warehouse) + .where(warehouse_table.warehouse_type == warehouse_type) + ) + + return query + + def apply_items_filters(self, query, item_table) -> str: + if item_group := self.filters.get("item_group"): + children = get_descendants_of("Item Group", item_group, ignore_permissions=True) + query = query.where(item_table.item_group.isin(children + [item_group])) + + for field in ["item_code", "brand"]: + if not self.filters.get(field): + continue + + query = query.where(item_table[field] == self.filters.get(field)) + + return query + + def apply_date_filters(self, query, sle) -> str: + if not self.filters.ignore_closing_balance and self.start_from: + query = query.where(sle.posting_date >= self.start_from) + + if self.to_date: + query = query.where(sle.posting_date <= self.to_date) + + return query + + def get_columns(self): + columns = [ + { + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, + {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + }, + ] + + for dimension in get_inventory_dimensions(): + columns.append( + { + "label": _(dimension.doctype), + "fieldname": dimension.fieldname, + "fieldtype": "Link", + "options": dimension.doctype, + "width": 110, + } + ) + + columns.extend( + [ + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, + { + "label": _("Balance Qty"), + "fieldname": "bal_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Balance Value"), + "fieldname": "bal_val", + "fieldtype": "Currency", + "width": 100, + "options": "currency", + }, + { + "label": _("Opening Qty"), + "fieldname": "opening_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Opening Value"), + "fieldname": "opening_val", + "fieldtype": "Currency", + "width": 110, + "options": "currency", + }, + { + "label": _("In Qty"), + "fieldname": "in_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + {"label": _("In Value"), "fieldname": "in_val", "fieldtype": "Float", "width": 80}, + { + "label": _("Out Qty"), + "fieldname": "out_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + {"label": _("Out Value"), "fieldname": "out_val", "fieldtype": "Float", "width": 80}, + { + "label": _("Valuation Rate"), + "fieldname": "val_rate", + "fieldtype": "Currency", + "width": 90, + "convertible": "rate", + "options": "currency", + }, + { + "label": _("Reorder Level"), + "fieldname": "reorder_level", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Reorder Qty"), + "fieldname": "reorder_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 100, + }, + ] + ) + + if self.filters.get("show_stock_ageing_data"): + columns += [ + {"label": _("Average Age"), "fieldname": "average_age", "width": 100}, + {"label": _("Earliest Age"), "fieldname": "earliest_age", "width": 100}, + {"label": _("Latest Age"), "fieldname": "latest_age", "width": 100}, + ] + + if self.filters.get("show_variant_attributes"): + columns += [ + {"label": att_name, "fieldname": att_name, "width": 100} + for att_name in get_variants_attributes() + ] + + return columns + + def add_additional_uom_columns(self): + if not self.filters.get("include_uom"): + return + + conversion_factors = self.get_itemwise_conversion_factor() + add_additional_uom_columns(self.columns, self.data, self.filters.include_uom, conversion_factors) + + def get_itemwise_conversion_factor(self): + items = [] + if self.filters.item_code or self.filters.item_group: + items = [d.item_code for d in self.data] + + table = frappe.qb.DocType("UOM Conversion Detail") + query = ( + frappe.qb.from_(table) + .select(table.conversion_factor) + .where((table.parenttype == "Item") & (table.uom == self.filters.include_uom)) + ) + + if items: + query = query.where(table.parent.isin(items)) + + result = query.run(as_dict=1) + if not result: + return {} + + return {d.parent: d.conversion_factor for d in result} + + def get_variant_values_for(self): + """Returns variant values for items.""" + attribute_map = {} + items = [] + if self.filters.item_code or self.filters.item_group: + items = [d.item_code for d in self.data] + + filters = {} + if items: + filters = {"parent": ("in", items)} + + attribute_info = frappe.get_all( + "Item Variant Attribute", + fields=["parent", "attribute", "attribute_value"], + filters=filters, + ) + + for attr in attribute_info: + attribute_map.setdefault(attr["parent"], {}) + attribute_map[attr["parent"]].update({attr["attribute"]: attr["attribute_value"]}) + + return attribute_map + + def get_opening_vouchers(self): + opening_vouchers = {"Stock Entry": [], "Stock Reconciliation": []} + + se = frappe.qb.DocType("Stock Entry") + sr = frappe.qb.DocType("Stock Reconciliation") + + vouchers_data = ( + frappe.qb.from_( + ( + frappe.qb.from_(se) + .select(se.name, Coalesce("Stock Entry").as_("voucher_type")) + .where((se.docstatus == 1) & (se.posting_date <= self.to_date) & (se.is_opening == "Yes")) + ) + + ( + frappe.qb.from_(sr) + .select(sr.name, Coalesce("Stock Reconciliation").as_("voucher_type")) + .where( + (sr.docstatus == 1) & (sr.posting_date <= self.to_date) & (sr.purpose == "Opening Stock") + ) + ) + ).select("voucher_type", "name") + ).run(as_dict=True) + + if vouchers_data: + for d in vouchers_data: + opening_vouchers[d.voucher_type].append(d.name) + + return opening_vouchers -def get_group_by_key(row, filters, inventory_dimension_fields) -> tuple: - group_by_key = [row.company, row.item_code, row.warehouse] - - for fieldname in inventory_dimension_fields: - if filters.get(fieldname): - group_by_key.append(row.get(fieldname)) - - return tuple(group_by_key) +def get_inventory_dimension_fields(): + return [dimension.fieldname for dimension in get_inventory_dimensions()] def filter_items_with_no_transactions(iwb_map, float_precision: float, inventory_dimensions: list): @@ -431,6 +571,9 @@ def filter_items_with_no_transactions(iwb_map, float_precision: float, inventory if key in inventory_dimensions: continue + if key in ["item_code", "warehouse", "item_name", "item_group", "projecy", "stock_uom"]: + continue + val = flt(val, float_precision) qty_dict[key] = val if key != "val_rate" and val: @@ -445,96 +588,6 @@ def filter_items_with_no_transactions(iwb_map, float_precision: float, inventory return iwb_map -def get_items(filters: StockBalanceFilter) -> List[str]: - "Get items based on item code, item group or brand." - if item_code := filters.get("item_code"): - return [item_code] - else: - item_filters = {} - if item_group := filters.get("item_group"): - children = get_descendants_of("Item Group", item_group, ignore_permissions=True) - item_filters["item_group"] = ("in", children + [item_group]) - if brand := filters.get("brand"): - item_filters["brand"] = brand - - return frappe.get_all("Item", filters=item_filters, pluck="name", order_by=None) - - -def get_item_details(items: List[str], sle: List[SLEntry], filters: StockBalanceFilter): - item_details = {} - if not items: - items = list(set(d.item_code for d in sle)) - - if not items: - return item_details - - item_table = frappe.qb.DocType("Item") - - query = ( - frappe.qb.from_(item_table) - .select( - item_table.name, - item_table.item_name, - item_table.description, - item_table.item_group, - item_table.brand, - item_table.stock_uom, - ) - .where(item_table.name.isin(items)) - ) - - if uom := filters.get("include_uom"): - uom_conv_detail = frappe.qb.DocType("UOM Conversion Detail") - query = ( - query.left_join(uom_conv_detail) - .on((uom_conv_detail.parent == item_table.name) & (uom_conv_detail.uom == uom)) - .select(uom_conv_detail.conversion_factor) - ) - - result = query.run(as_dict=1) - - for item_table in result: - item_details.setdefault(item_table.name, item_table) - - if filters.get("show_variant_attributes"): - variant_values = get_variant_values_for(list(item_details)) - item_details = {k: v.update(variant_values.get(k, {})) for k, v in item_details.items()} - - return item_details - - -def get_item_reorder_details(items): - item_reorder_details = frappe._dict() - - if items: - item_reorder_details = frappe.get_all( - "Item Reorder", - ["parent", "warehouse", "warehouse_reorder_qty", "warehouse_reorder_level"], - filters={"parent": ("in", items)}, - ) - - return dict((d.parent + d.warehouse, d) for d in item_reorder_details) - - def get_variants_attributes() -> List[str]: """Return all item variant attributes.""" return frappe.get_all("Item Attribute", pluck="name") - - -def get_variant_values_for(items): - """Returns variant values for items.""" - attribute_map = {} - - attribute_info = frappe.get_all( - "Item Variant Attribute", - ["parent", "attribute", "attribute_value"], - { - "parent": ("in", items), - }, - ) - - for attr in attribute_info: - attribute_map.setdefault(attr["parent"], {}) - attribute_map[attr["parent"]].update({attr["attribute"]: attr["attribute_value"]}) - - return attribute_map From 56ba7d6a8acec4fd2463a2419443eef2b3e4c4c6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 18 May 2023 00:05:54 +0530 Subject: [PATCH 09/46] fix: balance quantity (cherry picked from commit 545b2d32cd78a473c0612196812abd18ead2d993) --- .../closing_stock_balance.py | 2 + .../report/stock_balance/stock_balance.js | 3 +- .../report/stock_balance/stock_balance.py | 71 ++++++++++--------- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py index 544c9f3e1d2..1fbba27660a 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py @@ -83,6 +83,8 @@ class ClosingStockBalance(Document): "warehouse_type": self.warehouse_type, "include_uom": self.include_uom, "ignore_closing_balance": 1, + "show_variant_attributes": 1, + "show_stock_ageing_data": 1, } ) ) diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index a2b3b91f11e..33ed955a5c4 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -90,7 +90,8 @@ frappe.query_reports["Stock Balance"] = { { "fieldname": 'ignore_closing_balance', "label": __('Ignore Closing Balance'), - "fieldtype": 'Check' + "fieldtype": 'Check', + "default": 1 }, ], diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index f24e232b7d0..a757add318f 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -60,7 +60,7 @@ class StockBalanceReport(object): def run(self): self.float_precision = cint(frappe.db.get_default("float_precision")) or 3 - self.inventory_dimensions = get_inventory_dimension_fields() + self.inventory_dimensions = self.get_inventory_dimension_fields() self.prepare_opening_data_from_closing_balance() self.prepare_stock_ledger_entries() self.prepare_new_data() @@ -110,13 +110,18 @@ class StockBalanceReport(object): report_data.update(variant_data) if self.filters.get("show_stock_ageing_data"): - fifo_queue = item_wise_fifo_queue[(report_data.item_code, report_data.warehouse)].get( - "fifo_queue" - ) + opening_fifo_queue = self.get_opening_fifo_queue(report_data) or [] + + fifo_queue = [] + if fifo_queue := item_wise_fifo_queue.get((report_data.item_code, report_data.warehouse)): + fifo_queue = fifo_queue.get("fifo_queue") + + if fifo_queue: + opening_fifo_queue.extend(fifo_queue) stock_ageing_data = {"average_age": 0, "earliest_age": 0, "latest_age": 0} - if fifo_queue: - fifo_queue = sorted(filter(_func, fifo_queue), key=_func) + if opening_fifo_queue: + fifo_queue = sorted(filter(_func, opening_fifo_queue), key=_func) if not fifo_queue: continue @@ -124,6 +129,7 @@ class StockBalanceReport(object): stock_ageing_data["average_age"] = get_average_age(fifo_queue, to_date) stock_ageing_data["earliest_age"] = date_diff(to_date, fifo_queue[0][1]) stock_ageing_data["latest_age"] = date_diff(to_date, fifo_queue[-1][1]) + stock_ageing_data["fifo_queue"] = fifo_queue report_data.update(stock_ageing_data) @@ -146,11 +152,6 @@ class StockBalanceReport(object): if group_by_key not in item_warehouse_map: self.initialize_data(item_warehouse_map, group_by_key, entry) - qty_dict = item_warehouse_map[group_by_key] - qty_dict.val_rate = entry.valuation_rate - qty_dict.bal_qty += flt(qty_dict.opening_qty) - qty_dict.bal_val += flt(qty_dict.opening_val) - item_warehouse_map = filter_items_with_no_transactions( item_warehouse_map, self.float_precision, self.inventory_dimensions ) @@ -186,10 +187,6 @@ class StockBalanceReport(object): qty_dict.out_qty += abs(qty_diff) qty_dict.out_val += abs(value_diff) - # if self.opening_data and group_by_key in self.opening_data: - # qty_diff += flt(qty_dict.opening_qty) - # value_diff += flt(qty_dict.opening_val) - qty_dict.val_rate = entry.valuation_rate qty_dict.bal_qty += qty_diff qty_dict.bal_val += value_diff @@ -208,12 +205,13 @@ class StockBalanceReport(object): "item_name": entry.item_name, "opening_qty": opening_data.get("bal_qty") or 0.0, "opening_val": opening_data.get("bal_val") or 0.0, + "opening_fifo_queue": opening_data.get("fifo_queue") or [], "in_qty": 0.0, "in_val": 0.0, "out_qty": 0.0, "out_val": 0.0, - "bal_qty": 0.0, - "bal_val": 0.0, + "bal_qty": opening_data.get("bal_qty") or 0.0, + "bal_val": opening_data.get("bal_val") or 0.0, "val_rate": 0.0, } ) @@ -294,7 +292,7 @@ class StockBalanceReport(object): self.sle_entries = query.run(as_dict=True) def apply_inventory_dimensions_filters(self, query, sle) -> str: - inventory_dimension_fields = get_inventory_dimension_fields() + inventory_dimension_fields = self.get_inventory_dimension_fields() if inventory_dimension_fields: for fieldname in inventory_dimension_fields: query = query.select(fieldname) @@ -437,20 +435,6 @@ class StockBalanceReport(object): "convertible": "rate", "options": "currency", }, - { - "label": _("Reorder Level"), - "fieldname": "reorder_level", - "fieldtype": "Float", - "width": 80, - "convertible": "qty", - }, - { - "label": _("Reorder Qty"), - "fieldname": "reorder_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty", - }, { "label": _("Company"), "fieldname": "company", @@ -556,9 +540,17 @@ class StockBalanceReport(object): return opening_vouchers + @staticmethod + def get_inventory_dimension_fields(): + return [dimension.fieldname for dimension in get_inventory_dimensions()] -def get_inventory_dimension_fields(): - return [dimension.fieldname for dimension in get_inventory_dimensions()] + @staticmethod + def get_opening_fifo_queue(report_data): + opening_fifo_queue = report_data.get("opening_fifo_queue") or [] + for row in opening_fifo_queue: + row[1] = getdate(row[1]) + + return opening_fifo_queue def filter_items_with_no_transactions(iwb_map, float_precision: float, inventory_dimensions: list): @@ -571,7 +563,16 @@ def filter_items_with_no_transactions(iwb_map, float_precision: float, inventory if key in inventory_dimensions: continue - if key in ["item_code", "warehouse", "item_name", "item_group", "projecy", "stock_uom"]: + if key in [ + "item_code", + "warehouse", + "item_name", + "item_group", + "projecy", + "stock_uom", + "company", + "opening_fifo_queue", + ]: continue val = flt(val, float_precision) From 205899348a5df33757e154ee6641728e629d3107 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 19 May 2023 16:31:17 +0530 Subject: [PATCH 10/46] fix: Stock Analytics and Warehouse wise Item Balance Age and Value issue (cherry picked from commit 3f548ac91052a8c323ec42f2e8e86e006f3d03cd) --- .../closing_stock_balance.js | 16 +++ .../closing_stock_balance.py | 15 ++- .../stock/report/stock_ageing/stock_ageing.py | 2 +- .../report/stock_analytics/stock_analytics.py | 116 +++++++++++++++++- .../report/stock_balance/stock_balance.py | 19 +-- ...rehouse_wise_item_balance_age_and_value.py | 64 +++++++++- 6 files changed, 213 insertions(+), 19 deletions(-) diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js index 6e90884f56c..5c807a80a04 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js @@ -4,6 +4,7 @@ frappe.ui.form.on("Closing Stock Balance", { refresh(frm) { frm.trigger("generate_closing_balance"); + frm.trigger("regenerate_closing_balance"); }, generate_closing_balance(frm) { @@ -19,5 +20,20 @@ frappe.ui.form.on("Closing Stock Balance", { }) }) } + }, + + regenerate_closing_balance(frm) { + if (frm.doc.status == "Completed") { + frm.add_custom_button(__("Regenerate Closing Stock Balance"), () => { + frm.call({ + method: "regenerate_closing_balance", + doc: frm.doc, + freeze: true, + callback: () => { + frm.reload_doc(); + } + }) + }) + } } }); diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py index 1fbba27660a..a7963726ae3 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py @@ -4,6 +4,7 @@ import json import frappe +from frappe import _ from frappe.core.doctype.prepared_report.prepared_report import create_json_gz_file from frappe.desk.form.load import get_attachments from frappe.model.document import Document @@ -57,7 +58,7 @@ class ClosingStockBalance(Document): if query and query[0].name: name = get_link_to_form("Closing Stock Balance", query[0].name) msg = f"Closing Stock Balance {name} already exists for the selected date range" - frappe.throw(msg, title="Duplicate Closing Stock Balance") + frappe.throw(_(msg), title=_("Duplicate Closing Stock Balance")) def on_submit(self): self.set_status(save=True) @@ -65,11 +66,23 @@ class ClosingStockBalance(Document): def on_cancel(self): self.set_status(save=True) + self.clear_attachment() @frappe.whitelist() def enqueue_job(self): + self.db_set("status", "In Progress") + self.clear_attachment() enqueue(prepare_closing_stock_balance, name=self.name, queue="long", timeout=1500) + @frappe.whitelist() + def regenerate_closing_balance(self): + self.enqueue_job() + + def clear_attachment(self): + if attachments := get_attachments(self.doctype, self.name): + attachment = attachments[0] + frappe.delete_doc("File", attachment.name) + def create_closing_stock_balance_entries(self): columns, data = execute( filters=frappe._dict( diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 2fa97ae3545..d3f1f31af48 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -281,7 +281,7 @@ class FIFOSlots: # consume transfer data and add stock to fifo queue self.__adjust_incoming_transfer_qty(transfer_data, fifo_queue, row) else: - if not serial_nos: + if not serial_nos and not row.get("has_serial_no"): if fifo_queue and flt(fifo_queue[0][0]) <= 0: # neutralize 0/negative stock by adding positive stock fifo_queue[0][0] += flt(row.actual_qty) diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index 27b94ab3f96..6c5b58c6e45 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -5,15 +5,13 @@ from typing import List import frappe from frappe import _, scrub +from frappe.query_builder.functions import CombineDatetime from frappe.utils import get_first_day as get_first_day_of_month from frappe.utils import get_first_day_of_week, get_quarter_start, getdate +from frappe.utils.nestedset import get_descendants_of from erpnext.accounts.utils import get_fiscal_year -from erpnext.stock.report.stock_balance.stock_balance import ( - get_item_details, - get_items, - get_stock_ledger_entries, -) +from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter from erpnext.stock.utils import is_reposting_item_valuation_in_progress @@ -231,7 +229,7 @@ def get_data(filters): data = [] items = get_items(filters) sle = get_stock_ledger_entries(filters, items) - item_details = get_item_details(items, sle, filters) + item_details = get_item_details(items, sle) periodic_data = get_periodic_data(sle, filters) ranges = get_period_date_ranges(filters) @@ -265,3 +263,109 @@ def get_chart_data(columns): chart["type"] = "line" return chart + + +def get_items(filters): + "Get items based on item code, item group or brand." + if item_code := filters.get("item_code"): + return [item_code] + else: + item_filters = {} + if item_group := filters.get("item_group"): + children = get_descendants_of("Item Group", item_group, ignore_permissions=True) + item_filters["item_group"] = ("in", children + [item_group]) + if brand := filters.get("brand"): + item_filters["brand"] = brand + + return frappe.get_all("Item", filters=item_filters, pluck="name", order_by=None) + + +def get_stock_ledger_entries(filters, items): + sle = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(sle) + .select( + sle.item_code, + sle.warehouse, + sle.posting_date, + sle.actual_qty, + sle.valuation_rate, + sle.company, + sle.voucher_type, + sle.qty_after_transaction, + sle.stock_value_difference, + sle.item_code.as_("name"), + sle.voucher_no, + sle.stock_value, + sle.batch_no, + ) + .where((sle.docstatus < 2) & (sle.is_cancelled == 0)) + .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.creation) + .orderby(sle.actual_qty) + ) + + if items: + query = query.where(sle.item_code.isin(items)) + + query = apply_conditions(query, filters) + return query.run(as_dict=True) + + +def apply_conditions(query, filters): + sle = frappe.qb.DocType("Stock Ledger Entry") + warehouse_table = frappe.qb.DocType("Warehouse") + + if not filters.get("from_date"): + frappe.throw(_("'From Date' is required")) + + if to_date := filters.get("to_date"): + query = query.where(sle.posting_date <= to_date) + else: + frappe.throw(_("'To Date' is required")) + + if company := filters.get("company"): + query = query.where(sle.company == company) + + if filters.get("warehouse"): + query = apply_warehouse_filter(query, sle, filters) + elif warehouse_type := filters.get("warehouse_type"): + query = ( + query.join(warehouse_table) + .on(warehouse_table.name == sle.warehouse) + .where(warehouse_table.warehouse_type == warehouse_type) + ) + + return query + + +def get_item_details(items, sle): + item_details = {} + if not items: + items = list(set(d.item_code for d in sle)) + + if not items: + return item_details + + item_table = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(item_table) + .select( + item_table.name, + item_table.item_name, + item_table.description, + item_table.item_group, + item_table.brand, + item_table.stock_uom, + ) + .where(item_table.name.isin(items)) + ) + + result = query.run(as_dict=1) + + for item_table in result: + item_details.setdefault(item_table.name, item_table) + + return item_details diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index a757add318f..68df918e83e 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -137,6 +137,7 @@ class StockBalanceReport(object): def get_item_warehouse_map(self): item_warehouse_map = {} + self.opening_vouchers = self.get_opening_vouchers() for entry in self.sle_entries: group_by_key = self.get_group_by_key(entry) @@ -159,20 +160,18 @@ class StockBalanceReport(object): return item_warehouse_map def prepare_item_warehouse_map(self, item_warehouse_map, entry, group_by_key): - opening_vouchers = self.get_opening_vouchers() - qty_dict = item_warehouse_map[group_by_key] for field in self.inventory_dimensions: qty_dict[field] = entry.get(field) - if entry.voucher_type == "Stock Reconciliation" and not entry.batch_no: + if entry.voucher_type == "Stock Reconciliation" and (not entry.batch_no or entry.serial_no): qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty) else: qty_diff = flt(entry.actual_qty) value_diff = flt(entry.stock_value_difference) - if entry.posting_date < self.from_date or entry.voucher_no in opening_vouchers.get( + if entry.posting_date < self.from_date or entry.voucher_no in self.opening_vouchers.get( entry.voucher_type, [] ): qty_dict.opening_qty += qty_diff @@ -271,6 +270,7 @@ class StockBalanceReport(object): sle.voucher_no, sle.stock_value, sle.batch_no, + sle.serial_no, item_table.item_group, item_table.stock_uom, item_table.item_name, @@ -475,7 +475,10 @@ class StockBalanceReport(object): table = frappe.qb.DocType("UOM Conversion Detail") query = ( frappe.qb.from_(table) - .select(table.conversion_factor) + .select( + table.conversion_factor, + table.parent, + ) .where((table.parenttype == "Item") & (table.uom == self.filters.include_uom)) ) @@ -553,14 +556,16 @@ class StockBalanceReport(object): return opening_fifo_queue -def filter_items_with_no_transactions(iwb_map, float_precision: float, inventory_dimensions: list): +def filter_items_with_no_transactions( + iwb_map, float_precision: float, inventory_dimensions: list = None +): pop_keys = [] for group_by_key in iwb_map: qty_dict = iwb_map[group_by_key] no_transactions = True for key, val in qty_dict.items(): - if key in inventory_dimensions: + if inventory_dimensions and key in inventory_dimensions: continue if key in [ diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index abbb33b2f16..5dbdceff247 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -8,15 +8,15 @@ import frappe from frappe import _ from frappe.query_builder.functions import Count -from frappe.utils import flt +from frappe.utils import cint, flt, getdate from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age -from erpnext.stock.report.stock_balance.stock_balance import ( +from erpnext.stock.report.stock_analytics.stock_analytics import ( get_item_details, - get_item_warehouse_map, get_items, get_stock_ledger_entries, ) +from erpnext.stock.report.stock_balance.stock_balance import filter_items_with_no_transactions from erpnext.stock.utils import is_reposting_item_valuation_in_progress @@ -32,7 +32,7 @@ def execute(filters=None): items = get_items(filters) sle = get_stock_ledger_entries(filters, items) - item_map = get_item_details(items, sle, filters) + item_map = get_item_details(items, sle) iwb_map = get_item_warehouse_map(filters, sle) warehouse_list = get_warehouse_list(filters) item_ageing = FIFOSlots(filters).generate() @@ -128,3 +128,59 @@ def add_warehouse_column(columns, warehouse_list): for wh in warehouse_list: columns += [_(wh.name) + ":Int:100"] + + +def get_item_warehouse_map(filters, sle): + iwb_map = {} + from_date = getdate(filters.get("from_date")) + to_date = getdate(filters.get("to_date")) + float_precision = cint(frappe.db.get_default("float_precision")) or 3 + + for d in sle: + group_by_key = get_group_by_key(d) + if group_by_key not in iwb_map: + iwb_map[group_by_key] = frappe._dict( + { + "opening_qty": 0.0, + "opening_val": 0.0, + "in_qty": 0.0, + "in_val": 0.0, + "out_qty": 0.0, + "out_val": 0.0, + "bal_qty": 0.0, + "bal_val": 0.0, + "val_rate": 0.0, + } + ) + + qty_dict = iwb_map[group_by_key] + if d.voucher_type == "Stock Reconciliation" and not d.batch_no: + qty_diff = flt(d.qty_after_transaction) - flt(qty_dict.bal_qty) + else: + qty_diff = flt(d.actual_qty) + + value_diff = flt(d.stock_value_difference) + + if d.posting_date < from_date: + qty_dict.opening_qty += qty_diff + qty_dict.opening_val += value_diff + + elif d.posting_date >= from_date and d.posting_date <= to_date: + if flt(qty_diff, float_precision) >= 0: + qty_dict.in_qty += qty_diff + qty_dict.in_val += value_diff + else: + qty_dict.out_qty += abs(qty_diff) + qty_dict.out_val += abs(value_diff) + + qty_dict.val_rate = d.valuation_rate + qty_dict.bal_qty += qty_diff + qty_dict.bal_val += value_diff + + iwb_map = filter_items_with_no_transactions(iwb_map, float_precision) + + return iwb_map + + +def get_group_by_key(row) -> tuple: + return (row.company, row.item_code, row.warehouse) From a59c205d2ef8aaa4341c472cdb942fd365ac6167 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 25 May 2023 14:18:41 +0530 Subject: [PATCH 11/46] fix(Gross Profit): 'company' column is ambiguous in filter (cherry picked from commit 448712f719219806c9dbd75f6ec4cfd00ddd61b3) --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 224f79babad..2d56b597b0c 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -736,7 +736,7 @@ class GrossProfitGenerator(object): def load_invoice_items(self): conditions = "" if self.filters.company: - conditions += " and company = %(company)s" + conditions += " and `tabSales Invoice`.company = %(company)s" if self.filters.from_date: conditions += " and posting_date >= %(from_date)s" if self.filters.to_date: From 10a1681e87409a7f21d353915558c4ff826b0dd9 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 25 May 2023 16:03:15 +0530 Subject: [PATCH 12/46] chore: typo in stock balance (cherry picked from commit c4e1f927eef44f792bd2f1521480015488206d94) --- erpnext/stock/report/stock_balance/stock_balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 68df918e83e..7c821700df3 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -573,7 +573,7 @@ def filter_items_with_no_transactions( "warehouse", "item_name", "item_group", - "projecy", + "project", "stock_uom", "company", "opening_fifo_queue", From 2b754746494a0f7a5ccc043a260c0ed374fd9b70 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 27 Apr 2023 15:48:02 +0530 Subject: [PATCH 13/46] feat: add field `pi_detail` in `Packing Slip` (cherry picked from commit eca77134ae46d487da9889657f44dfc7b77551c2) --- .../packing_slip_item/packing_slip_item.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json index 4270839bfdb..4a566b6ff2c 100644 --- a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json +++ b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json @@ -20,7 +20,8 @@ "stock_uom", "weight_uom", "page_break", - "dn_detail" + "dn_detail", + "pi_detail" ], "fields": [ { @@ -121,13 +122,21 @@ "fieldtype": "Data", "hidden": 1, "in_list_view": 1, - "label": "DN Detail" + "label": "Delivery Note Item", + "read_only": 1 + }, + { + "fieldname": "pi_detail", + "fieldtype": "Data", + "hidden": 1, + "label": "Delivery Note Packed Item", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-12-14 01:22:00.715935", + "modified": "2023-04-27 15:37:17.023153", "modified_by": "Administrator", "module": "Stock", "name": "Packing Slip Item", @@ -136,5 +145,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 984e32c34a28756d2fabf1b5992287db99cf2f59 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 27 Apr 2023 18:37:32 +0530 Subject: [PATCH 14/46] fix: map `Packed Items` while creating `Packing Slip` (cherry picked from commit 380dd730650a8512c0d7ce485357ed9301ce9a6a) --- .../doctype/delivery_note/delivery_note.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 16caceba09f..dc0642c00ac 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -695,8 +695,27 @@ def make_packing_slip(source_name, target_doc=None): "field_map": { "item_code": "item_code", "item_name": "item_name", + "batch_no": "batch_no", "description": "description", "qty": "qty", + "total_weight": "net_weight", + "stock_uom": "stock_uom", + "weight_uom": "weight_uom", + "name": "dn_detail", + }, + "condition": lambda doc: not frappe.db.exists( + "Product Bundle", {"new_item_code": doc.item_code} + ), + }, + "Packed Item": { + "doctype": "Packing Slip Item", + "field_map": { + "item_code": "item_code", + "item_name": "item_name", + "batch_no": "batch_no", + "description": "description", + "qty": "qty", + "name": "pi_detail", }, }, }, From 0c7efae858ec873535ca6ef113b831509010c65b Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 27 Apr 2023 19:43:37 +0530 Subject: [PATCH 15/46] refactor: `packing_slip.js` (cherry picked from commit b62bf788146a689481598ed3009ec2509272193c) --- .../doctype/packing_slip/packing_slip.js | 195 +++++++++--------- 1 file changed, 102 insertions(+), 93 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js index 40d46852d03..f9cd2bf08c6 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.js +++ b/erpnext/stock/doctype/packing_slip/packing_slip.js @@ -1,113 +1,122 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// License: GNU General Public License v3. See license.txt +// 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); + }, + + validate: (frm) => { + frm.trigger("validate_case_nos"); + frm.trigger("validate_calculate_item_details"); + }, + + onload_post_render: (frm) => { + if(frm.doc.delivery_note && frm.doc.__islocal) { + frm.trigger("get_items"); } - } -} + }, -cur_frm.cscript.onload_post_render = function(doc, cdt, cdn) { - if(doc.delivery_note && doc.__islocal) { - cur_frm.cscript.get_items(doc, cdt, cdn); - } -} + get_items: (frm) => { + return frm.call({ + doc: frm.doc, + method: "get_items", + callback: function(r) { + if(!r.exc) { + frm.refresh(); + } + } + }); + }, -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(); + // To Case No. cannot be less than From Case No. + validate_case_nos: (frm) => { + doc = locals[frm.doc.doctype][frm.doc.name]; + + if(cint(doc.from_case_no) == 0) { + frappe.msgprint(__("The 'From Package No.' field must neither be empty nor it's value less than 1.")); + frappe.validated = false; + } else if(!cint(doc.to_case_no)) { + doc.to_case_no = doc.from_case_no; + refresh_field('to_case_no'); + } else if(cint(doc.to_case_no) < cint(doc.from_case_no)) { + frappe.msgprint(__("'To Case No.' cannot be less than 'From Case No.'")); + frappe.validated = false; } - }); -} + }, -cur_frm.cscript.refresh = function(doc, dt, dn) { - cur_frm.toggle_display("misc_details", doc.amended_from); -} + validate_calculate_item_details: (frm) => { + doc = locals[frm.doc.doctype][frm.doc.name]; + var ps_detail = doc.items || []; -cur_frm.cscript.validate = function(doc, cdt, cdn) { - cur_frm.cscript.validate_case_nos(doc); - cur_frm.cscript.validate_calculate_item_details(doc); -} + frm.events.validate_duplicate_items(doc, ps_detail); + frm.events.calc_net_total_pkg(doc, ps_detail); + }, -// 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; - } -} + // Do not allow duplicate items i.e. items with same item_code + // Also check for 0 qty + validate_duplicate_items: (doc, ps_detail) => { + for(var i=0; i { + var net_weight_pkg = 0; + doc.net_weight_uom = (ps_detail && ps_detail.length) ? ps_detail[0].weight_uom : ''; + doc.gross_weight_uom = doc.net_weight_uom; + + for(var i=0; i Date: Fri, 28 Apr 2023 09:06:50 +0530 Subject: [PATCH 16/46] fix: remove duplicate items validation (cherry picked from commit ee9f97ca7ca052b39f181dfbeca65278f1957b3c) --- .../doctype/packing_slip/packing_slip.js | 48 +++++++------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js index f9cd2bf08c6..fb91930273c 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.js +++ b/erpnext/stock/doctype/packing_slip/packing_slip.js @@ -70,51 +70,37 @@ frappe.ui.form.on("Packing Slip", { }, validate_calculate_item_details: (frm) => { - doc = locals[frm.doc.doctype][frm.doc.name]; - var ps_detail = doc.items || []; - - frm.events.validate_duplicate_items(doc, ps_detail); - frm.events.calc_net_total_pkg(doc, ps_detail); + frm.trigger("validate_items_qty"); + frm.trigger("calc_net_total_pkg"); }, - // Do not allow duplicate items i.e. items with same item_code - // Also check for 0 qty - validate_duplicate_items: (doc, ps_detail) => { - for(var i=0; i { + frm.doc.items.forEach(item => { + if (item.qty <= 0) { + frappe.msgprint(__("Invalid quantity specified for item {0}. Quantity should be greater than 0.", [item.item_code])); frappe.validated = false; } - } + }); }, - // Calculate Net Weight of Package - calc_net_total_pkg: (doc, ps_detail) => { + calc_net_total_pkg: (frm) => { var net_weight_pkg = 0; - doc.net_weight_uom = (ps_detail && ps_detail.length) ? ps_detail[0].weight_uom : ''; - doc.gross_weight_uom = doc.net_weight_uom; + var items = frm.doc.items || []; + frm.doc.net_weight_uom = (items && items.length) ? items[0].weight_uom : ''; + frm.doc.gross_weight_uom = frm.doc.net_weight_uom; - for(var i=0; i { + if(item.weight_uom != frm.doc.net_weight_uom) { frappe.msgprint(__("Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM.")); frappe.validated = false; } net_weight_pkg += flt(item.net_weight) * flt(item.qty); - } + }); - doc.net_weight_pkg = roundNumber(net_weight_pkg, 2); + frm.doc.net_weight_pkg = roundNumber(net_weight_pkg, 2); - if(!flt(doc.gross_weight_pkg)) { - doc.gross_weight_pkg = doc.net_weight_pkg; + if(!flt(frm.doc.gross_weight_pkg)) { + frm.doc.gross_weight_pkg = frm.doc.net_weight_pkg; } refresh_many(['net_weight_pkg', 'net_weight_uom', 'gross_weight_uom', 'gross_weight_pkg']); From 6f0c7cf9a413922551f632d2e66f63a9f9376698 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 10:38:02 +0530 Subject: [PATCH 17/46] fix: don't map items twice * don't explicitly map Delivery Note Item custom fields to Packing Slip Item, get auto-mapped while mapping the doc. * call Packing List `set_missing_values` after mapping the doc. * refactor `get_recommended_case_no`, use `frappe.db.get_value` instead of `frappe.db.sql`. (cherry picked from commit 75fe9dd3ea10342bbbcb29f5fa17a467a3fe7637) --- .../doctype/delivery_note/delivery_note.py | 6 +- .../doctype/packing_slip/packing_slip.js | 18 ----- .../doctype/packing_slip/packing_slip.py | 73 +++++++------------ 3 files changed, 29 insertions(+), 68 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index dc0642c00ac..9449f6e8178 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -681,6 +681,9 @@ 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") + doclist = get_mapped_doc( "Delivery Note", source_name, @@ -698,9 +701,7 @@ def make_packing_slip(source_name, target_doc=None): "batch_no": "batch_no", "description": "description", "qty": "qty", - "total_weight": "net_weight", "stock_uom": "stock_uom", - "weight_uom": "weight_uom", "name": "dn_detail", }, "condition": lambda doc: not frappe.db.exists( @@ -720,6 +721,7 @@ def make_packing_slip(source_name, target_doc=None): }, }, target_doc, + set_missing_values, ) return doclist diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js index fb91930273c..85a611f2b1f 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.js +++ b/erpnext/stock/doctype/packing_slip/packing_slip.js @@ -35,24 +35,6 @@ frappe.ui.form.on("Packing Slip", { frm.trigger("validate_calculate_item_details"); }, - onload_post_render: (frm) => { - if(frm.doc.delivery_note && frm.doc.__islocal) { - frm.trigger("get_items"); - } - }, - - get_items: (frm) => { - return frm.call({ - doc: frm.doc, - method: "get_items", - callback: function(r) { - if(!r.exc) { - frm.refresh(); - } - } - }); - }, - // To Case No. cannot be less than From Case No. validate_case_nos: (frm) => { doc = locals[frm.doc.doctype][frm.doc.name]; diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index e5b9de8789f..415c9e86b56 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -28,6 +28,8 @@ class PackingSlip(Document): validate_uom_is_integer(self, "stock_uom", "qty") validate_uom_is_integer(self, "weight_uom", "net_weight") + self.set_missing_values() + def validate_delivery_note(self): """ Validates if delivery note has status as draft @@ -80,6 +82,20 @@ class PackingSlip(Document): if new_packed_qty > flt(item["qty"]) and no_of_cases: self.recommend_new_qty(item, ps_item_qty, no_of_cases) + def set_missing_values(self): + if not self.from_case_no: + self.from_case_no = self.get_recommended_case_no() + + for item in self.items: + weight_per_unit, weight_uom = frappe.db.get_value( + "Item", item.item_code, ["weight_per_unit", "weight_uom"] + ) + + if weight_per_unit and not item.net_weight: + item.net_weight = weight_per_unit + if weight_uom and not item.weight_uom: + item.weight_uom = weight_uom + def get_details_for_packing(self): """ Returns @@ -141,57 +157,18 @@ class PackingSlip(Document): ) ) - def update_item_details(self): - """ - Fill empty columns in Packing Slip Item - """ - if not self.from_case_no: - self.from_case_no = self.get_recommended_case_no() - - for d in self.get("items"): - res = frappe.db.get_value("Item", d.item_code, ["weight_per_unit", "weight_uom"], as_dict=True) - - if res and len(res) > 0: - d.net_weight = res["weight_per_unit"] - d.weight_uom = res["weight_uom"] - def get_recommended_case_no(self): - """ - Returns the next case no. for a new packing slip for a delivery - note - """ - recommended_case_no = frappe.db.sql( - """SELECT MAX(to_case_no) FROM `tabPacking Slip` - WHERE delivery_note = %s AND docstatus=1""", - self.delivery_note, + """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 - - @frappe.whitelist() - def get_items(self): - self.set("items", []) - - custom_fields = frappe.get_meta("Delivery Note Item").get_custom_fields() - - dn_details = self.get_details_for_packing()[0] - for item in dn_details: - if flt(item.qty) > flt(item.packed_qty): - ch = self.append("items", {}) - ch.item_code = item.item_code - ch.item_name = item.item_name - ch.stock_uom = item.stock_uom - ch.description = item.description - ch.batch_no = item.batch_no - ch.qty = flt(item.qty) - flt(item.packed_qty) - - # copy custom fields - for d in custom_fields: - if item.get(d.fieldname): - ch.set(d.fieldname, item.get(d.fieldname)) - - self.update_item_details() - @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From 509b68404c05a1d52b77925c4f14b45be9bfc47a Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 13:20:47 +0530 Subject: [PATCH 18/46] feat: add field `Packed Qty` in `Delivery Note Item` and `Packed Item` (cherry picked from commit e6fc281acfd91b7107d6bf2c78c61a31b35c3529) # Conflicts: # erpnext/stock/doctype/delivery_note_item/delivery_note_item.json --- .../delivery_note_item/delivery_note_item.json | 15 +++++++++++++++ .../stock/doctype/packed_item/packed_item.json | 13 ++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index e46cab05762..bad66b4aded 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,13 +851,27 @@ "print_hide": 1, "read_only": 1, "report_hide": 1 + }, + { + "default": "0", + "depends_on": "eval: doc.packed_qty", + "fieldname": "packed_qty", + "fieldtype": "Float", + "label": "Packed Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-05-01 21:05:14.175640", +======= + "modified": "2023-04-28 13:14:10.648655", +>>>>>>> e6fc281acf (feat: add field `Packed Qty` in `Delivery Note Item` and `Packed Item`) "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index cb8eb30cb30..c5fb2411c28 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -27,6 +27,7 @@ "actual_qty", "projected_qty", "ordered_qty", + "packed_qty", "column_break_16", "incoming_rate", "picked_qty", @@ -242,13 +243,23 @@ "label": "Picked Qty", "no_copy": 1, "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval: doc.packed_qty", + "fieldname": "packed_qty", + "fieldtype": "Float", + "label": "Packed Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-04-27 05:23:08.683245", + "modified": "2023-04-28 13:16:38.460806", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", From 0bed06284e4ca5adff72fe0f2e1c648536b062b1 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 15:04:41 +0530 Subject: [PATCH 19/46] fix: update `Packed Qty` in DN on submit and cancel of `Packing Slip` (cherry picked from commit 77f1e8ce78742cfbf157b6fe4ab263ee75c20052) --- .../doctype/packing_slip/packing_slip.py | 196 +++++++----------- 1 file changed, 72 insertions(+), 124 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index 415c9e86b56..d1c122d046a 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -4,159 +4,107 @@ import frappe from frappe import _ -from frappe.model import no_value_fields -from frappe.model.document import Document -from frappe.utils import cint, flt +from frappe.utils import cint + +from erpnext.controllers.status_updater import StatusUpdater -class PackingSlip(Document): - def validate(self): - """ - * Validate existence of submitted Delivery Note - * Case nos do not overlap - * Check if packed qty doesn't exceed actual qty of delivery note - - It is necessary to validate case nos before checking quantity - """ - self.validate_delivery_note() - self.validate_items_mandatory() - self.validate_case_nos() - self.validate_qty() +class PackingSlip(StatusUpdater): + def __init__(self, *args, **kwargs) -> None: + super(PackingSlip, self).__init__(*args, **kwargs) + self.status_updater = [ + { + "target_dt": "Delivery Note Item", + "join_field": "dn_detail", + "target_field": "packed_qty", + "target_parent_dt": "Delivery Note", + "target_ref_field": "qty", + "source_dt": "Packing Slip Item", + "source_field": "qty", + }, + { + "target_dt": "Packed Item", + "join_field": "pi_detail", + "target_field": "packed_qty", + "target_parent_dt": "Delivery Note", + "target_ref_field": "qty", + "source_dt": "Packing Slip Item", + "source_field": "qty", + }, + ] + def validate(self) -> None: from erpnext.utilities.transaction_base import validate_uom_is_integer + self.validate_delivery_note() + self.validate_case_nos() + validate_uom_is_integer(self, "stock_uom", "qty") validate_uom_is_integer(self, "weight_uom", "net_weight") self.set_missing_values() + def on_submit(self): + self.update_prevdoc_status() + + def on_cancel(self): + self.update_prevdoc_status() + def validate_delivery_note(self): - """ - Validates if delivery note has status as draft - """ + """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(_("Delivery Note {0} must not be submitted").format(self.delivery_note)) - - 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 validate_case_nos(self): - """ - Validate if case nos overlap. If they do, recommend next case no. - """ - if not cint(self.from_case_no): - frappe.msgprint(_("Please specify a valid 'From Case No.'"), raise_exception=1) - elif not self.to_case_no: - self.to_case_no = self.from_case_no - elif cint(self.from_case_no) > cint(self.to_case_no): - frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), raise_exception=1) - - res = frappe.db.sql( - """SELECT name FROM `tabPacking Slip` - WHERE delivery_note = %(delivery_note)s AND docstatus = 1 AND - ((from_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s) - OR (to_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s) - OR (%(from_case_no)s BETWEEN from_case_no AND to_case_no)) - """, - { - "delivery_note": self.delivery_note, - "from_case_no": self.from_case_no, - "to_case_no": self.to_case_no, - }, - ) - - if res: frappe.throw( - _("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no()) + _("A Packing Slip can only be created for Draft Delivery Note.").format(self.delivery_note) ) - def validate_qty(self): - """Check packed qty across packing slips and delivery note""" - # Get Delivery Note Items, Item Quantity Dict and No. of Cases for this Packing slip - dn_details, ps_item_qty, no_of_cases = self.get_details_for_packing() + def validate_case_nos(self): + """Validate if case nos overlap. If they do, recommend next case no.""" - for item in dn_details: - new_packed_qty = (flt(ps_item_qty[item["item_code"]]) * no_of_cases) + flt(item["packed_qty"]) - if new_packed_qty > flt(item["qty"]) and no_of_cases: - self.recommend_new_qty(item, ps_item_qty, no_of_cases) + if not self.to_case_no: + self.to_case_no = self.from_case_no + elif cint(self.from_case_no) > cint(self.to_case_no): + frappe.throw(_("'To Package No.' cannot be less than 'From Package No.'")) + else: + ps = frappe.qb.DocType("Packing Slip") + res = ( + frappe.qb.from_(ps) + .select( + ps.name, + ) + .where( + (ps.delivery_note == self.delivery_note) + & (ps.docstatus == 1) + & ( + (ps.from_case_no.between(self.from_case_no, self.to_case_no)) + | (ps.to_case_no.between(self.from_case_no, self.to_case_no)) + | ((ps.from_case_no <= self.from_case_no) & (ps.to_case_no >= self.from_case_no)) + ) + ) + ).run() + + if res: + frappe.throw( + _("""Package No(s) already in use. Try from Package No {0}""").format( + self.get_recommended_case_no() + ) + ) def set_missing_values(self): if not self.from_case_no: self.from_case_no = self.get_recommended_case_no() for item in self.items: - weight_per_unit, weight_uom = frappe.db.get_value( - "Item", item.item_code, ["weight_per_unit", "weight_uom"] + stock_uom, weight_per_unit, weight_uom = frappe.db.get_value( + "Item", item.item_code, ["stock_uom", "weight_per_unit", "weight_uom"] ) + item.stock_uom = stock_uom if weight_per_unit and not item.net_weight: item.net_weight = weight_per_unit if weight_uom and not item.weight_uom: item.weight_uom = weight_uom - def get_details_for_packing(self): - """ - Returns - * 'Delivery Note Items' query result as a list of dict - * Item Quantity dict of current packing slip doc - * No. of Cases of this packing slip - """ - - rows = [d.item_code for d in self.get("items")] - - # also pick custom fields from delivery note - custom_fields = ", ".join( - "dni.`{0}`".format(d.fieldname) - for d in frappe.get_meta("Delivery Note Item").get_custom_fields() - if d.fieldtype not in no_value_fields - ) - - if custom_fields: - custom_fields = ", " + custom_fields - - condition = "" - if rows: - condition = " and item_code in (%s)" % (", ".join(["%s"] * len(rows))) - - # gets item code, qty per item code, latest packed qty per item code and stock uom - res = frappe.db.sql( - """select item_code, sum(qty) as qty, - (select sum(psi.qty * (abs(ps.to_case_no - ps.from_case_no) + 1)) - from `tabPacking Slip` ps, `tabPacking Slip Item` psi - where ps.name = psi.parent and ps.docstatus = 1 - and ps.delivery_note = dni.parent and psi.item_code=dni.item_code) as packed_qty, - stock_uom, item_name, description, dni.batch_no {custom_fields} - from `tabDelivery Note Item` dni - where parent=%s {condition} - group by item_code""".format( - condition=condition, custom_fields=custom_fields - ), - tuple([self.delivery_note] + rows), - as_dict=1, - ) - - ps_item_qty = dict([[d.item_code, d.qty] for d in self.get("items")]) - no_of_cases = cint(self.to_case_no) - cint(self.from_case_no) + 1 - - return res, ps_item_qty, no_of_cases - - def recommend_new_qty(self, item, ps_item_qty, no_of_cases): - """ - Recommend a new quantity and raise a validation exception - """ - item["recommended_qty"] = (flt(item["qty"]) - flt(item["packed_qty"])) / no_of_cases - item["specified_qty"] = flt(ps_item_qty[item["item_code"]]) - if not item["packed_qty"]: - item["packed_qty"] = 0 - - frappe.throw( - _("Quantity for Item {0} must be less than {1}").format( - item.get("item_code"), item.get("recommended_qty") - ) - ) - def get_recommended_case_no(self): """Returns the next case no. for a new packing slip for a delivery note""" From 19713f9f6f7a4871f834342ae39948dfa675267e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 15:05:28 +0530 Subject: [PATCH 20/46] chore: enable `no_copy` for `dn_detail` and `pi_detail` in Packing Slip Item (cherry picked from commit 0add90e7ec8b9bdad8500de381cb98990e0c513a) --- .../stock/doctype/packing_slip_item/packing_slip_item.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json index 4a566b6ff2c..4bd90355acb 100644 --- a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json +++ b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json @@ -123,6 +123,7 @@ "hidden": 1, "in_list_view": 1, "label": "Delivery Note Item", + "no_copy": 1, "read_only": 1 }, { @@ -130,13 +131,14 @@ "fieldtype": "Data", "hidden": 1, "label": "Delivery Note Packed Item", + "no_copy": 1, "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-04-27 15:37:17.023153", + "modified": "2023-04-28 15:00:14.079306", "modified_by": "Administrator", "module": "Stock", "name": "Packing Slip Item", From 5345ebe2426d518f8de7d9463fec47fc020bace2 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 15:24:23 +0530 Subject: [PATCH 21/46] fix: Packing Slip Item Qty (cherry picked from commit 372bce45675ac0232219097cc191bc662962a49f) --- erpnext/stock/doctype/delivery_note/delivery_note.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 9449f6e8178..6fbc8870eef 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -684,6 +684,9 @@ def make_packing_slip(source_name, target_doc=None): def set_missing_values(source, target): target.run_method("set_missing_values") + def update_item(obj, target, source_parent): + target.qty = flt(obj.qty) - flt(obj.packed_qty) + doclist = get_mapped_doc( "Delivery Note", source_name, @@ -704,8 +707,10 @@ def make_packing_slip(source_name, target_doc=None): "stock_uom": "stock_uom", "name": "dn_detail", }, - "condition": lambda doc: not frappe.db.exists( - "Product Bundle", {"new_item_code": doc.item_code} + "postprocess": update_item, + "condition": lambda doc: ( + not frappe.db.exists("Product Bundle", {"new_item_code": doc.item_code}) + and (doc.qty - doc.packed_qty) > 0 ), }, "Packed Item": { @@ -718,6 +723,8 @@ def make_packing_slip(source_name, target_doc=None): "qty": "qty", "name": "pi_detail", }, + "postprocess": update_item, + "condition": lambda doc: ((doc.qty - doc.packed_qty) > 0), }, }, target_doc, From b4e481a39003ea2acb3f9f848808a63a45dc8444 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 15:40:26 +0530 Subject: [PATCH 22/46] fix: make DN item reference mandatory for Packing Slip Item (cherry picked from commit 9e5b102768395601812fa4e09980a3fe0d6289b4) --- erpnext/stock/doctype/packing_slip/packing_slip.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index d1c122d046a..b356a205963 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -38,6 +38,7 @@ class PackingSlip(StatusUpdater): self.validate_delivery_note() self.validate_case_nos() + self.validate_mandatory() validate_uom_is_integer(self, "stock_uom", "qty") validate_uom_is_integer(self, "weight_uom", "net_weight") @@ -90,6 +91,13 @@ class PackingSlip(StatusUpdater): ) ) + def validate_mandatory(self): + for item in self.items: + if not item.dn_detail and not item.pi_detail: + frappe.throw( + _("Row {0}: Either Delivery Note Item or Packed Item reference is mandatory").format(item.idx) + ) + def set_missing_values(self): if not self.from_case_no: self.from_case_no = self.get_recommended_case_no() From cc7e267c350116d598a1f0e6cb8223ad7e1561e9 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 17:05:51 +0530 Subject: [PATCH 23/46] fix: validate Packing Slip Item Qty with DN Items (cherry picked from commit 90701c7ae9249918fa81c7605baa80b1eaf51c09) --- .../doctype/packing_slip/packing_slip.py | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index b356a205963..c5b928bd5c1 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -38,7 +38,7 @@ class PackingSlip(StatusUpdater): self.validate_delivery_note() self.validate_case_nos() - self.validate_mandatory() + self.validate_items() validate_uom_is_integer(self, "stock_uom", "qty") validate_uom_is_integer(self, "weight_uom", "net_weight") @@ -91,13 +91,38 @@ class PackingSlip(StatusUpdater): ) ) - def validate_mandatory(self): + def validate_items(self): for item in self.items: if not item.dn_detail and not item.pi_detail: frappe.throw( _("Row {0}: Either Delivery Note Item or Packed Item reference is mandatory").format(item.idx) ) + remaining_qty = frappe.db.get_value( + "Delivery Note Item" if item.dn_detail else "Packed Item", + {"name": item.dn_detail or item.pi_detail, "docstatus": 0}, + ["sum(qty - packed_qty)"], + ) + + if remaining_qty is None: + frappe.throw( + _("Row {0}: Please provide a valid Delivery Note Item or Packed Item reference.").format( + item.idx + ) + ) + elif remaining_qty <= 0: + frappe.throw( + _("Row {0}: Packing Slip is already created for Item {1}.").format( + item.idx, frappe.bold(item.item_code) + ) + ) + elif item.qty > remaining_qty: + frappe.throw( + _("Row {0}: Qty cannot be greater than {1} for the Item {2}.").format( + item.idx, frappe.bold(remaining_qty), frappe.bold(item.item_code) + ) + ) + def set_missing_values(self): if not self.from_case_no: self.from_case_no = self.get_recommended_case_no() From 1412c63158d8b65f9884a41046899bf296cefcc9 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 17:47:36 +0530 Subject: [PATCH 24/46] refactor: move `js` validations to `py` (cherry picked from commit 269cc96c412b0dacf5e1f0fcc3e9ead2f0cf3e95) --- .../doctype/packing_slip/packing_slip.js | 58 ------------------- .../doctype/packing_slip/packing_slip.py | 38 ++++++++++-- 2 files changed, 34 insertions(+), 62 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js index 85a611f2b1f..ae3d9bae931 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.js +++ b/erpnext/stock/doctype/packing_slip/packing_slip.js @@ -28,63 +28,5 @@ frappe.ui.form.on("Packing Slip", { refresh: (frm) => { frm.toggle_display("misc_details", frm.doc.amended_from); - }, - - validate: (frm) => { - frm.trigger("validate_case_nos"); - frm.trigger("validate_calculate_item_details"); - }, - - // To Case No. cannot be less than From Case No. - validate_case_nos: (frm) => { - doc = locals[frm.doc.doctype][frm.doc.name]; - - if(cint(doc.from_case_no) == 0) { - frappe.msgprint(__("The 'From Package No.' field must neither be empty nor it's value less than 1.")); - frappe.validated = false; - } else if(!cint(doc.to_case_no)) { - doc.to_case_no = doc.from_case_no; - refresh_field('to_case_no'); - } else if(cint(doc.to_case_no) < cint(doc.from_case_no)) { - frappe.msgprint(__("'To Case No.' cannot be less than 'From Case No.'")); - frappe.validated = false; - } - }, - - validate_calculate_item_details: (frm) => { - frm.trigger("validate_items_qty"); - frm.trigger("calc_net_total_pkg"); - }, - - validate_items_qty: (frm) => { - frm.doc.items.forEach(item => { - if (item.qty <= 0) { - frappe.msgprint(__("Invalid quantity specified for item {0}. Quantity should be greater than 0.", [item.item_code])); - frappe.validated = false; - } - }); - }, - - calc_net_total_pkg: (frm) => { - var net_weight_pkg = 0; - var items = frm.doc.items || []; - frm.doc.net_weight_uom = (items && items.length) ? items[0].weight_uom : ''; - frm.doc.gross_weight_uom = frm.doc.net_weight_uom; - - items.forEach(item => { - if(item.weight_uom != frm.doc.net_weight_uom) { - frappe.msgprint(__("Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM.")); - frappe.validated = false; - } - net_weight_pkg += flt(item.net_weight) * flt(item.qty); - }); - - frm.doc.net_weight_pkg = roundNumber(net_weight_pkg, 2); - - if(!flt(frm.doc.gross_weight_pkg)) { - frm.doc.gross_weight_pkg = frm.doc.net_weight_pkg; - } - - refresh_many(['net_weight_pkg', 'net_weight_uom', 'gross_weight_uom', 'gross_weight_pkg']); } }); diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index c5b928bd5c1..6ea5938917a 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.utils import cint +from frappe.utils import cint, flt from erpnext.controllers.status_updater import StatusUpdater @@ -44,6 +44,7 @@ class PackingSlip(StatusUpdater): validate_uom_is_integer(self, "weight_uom", "net_weight") self.set_missing_values() + self.calculate_net_total_pkg() def on_submit(self): self.update_prevdoc_status() @@ -62,9 +63,13 @@ class PackingSlip(StatusUpdater): def validate_case_nos(self): """Validate if case nos overlap. If they do, recommend next case no.""" - if not self.to_case_no: + if cint(self.from_case_no) <= 0: + frappe.throw( + _("The 'From Package No.' field must neither be empty nor it's value less than 1.") + ) + elif not self.to_case_no: self.to_case_no = self.from_case_no - elif cint(self.from_case_no) > cint(self.to_case_no): + elif cint(self.to_case_no) < cint(self.from_case_no): frappe.throw(_("'To Package No.' cannot be less than 'From Package No.'")) else: ps = frappe.qb.DocType("Packing Slip") @@ -93,9 +98,14 @@ class PackingSlip(StatusUpdater): def validate_items(self): for item in self.items: + if item.qty <= 0: + frappe.throw(_("Row {0}: Qty must be greater than 0.").format(item.idx)) + if not item.dn_detail and not item.pi_detail: frappe.throw( - _("Row {0}: Either Delivery Note Item or Packed Item reference is mandatory").format(item.idx) + _("Row {0}: Either Delivery Note Item or Packed Item reference is mandatory.").format( + item.idx + ) ) remaining_qty = frappe.db.get_value( @@ -150,6 +160,26 @@ class PackingSlip(StatusUpdater): + 1 ) + def calculate_net_total_pkg(self): + self.net_weight_uom = self.items[0].weight_uom if self.items else None + self.gross_weight_uom = self.net_weight_uom + + net_weight_pkg = 0 + for item in self.items: + if item.weight_uom != self.net_weight_uom: + frappe.throw( + _( + "Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM." + ) + ) + + net_weight_pkg += flt(item.net_weight) * flt(item.qty) + + self.net_weight_pkg = round(net_weight_pkg, 2) + + if not flt(self.gross_weight_pkg): + self.gross_weight_pkg = self.net_weight_pkg + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From b96aa75ded13def19a3f587884d84a30b6786809 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 17:59:34 +0530 Subject: [PATCH 25/46] fix(ux): get items on selecting DN in Packing Slip (cherry picked from commit e75aa4e291694c7722ef568be162916ccf3ebcc6) --- .../doctype/packing_slip/packing_slip.js | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js index ae3d9bae931..95e5ea309f8 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.js +++ b/erpnext/stock/doctype/packing_slip/packing_slip.js @@ -1,7 +1,7 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on("Packing Slip", { +frappe.ui.form.on('Packing Slip', { setup: (frm) => { frm.set_query('delivery_note', () => { return { @@ -13,7 +13,7 @@ frappe.ui.form.on("Packing Slip", { frm.set_query('item_code', 'items', (doc, cdt, cdn) => { if (!doc.delivery_note) { - frappe.throw(__("Please select a Delivery Note")); + frappe.throw(__('Please select a Delivery Note')); } else { let d = locals[cdt][cdn]; return { @@ -27,6 +27,20 @@ frappe.ui.form.on("Packing Slip", { }, refresh: (frm) => { - frm.toggle_display("misc_details", frm.doc.amended_from); - } + frm.toggle_display('misc_details', frm.doc.amended_from); + }, + + delivery_note: (frm) => { + frm.set_value('items', null); + + if (frm.doc.delivery_note) { + erpnext.utils.map_current_doc({ + method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip', + source_name: frm.doc.delivery_note, + target_doc: frm, + freeze: true, + freeze_message: __('Creating Packing Slip ...'), + }); + } + }, }); From 4017342c151e6f0352c82a1c88ad6ef4c06c71cd Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 18:02:24 +0530 Subject: [PATCH 26/46] fix(ux): remove `Get Items` button from `Packing Slip` (cherry picked from commit 8d1bccada467338f870226d9aba79a9bf7a74c90) --- .../doctype/packing_slip/packing_slip.json | 522 +++++++++--------- 1 file changed, 260 insertions(+), 262 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.json b/erpnext/stock/doctype/packing_slip/packing_slip.json index ec8d57c9652..86ed794c620 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.json +++ b/erpnext/stock/doctype/packing_slip/packing_slip.json @@ -1,264 +1,262 @@ { - "allow_import": 1, - "autoname": "MAT-PAC-.YYYY.-.#####", - "creation": "2013-04-11 15:32:24", - "description": "Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.", - "doctype": "DocType", - "document_type": "Document", - "engine": "InnoDB", - "field_order": [ - "packing_slip_details", - "column_break0", - "delivery_note", - "column_break1", - "naming_series", - "section_break0", - "column_break2", - "from_case_no", - "column_break3", - "to_case_no", - "package_item_details", - "get_items", - "items", - "package_weight_details", - "net_weight_pkg", - "net_weight_uom", - "column_break4", - "gross_weight_pkg", - "gross_weight_uom", - "letter_head_details", - "letter_head", - "misc_details", - "amended_from" - ], - "fields": [ - { - "fieldname": "packing_slip_details", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break0", - "fieldtype": "Column Break" - }, - { - "description": "Indicates that the package is a part of this delivery (Only Draft)", - "fieldname": "delivery_note", - "fieldtype": "Link", - "in_global_search": 1, - "in_list_view": 1, - "label": "Delivery Note", - "options": "Delivery Note", - "reqd": 1 - }, - { - "fieldname": "column_break1", - "fieldtype": "Column Break" - }, - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "options": "MAT-PAC-.YYYY.-", - "print_hide": 1, - "reqd": 1, - "set_only_once": 1 - }, - { - "fieldname": "section_break0", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break2", - "fieldtype": "Column Break" - }, - { - "description": "Identification of the package for the delivery (for print)", - "fieldname": "from_case_no", - "fieldtype": "Int", - "in_list_view": 1, - "label": "From Package No.", - "no_copy": 1, - "reqd": 1, - "width": "50px" - }, - { - "fieldname": "column_break3", - "fieldtype": "Column Break" - }, - { - "description": "If more than one package of the same type (for print)", - "fieldname": "to_case_no", - "fieldtype": "Int", - "in_list_view": 1, - "label": "To Package No.", - "no_copy": 1, - "width": "50px" - }, - { - "fieldname": "package_item_details", - "fieldtype": "Section Break" - }, - { - "fieldname": "get_items", - "fieldtype": "Button", - "label": "Get Items" - }, - { - "fieldname": "items", - "fieldtype": "Table", - "label": "Items", - "options": "Packing Slip Item", - "reqd": 1 - }, - { - "fieldname": "package_weight_details", - "fieldtype": "Section Break", - "label": "Package Weight Details" - }, - { - "description": "The net weight of this package. (calculated automatically as sum of net weight of items)", - "fieldname": "net_weight_pkg", - "fieldtype": "Float", - "label": "Net Weight", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "net_weight_uom", - "fieldtype": "Link", - "label": "Net Weight UOM", - "no_copy": 1, - "options": "UOM", - "read_only": 1 - }, - { - "fieldname": "column_break4", - "fieldtype": "Column Break" - }, - { - "description": "The gross weight of the package. Usually net weight + packaging material weight. (for print)", - "fieldname": "gross_weight_pkg", - "fieldtype": "Float", - "label": "Gross Weight", - "no_copy": 1 - }, - { - "fieldname": "gross_weight_uom", - "fieldtype": "Link", - "label": "Gross Weight UOM", - "no_copy": 1, - "options": "UOM" - }, - { - "fieldname": "letter_head_details", - "fieldtype": "Section Break", - "label": "Letter Head" - }, - { - "allow_on_submit": 1, - "fieldname": "letter_head", - "fieldtype": "Link", - "label": "Letter Head", - "options": "Letter Head", - "print_hide": 1 - }, - { - "fieldname": "misc_details", - "fieldtype": "Section Break" - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Amended From", - "no_copy": 1, - "options": "Packing Slip", - "print_hide": 1, - "read_only": 1 - } - ], - "icon": "fa fa-suitcase", - "idx": 1, - "is_submittable": 1, - "modified": "2019-09-09 04:45:08.082862", - "modified_by": "Administrator", - "module": "Stock", - "name": "Packing Slip", - "owner": "Administrator", - "permissions": [ - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock User", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Item Manager", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock Manager", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "share": 1, - "submit": 1, - "write": 1 - } - ], - "search_fields": "delivery_note", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC" + "actions": [], + "allow_import": 1, + "autoname": "MAT-PAC-.YYYY.-.#####", + "creation": "2013-04-11 15:32:24", + "description": "Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "packing_slip_details", + "column_break0", + "delivery_note", + "column_break1", + "naming_series", + "section_break0", + "column_break2", + "from_case_no", + "column_break3", + "to_case_no", + "package_item_details", + "items", + "package_weight_details", + "net_weight_pkg", + "net_weight_uom", + "column_break4", + "gross_weight_pkg", + "gross_weight_uom", + "letter_head_details", + "letter_head", + "misc_details", + "amended_from" + ], + "fields": [ + { + "fieldname": "packing_slip_details", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break0", + "fieldtype": "Column Break" + }, + { + "description": "Indicates that the package is a part of this delivery (Only Draft)", + "fieldname": "delivery_note", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Delivery Note", + "options": "Delivery Note", + "reqd": 1 + }, + { + "fieldname": "column_break1", + "fieldtype": "Column Break" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "MAT-PAC-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "section_break0", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break2", + "fieldtype": "Column Break" + }, + { + "description": "Identification of the package for the delivery (for print)", + "fieldname": "from_case_no", + "fieldtype": "Int", + "in_list_view": 1, + "label": "From Package No.", + "no_copy": 1, + "reqd": 1, + "width": "50px" + }, + { + "fieldname": "column_break3", + "fieldtype": "Column Break" + }, + { + "description": "If more than one package of the same type (for print)", + "fieldname": "to_case_no", + "fieldtype": "Int", + "in_list_view": 1, + "label": "To Package No.", + "no_copy": 1, + "width": "50px" + }, + { + "fieldname": "package_item_details", + "fieldtype": "Section Break" + }, + { + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Packing Slip Item", + "reqd": 1 + }, + { + "fieldname": "package_weight_details", + "fieldtype": "Section Break", + "label": "Package Weight Details" + }, + { + "description": "The net weight of this package. (calculated automatically as sum of net weight of items)", + "fieldname": "net_weight_pkg", + "fieldtype": "Float", + "label": "Net Weight", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "net_weight_uom", + "fieldtype": "Link", + "label": "Net Weight UOM", + "no_copy": 1, + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "column_break4", + "fieldtype": "Column Break" + }, + { + "description": "The gross weight of the package. Usually net weight + packaging material weight. (for print)", + "fieldname": "gross_weight_pkg", + "fieldtype": "Float", + "label": "Gross Weight", + "no_copy": 1 + }, + { + "fieldname": "gross_weight_uom", + "fieldtype": "Link", + "label": "Gross Weight UOM", + "no_copy": 1, + "options": "UOM" + }, + { + "fieldname": "letter_head_details", + "fieldtype": "Section Break", + "label": "Letter Head" + }, + { + "allow_on_submit": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head", + "print_hide": 1 + }, + { + "fieldname": "misc_details", + "fieldtype": "Section Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "options": "Packing Slip", + "print_hide": 1, + "read_only": 1 } + ], + "icon": "fa fa-suitcase", + "idx": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-04-28 18:01:37.341619", + "modified_by": "Administrator", + "module": "Stock", + "name": "Packing Slip", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Item Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "search_fields": "delivery_note", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file From 9854c84ad8717dbe93ffb7745191f9220c3486e5 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 18:28:28 +0530 Subject: [PATCH 27/46] fix(ux): don't show `Create > Packing Slip` button if items are already packed (cherry picked from commit da00fc0f16bd980243548cd1560078aa1891a323) --- .../doctype/delivery_note/delivery_note.js | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index ae56645b730..08419c26ed6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -185,11 +185,30 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends erpn } if(doc.docstatus==0 && !doc.__islocal) { - this.frm.add_custom_button(__('Packing Slip'), function() { - frappe.model.open_mapped_doc({ - method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip", - frm: me.frm - }) }, __('Create')); + var remaining_qty = 0; + + doc.items.forEach(item => { + frappe.db.exists("Product Bundle", item.item_code).then(exists => { + if (!exists) { + remaining_qty += (item.qty - item.packed_qty); + } + }); + }); + + if (!remaining_qty) { + doc.packed_items.forEach(item => { + remaining_qty += (item.qty - item.packed_qty); + }); + } + + if (remaining_qty > 0) { + this.frm.add_custom_button(__('Packing Slip'), function() { + frappe.model.open_mapped_doc({ + method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip", + frm: me.frm + }) }, __('Create') + ); + } } if (!doc.__islocal && doc.docstatus==1) { From 1ead0a3fefe26201afd85e337dccba74eb1c0b79 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 21:51:15 +0530 Subject: [PATCH 28/46] test: add test cases for `Packing Slip` (cherry picked from commit 7742c592c5528002813e0247240cb0cb3f67985a) --- .../doctype/packing_slip/test_packing_slip.py | 106 +++++++++++++++++- 1 file changed, 103 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py index bc405b20995..e8873e38212 100644 --- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py @@ -3,9 +3,109 @@ import unittest -# test_records = frappe.get_test_records('Packing Slip') +import frappe from frappe.tests.utils import FrappeTestCase +from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle +from erpnext.stock.doctype.delivery_note.delivery_note import make_packing_slip +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.item.test_item import make_item -class TestPackingSlip(unittest.TestCase): - pass + +class TestPackingSlip(FrappeTestCase): + def test_packing_slip(self): + # Step - 1: Create a Product Bundle + items = create_items() + make_product_bundle(items[0], items[1:], 5) + + # Step - 2: Create a Delivery Note (Draft) with Product Bundle + dn = create_delivery_note( + item_code=items[0], + qty=2, + do_not_save=True, + ) + dn.append( + "items", + { + "item_code": items[1], + "warehouse": "_Test Warehouse - _TC", + "qty": 10, + }, + ) + dn.save() + + # Step - 3: Make a Packing Slip from Delivery Note for 4 Qty + ps1 = make_packing_slip(dn.name) + for item in ps1.items: + item.qty = 4 + ps1.save() + ps1.submit() + + # Test - 1: `Packed Qty` should be updated to 4 in Delivery Note Items and Packed Items. + dn.load_from_db() + for item in dn.items: + if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): + self.assertEqual(item.packed_qty, 4) + + for item in dn.packed_items: + self.assertEqual(item.packed_qty, 4) + + # Step - 4: Make another Packing Slip from Delivery Note for 6 Qty + ps2 = make_packing_slip(dn.name) + ps2.save() + ps2.submit() + + # Test - 2: `Packed Qty` should be updated to 10 in Delivery Note Items and Packed Items. + dn.load_from_db() + for item in dn.items: + if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): + self.assertEqual(item.packed_qty, 10) + + for item in dn.packed_items: + self.assertEqual(item.packed_qty, 10) + + # Step - 5: Cancel Packing Slip [1] + ps1.cancel() + + # Test - 3: `Packed Qty` should be updated to 4 in Delivery Note Items and Packed Items. + dn.load_from_db() + for item in dn.items: + if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): + self.assertEqual(item.packed_qty, 6) + + for item in dn.packed_items: + self.assertEqual(item.packed_qty, 6) + + # Step - 6: Cancel Packing Slip [2] + ps2.cancel() + + # Test - 4: `Packed Qty` should be updated to 0 in Delivery Note Items and Packed Items. + dn.load_from_db() + for item in dn.items: + if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): + self.assertEqual(item.packed_qty, 0) + + for item in dn.packed_items: + self.assertEqual(item.packed_qty, 0) + + # Step - 7: Make Packing Slip for more Qty than Delivery Note + ps3 = make_packing_slip(dn.name) + ps3.items[0].qty = 20 + + # Test - 5: Should throw an ValidationError, as Packing Slip Qty is more than Delivery Note Qty + self.assertRaises(frappe.exceptions.ValidationError, ps3.save) + + +def create_items(): + items_properties = [ + {"is_stock_item": 0}, + {"is_stock_item": 1}, + {"is_stock_item": 1}, + {"is_stock_item": 1}, + ] + + items = [] + for properties in items_properties: + items.append(make_item(properties=properties).name) + + return items From 7105648468b04e99bbfcb4235bbdf917c611bd95 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 22:47:27 +0530 Subject: [PATCH 29/46] refactor(minor): use `set_onload` to get unpacked items details (cherry picked from commit b0eb9ea7bde7329e41012c468ff2bc76c9330cf3) --- .../doctype/delivery_note/delivery_note.js | 18 +------------ .../doctype/delivery_note/delivery_note.py | 26 ++++++++++++++++--- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 08419c26ed6..77545e0e1ad 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -185,23 +185,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends erpn } if(doc.docstatus==0 && !doc.__islocal) { - var remaining_qty = 0; - - doc.items.forEach(item => { - frappe.db.exists("Product Bundle", item.item_code).then(exists => { - if (!exists) { - remaining_qty += (item.qty - item.packed_qty); - } - }); - }); - - if (!remaining_qty) { - doc.packed_items.forEach(item => { - remaining_qty += (item.qty - item.packed_qty); - }); - } - - if (remaining_qty > 0) { + if (doc.__onload && doc.__onload.has_unpacked_items) { this.frm.add_custom_button(__('Packing Slip'), function() { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 6fbc8870eef..a8a8c3bb2e2 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -86,6 +86,10 @@ class DeliveryNote(SellingController): ] ) + def onload(self): + if self.docstatus == 0: + self.set_onload("has_unpacked_items", self.has_unpacked_items()) + def before_print(self, settings=None): def toggle_print_hide(meta, fieldname): df = meta.get_field(fieldname) @@ -390,6 +394,20 @@ class DeliveryNote(SellingController): ) ) + def has_unpacked_items(self): + for item in self.items: + if ( + not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}) + and item.packed_qty < item.qty + ): + return True + + for item in self.packed_items: + if item.packed_qty < item.qty: + return True + + return False + def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum @@ -708,9 +726,9 @@ def make_packing_slip(source_name, target_doc=None): "name": "dn_detail", }, "postprocess": update_item, - "condition": lambda doc: ( - not frappe.db.exists("Product Bundle", {"new_item_code": doc.item_code}) - and (doc.qty - doc.packed_qty) > 0 + "condition": lambda item: ( + not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}) + and item.packed_qty < item.qty ), }, "Packed Item": { @@ -724,7 +742,7 @@ def make_packing_slip(source_name, target_doc=None): "name": "pi_detail", }, "postprocess": update_item, - "condition": lambda doc: ((doc.qty - doc.packed_qty) > 0), + "condition": lambda item: (item.packed_qty < item.qty), }, }, target_doc, From dd7e5e019f403ce723f94010774c3f2a5f258a1e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Apr 2023 23:03:33 +0530 Subject: [PATCH 30/46] refactor: validate_packed_qty() (cherry picked from commit 699532647d826ef0a561d57d7aee817c7e0380e4) --- .../doctype/delivery_note/delivery_note.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index a8a8c3bb2e2..1183281d52f 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -303,20 +303,13 @@ 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) + """Validate that if packed qty exists, it should be equal to qty""" + + for item in self.items + self.packed_items: + if item.packed_qty and item.packed_qty != item.qty: + frappe.throw( + _("Row {0}: Packed Qty must be equal to {1} Qty.").format(item.idx, frappe.bold(item.doctype)) ) - has_error = True - if has_error: - raise frappe.ValidationError def update_pick_list_status(self): from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status From 70857bf1577b2e1734cd96c1d19dfbea458cd61c Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 29 Apr 2023 07:25:35 +0530 Subject: [PATCH 31/46] test: add test case for packed qty validation on DN submit (cherry picked from commit ba61292dfc0d767ab8c88bd05cadff2ad7ed1237) --- .../doctype/packing_slip/test_packing_slip.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py index e8873e38212..96da23db4a8 100644 --- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py @@ -95,13 +95,22 @@ class TestPackingSlip(FrappeTestCase): # Test - 5: Should throw an ValidationError, as Packing Slip Qty is more than Delivery Note Qty self.assertRaises(frappe.exceptions.ValidationError, ps3.save) + # Step - 8: Make Packing Slip for less Qty than Delivery Note + ps4 = make_packing_slip(dn.name) + ps4.items[0].qty = 5 + ps4.save() + ps4.submit() + + # Test - 6: Delivery Note should throw a ValidationError on Submit, as Packed Qty and Delivery Note Qty are not the same + dn.load_from_db() + self.assertRaises(frappe.exceptions.ValidationError, dn.submit) + def create_items(): items_properties = [ {"is_stock_item": 0}, - {"is_stock_item": 1}, - {"is_stock_item": 1}, - {"is_stock_item": 1}, + {"is_stock_item": 1, "stock_uom": "Nos"}, + {"is_stock_item": 1, "stock_uom": "Box"}, ] items = [] From ca6607e800346b9c284cee7f394511a95dc85056 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sun, 30 Apr 2023 08:04:02 +0530 Subject: [PATCH 32/46] refactor: use `get_product_bundle_list()` to get all product bundle at once (cherry picked from commit bbcb65894b4d957e45b03bb7d170a71e021c967c) --- .../doctype/delivery_note/delivery_note.py | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 1183281d52f..3a056500b54 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -305,11 +305,19 @@ class DeliveryNote(SellingController): def validate_packed_qty(self): """Validate that if packed qty exists, it should be equal to qty""" - for item in self.items + self.packed_items: - if item.packed_qty and item.packed_qty != item.qty: - frappe.throw( - _("Row {0}: Packed Qty must be equal to {1} Qty.").format(item.idx, frappe.bold(item.doctype)) - ) + if frappe.db.exists("Packing Slip", {"docstatus": 1, "delivery_note": self.name}): + product_bundle_list = self.get_product_bundle_list() + for item in self.items + self.packed_items: + if ( + item.item_code not in product_bundle_list + and flt(item.packed_qty) + and flt(item.packed_qty) != flt(item.qty) + ): + frappe.throw( + _("Row {0}: Packed Qty must be equal to {1} Qty.").format( + item.idx, frappe.bold(item.doctype) + ) + ) def update_pick_list_status(self): from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status @@ -388,19 +396,22 @@ class DeliveryNote(SellingController): ) def has_unpacked_items(self): - for item in self.items: - if ( - not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}) - and item.packed_qty < item.qty - ): - return True + product_bundle_list = self.get_product_bundle_list() - for item in self.packed_items: - if item.packed_qty < item.qty: + for item in self.items + self.packed_items: + if item.item_code not in product_bundle_list and flt(item.packed_qty) < flt(item.qty): return True return False + def get_product_bundle_list(self): + items_list = [item.item_code for item in self.items] + return frappe.db.get_all( + "Product Bundle", + filters={"new_item_code": ["in", items_list]}, + pluck="name", + ) + def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum @@ -721,7 +732,7 @@ def make_packing_slip(source_name, target_doc=None): "postprocess": update_item, "condition": lambda item: ( not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}) - and item.packed_qty < item.qty + and flt(item.packed_qty) < flt(item.qty) ), }, "Packed Item": { @@ -735,7 +746,7 @@ def make_packing_slip(source_name, target_doc=None): "name": "pi_detail", }, "postprocess": update_item, - "condition": lambda item: (item.packed_qty < item.qty), + "condition": lambda item: (flt(item.packed_qty) < flt(item.qty)), }, }, target_doc, From b3da2f7c261fc4d465dc63e3924a13ef5a9d137c Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 25 May 2023 14:39:36 +0530 Subject: [PATCH 33/46] fix(patch): add patch to set `packed_qty` in draft DN (cherry picked from commit 196e18187f3c33bb9f9e272a8e2cdb14002c5a9c) --- erpnext/patches.txt | 1 + .../set_packed_qty_in_draft_delivery_notes.py | 60 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f010f1a1be6..fbfb7d2094e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -331,3 +331,4 @@ execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_h # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.update_company_in_ldc +erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes diff --git a/erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py b/erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py new file mode 100644 index 00000000000..1aeb2e6cc3d --- /dev/null +++ b/erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py @@ -0,0 +1,60 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.query_builder.functions import Sum + + +def execute(): + ps = frappe.qb.DocType("Packing Slip") + dn = frappe.qb.DocType("Delivery Note") + ps_item = frappe.qb.DocType("Packing Slip Item") + + ps_details = ( + frappe.qb.from_(ps) + .join(ps_item) + .on(ps.name == ps_item.parent) + .join(dn) + .on(ps.delivery_note == dn.name) + .select( + dn.name.as_("delivery_note"), + ps_item.item_code.as_("item_code"), + Sum(ps_item.qty).as_("packed_qty"), + ) + .where((ps.docstatus == 1) & (dn.docstatus == 0)) + .groupby(dn.name, ps_item.item_code) + ).run(as_dict=True) + + if ps_details: + dn_list = set() + item_code_list = set() + for ps_detail in ps_details: + dn_list.add(ps_detail.delivery_note) + item_code_list.add(ps_detail.item_code) + + dn_item = frappe.qb.DocType("Delivery Note Item") + dn_item_query = ( + frappe.qb.from_(dn_item) + .select( + dn.parent.as_("delivery_note"), + dn_item.name, + dn_item.item_code, + dn_item.qty, + ) + .where((dn_item.parent.isin(dn_list)) & (dn_item.item_code.isin(item_code_list))) + ) + + dn_details = frappe._dict() + for r in dn_item_query.run(as_dict=True): + dn_details.setdefault((r.delivery_note, r.item_code), frappe._dict()).setdefault(r.name, r.qty) + + for ps_detail in ps_details: + dn_items = dn_details.get((ps_detail.delivery_note, ps_detail.item_code)) + + if dn_items: + remaining_qty = ps_detail.packed_qty + for name, qty in dn_items.items(): + if remaining_qty > 0: + row_packed_qty = min(qty, remaining_qty) + frappe.db.set_value("Delivery Note Item", name, "packed_qty", row_packed_qty) + remaining_qty -= row_packed_qty From f5b2b807077cba5774e8c26c872fe4d80c99ae96 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 26 May 2023 00:26:14 +0530 Subject: [PATCH 34/46] chore: `conflict` --- .../doctype/delivery_note_item/delivery_note_item.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index bad66b4aded..b97e42c2468 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -867,11 +867,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD "modified": "2023-05-01 21:05:14.175640", -======= - "modified": "2023-04-28 13:14:10.648655", ->>>>>>> e6fc281acf (feat: add field `Packed Qty` in `Delivery Note Item` and `Packed Item`) "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", @@ -881,4 +877,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} From 5a9452f4a30a6ed7523a46884824a04ac8f2616b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 25 May 2023 23:41:56 +0530 Subject: [PATCH 35/46] fix: incorrect available quantity in BIN (cherry picked from commit 9e5e2de5d50c7b8eebbb73ac348d4990d9eab767) --- erpnext/stock/stock_ledger.py | 15 +++++++++++++-- erpnext/stock/utils.py | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 6106809273f..9f81a8cd3f8 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -443,12 +443,11 @@ class update_entries_after(object): i += 1 self.process_sle(sle) + self.update_bin_data(sle) if sle.dependant_sle_voucher_detail_no: entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle) - self.update_bin() - if self.exceptions: self.raise_exceptions() @@ -1065,6 +1064,18 @@ class update_entries_after(object): else: raise NegativeStockError(message) + def update_bin_data(self, sle): + bin_name = get_or_make_bin(sle.item_code, sle.warehouse) + frappe.db.set_value( + "Bin", + bin_name, + { + "actual_qty": sle.qty_after_transaction, + "valuation_rate": sle.valuation_rate, + "stock_value": sle.stock_value, + }, + ) + def update_bin(self): # update bin for each warehouse for warehouse, data in self.data.items(): diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index fb526971ede..67233dd44da 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -220,7 +220,7 @@ def get_bin(item_code, warehouse): def get_or_make_bin(item_code: str, warehouse: str) -> str: - bin_record = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}) + bin_record = frappe.get_cached_value("Bin", {"item_code": item_code, "warehouse": warehouse}) if not bin_record: bin_obj = _create_bin(item_code, warehouse) From fe1e2fec7aefbc68d02d6a350454e83524dfd53e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 26 May 2023 11:29:22 +0530 Subject: [PATCH 36/46] fix: travis (cherry picked from commit 718ad3f24048b5acb32655a31a4965866cb429b0) --- erpnext/stock/stock_ledger.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9f81a8cd3f8..2f64eddb300 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1066,15 +1066,15 @@ class update_entries_after(object): def update_bin_data(self, sle): bin_name = get_or_make_bin(sle.item_code, sle.warehouse) - frappe.db.set_value( - "Bin", - bin_name, - { - "actual_qty": sle.qty_after_transaction, - "valuation_rate": sle.valuation_rate, - "stock_value": sle.stock_value, - }, - ) + values_to_update = { + "actual_qty": sle.qty_after_transaction, + "stock_value": sle.stock_value, + } + + if sle.valuation_rate is not None: + values_to_update["valuation_rate"] = sle.valuation_rate + + frappe.db.set_value("Bin", bin_name, values_to_update) def update_bin(self): # update bin for each warehouse From 71e4f34b86df785cb055a713debe2283683defa8 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Sat, 27 May 2023 12:05:37 +0530 Subject: [PATCH 37/46] fix: incorrect `POS Reserved Qty` in `Stock Projected Qty` Report (cherry picked from commit 027de4160097b90f93b90c0a2d24b82e1553df54) --- .../doctype/pos_invoice/pos_invoice.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 27e6f0e598f..aea801af9dd 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points @@ -673,18 +674,22 @@ def get_bin_qty(item_code, warehouse): def get_pos_reserved_qty(item_code, warehouse): - reserved_qty = frappe.db.sql( - """select sum(p_item.stock_qty) as qty - from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item - where p.name = p_item.parent - and ifnull(p.consolidated_invoice, '') = '' - and p_item.docstatus = 1 - and p_item.item_code = %s - and p_item.warehouse = %s - """, - (item_code, warehouse), - as_dict=1, - ) + p_inv = frappe.qb.DocType("POS Invoice") + p_item = frappe.qb.DocType("POS Invoice Item") + + reserved_qty = ( + frappe.qb.from_(p_inv) + .from_(p_item) + .select(Sum(p_item.qty).as_("qty")) + .where( + (p_inv.name == p_item.parent) + & (IfNull(p_inv.consolidated_invoice, "") == "") + & (p_inv.is_return == 0) + & (p_item.docstatus == 1) + & (p_item.item_code == item_code) + & (p_item.warehouse == warehouse) + ) + ).run(as_dict=True) return reserved_qty[0].qty or 0 if reserved_qty else 0 From 11440cca4c066969a2f04d82d4c93245eab5b62b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 29 May 2023 09:52:48 +0530 Subject: [PATCH 38/46] fix: Show future payments in accounts receivable summary (#35416) fix: Show future payments in accounts receivable summary (#35416) (cherry picked from commit 3504bf7f6250a23f4033df7e68f9c820286f7a6f) Co-authored-by: Nabin Hait --- erpnext/accounts/party.py | 8 +++++--- .../accounts_receivable_summary.py | 10 +++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 30d949b73b6..946170340ee 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -880,18 +880,21 @@ def get_party_shipping_address(doctype, name): def get_partywise_advanced_payment_amount( - party_type, posting_date=None, future_payment=0, company=None + party_type, posting_date=None, future_payment=0, company=None, party=None ): cond = "1=1" if posting_date: if future_payment: - cond = "posting_date <= '{0}' OR DATE(creation) <= '{0}' " "".format(posting_date) + cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date) else: cond = "posting_date <= '{0}'".format(posting_date) if company: cond += "and company = {0}".format(frappe.db.escape(company)) + if party: + cond += "and party = {0}".format(frappe.db.escape(party)) + data = frappe.db.sql( """ SELECT party, sum({0}) as amount FROM `tabGL Entry` @@ -903,7 +906,6 @@ def get_partywise_advanced_payment_amount( ), party_type, ) - if data: return frappe._dict(data) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 29217b04be2..9c01b1a4980 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -31,7 +31,6 @@ class AccountsReceivableSummary(ReceivablePayableReport): def get_data(self, args): self.data = [] - self.receivables = ReceivablePayableReport(self.filters).run(args)[1] self.get_party_total(args) @@ -42,6 +41,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.filters.report_date, self.filters.show_future_payments, self.filters.company, + party=self.filters.get(scrub(self.party_type)), ) or {} ) @@ -74,6 +74,9 @@ class AccountsReceivableSummary(ReceivablePayableReport): row.gl_balance = gl_balance_map.get(party) row.diff = flt(row.outstanding) - flt(row.gl_balance) + if self.filters.show_future_payments: + row.remaining_balance = flt(row.outstanding) - flt(row.future_amount) + self.data.append(row) def get_party_total(self, args): @@ -106,6 +109,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): "range4": 0.0, "range5": 0.0, "total_due": 0.0, + "future_amount": 0.0, "sales_person": [], } ), @@ -151,6 +155,10 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.setup_ageing_columns() + if self.filters.show_future_payments: + self.add_column(label=_("Future Payment Amount"), fieldname="future_amount") + self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance") + if self.party_type == "Customer": self.add_column( label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory" From 37d437a33cb2741b87c7c0e913c825aea85dc9ca Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Mon, 29 May 2023 23:17:59 +0530 Subject: [PATCH 39/46] fix: monthly WDV depr schedule for existing assets [v14] (#35458) * fix: monthly wdv depr schedule for existing assets * fix: monthly wdv depr schedule for existing assets properly --- erpnext/assets/doctype/asset/asset.py | 32 ++++++++++++--------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 8754bb7125a..ae77b9b94ce 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -337,10 +337,6 @@ class Asset(AccountsController): if should_get_last_day: schedule_date = get_last_day(schedule_date) - # schedule date will be a year later from start date - # so monthly schedule date is calculated by removing 11 months from it - monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1) - # if asset is being sold if date_of_disposal: from_date = self.get_from_date_for_disposal(finance_book) @@ -363,14 +359,20 @@ class Asset(AccountsController): break # For first row - if ( - (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) - and not self.opening_accumulated_depreciation - and n == 0 - ): - from_date = add_days( - self.available_for_use_date, -1 - ) # needed to calc depr amount for available_for_use_date too + if n == 0 and has_pro_rata and not self.opening_accumulated_depreciation: + from_date = add_days(self.available_for_use_date, -1) + depreciation_amount, days, months = self.get_pro_rata_amt( + finance_book, + depreciation_amount, + from_date, + finance_book.depreciation_start_date, + has_wdv_or_dd_non_yearly_pro_rata, + ) + elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation: + from_date = add_months( + getdate(self.available_for_use_date), + (self.number_of_depreciations_booked * finance_book.frequency_of_depreciation), + ) depreciation_amount, days, months = self.get_pro_rata_amt( finance_book, depreciation_amount, @@ -378,10 +380,6 @@ class Asset(AccountsController): finance_book.depreciation_start_date, has_wdv_or_dd_non_yearly_pro_rata, ) - - # For first depr schedule date will be the start date - # so monthly schedule date is calculated by removing month difference between use date and start date - monthly_schedule_date = add_months(finance_book.depreciation_start_date, -months + 1) # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: @@ -406,9 +404,7 @@ class Asset(AccountsController): depreciation_amount_without_pro_rata, depreciation_amount, finance_book.finance_book ) - monthly_schedule_date = add_months(schedule_date, 1) schedule_date = add_days(schedule_date, days) - last_schedule_date = schedule_date if not depreciation_amount: continue From 7b75f454d391d7635ac70dd3b8401ef143f63fb7 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 30 May 2023 08:34:12 +0530 Subject: [PATCH 40/46] fix: rate not fetching properly for inter transfer purchase order (cherry picked from commit 2931c657f4666d28f9614b55322d84f415a46b83) --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 29437beab10..83630df3a0b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -494,7 +494,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }, () => { // for internal customer instead of pricing rule directly apply valuation rate on item - if (me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) { + if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) && me.frm.doc.represents_company === me.frm.doc.company) { me.get_incoming_rate(item, me.frm.posting_date, me.frm.posting_time, me.frm.doc.doctype, me.frm.doc.company); } else { From bd75584c27b0236607b248273f3e7eeb8ca1d4ea Mon Sep 17 00:00:00 2001 From: Marc de Lima Lucio <68746600+marc-dll@users.noreply.github.com> Date: Mon, 29 May 2023 17:41:14 +0200 Subject: [PATCH 41/46] fix: retention stock entry: grab conversion factor from source (cherry picked from commit 6954f538c96c5c266d0dbc8423888722f90bc3a6) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d82fab8749a..efadf36199c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2401,7 +2401,7 @@ def move_sample_to_retention_warehouse(company, items): "basic_rate": item.get("valuation_rate"), "uom": item.get("uom"), "stock_uom": item.get("stock_uom"), - "conversion_factor": 1.0, + "conversion_factor": item.get("conversion_factor") or 1.0, "serial_no": sample_serial_nos, "batch_no": item.get("batch_no"), }, From d231b19b9fd701b18cd9aaa0fcae6fbd87394544 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 30 May 2023 18:42:07 +0530 Subject: [PATCH 42/46] fix: stock onboarding (backport #35453) (#35480) * fix: stock settings tour - remove dead fields - Keep only essential fields in tour (cherry picked from commit 2e13fbab5e15a8b29eaff86f6d4b7987114d31c9) # Conflicts: # erpnext/stock/doctype/stock_settings/stock_settings.json * fix: hide ledger button on new warehouse (cherry picked from commit 7a18db561fd0dd83c775fcfecdd8dbd28e25f0c1) * fix: warehouse form cleanup - organize fields - group transit fields and move them lower - enable/disable should be button - hide pointless fields from listview (cherry picked from commit 40ce33dff18cb4d63a4211c68c87cb12ebcad3d6) * fix: disable/enable with button (cherry picked from commit 81e901ba627e49e9e93a2f8b5be09a0c5d6e8112) * fix: warehouse tour - remove warehouse type, it doesn't do what it says. Misleading. (cherry picked from commit aa9f926298233ff5dfa2aff8fd3eaeb226264220) * fix: filter parent warehouses by company (cherry picked from commit 3341cd6b80e28c60225b4f7edb77e8b9ca7d1437) * fix: reorder stock reco tour (cherry picked from commit dd245ccc7fd24d60f52c19488789761f14eb4b6a) * fix: replace stock projected with ledger Ledger is much more widely used report, better to show that first? (cherry picked from commit 8fe8f800336c9e7702dd6c8d60d28b0bcc7a86c5) * chore: docs for stock settings [skip ci] (cherry picked from commit 964bb1d9483c43d1054f108b17daf94c0ce20ee3) # Conflicts: # erpnext/stock/doctype/stock_settings/stock_settings.json * chore: conflicts --------- Co-authored-by: Ankush Menat --- .../stock_settings/stock_settings.json | 7 +- erpnext/stock/doctype/warehouse/warehouse.js | 39 +++++---- .../stock/doctype/warehouse/warehouse.json | 30 ++++--- .../stock_reconciliation.json | 63 ++++++++++----- .../stock_settings/stock_settings.json | 79 ++++++++----------- .../stock/form_tour/warehouse/warehouse.json | 44 ++++++----- .../stock/module_onboarding/stock/stock.json | 6 +- .../check_stock_ledger_report.json | 24 ++++++ .../create_a_stock_entry.json | 2 +- .../create_a_warehouse.json | 2 +- .../stock_opening_balance.json | 2 +- .../stock_settings/stock_settings.json | 2 +- 12 files changed, 177 insertions(+), 123 deletions(-) create mode 100644 erpnext/stock/onboarding_step/check_stock_ledger_report/check_stock_ledger_report.json diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index ec7fb0f4a28..cb2bc0a7f51 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -8,12 +8,12 @@ "defaults_tab", "item_defaults_section", "item_naming_by", + "valuation_method", "item_group", - "stock_uom", "column_break_4", "default_warehouse", "sample_retention_warehouse", - "valuation_method", + "stock_uom", "price_list_defaults_section", "auto_insert_price_list_rate_if_missing", "column_break_12", @@ -96,6 +96,7 @@ "fieldtype": "Column Break" }, { + "documentation_url": "https://docs.erpnext.com/docs/v14/user/manual/en/stock/articles/calculation-of-valuation-rate-in-fifo-and-moving-average", "fieldname": "valuation_method", "fieldtype": "Select", "label": "Default Valuation Method", @@ -346,7 +347,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-02-05 15:33:43.692736", + "modified": "2023-05-29 15:09:54.959411", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index d69c624fba3..87a23efc590 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -17,6 +17,7 @@ frappe.ui.form.on("Warehouse", { return { filters: { is_group: 1, + company: doc.company, }, }; }); @@ -39,26 +40,34 @@ frappe.ui.form.on("Warehouse", { !frm.doc.__islocal ); - if (!frm.doc.__islocal) { + if (!frm.is_new()) { frappe.contacts.render_address_and_contact(frm); + + let enable_toggle = frm.doc.disabled ? "Enable" : "Disable"; + frm.add_custom_button(__(enable_toggle), () => { + frm.set_value('disabled', 1 - frm.doc.disabled); + frm.save() + }); + + frm.add_custom_button(__("Stock Balance"), function () { + frappe.set_route("query-report", "Stock Balance", { + warehouse: frm.doc.name, + }); + }); + + frm.add_custom_button( + frm.doc.is_group + ? __("Convert to Ledger", null, "Warehouse") + : __("Convert to Group", null, "Warehouse"), + function () { + convert_to_group_or_ledger(frm); + }, + ); + } else { frappe.contacts.clear_address_and_contact(frm); } - frm.add_custom_button(__("Stock Balance"), function () { - frappe.set_route("query-report", "Stock Balance", { - warehouse: frm.doc.name, - }); - }); - - frm.add_custom_button( - frm.doc.is_group - ? __("Convert to Ledger", null, "Warehouse") - : __("Convert to Group", null, "Warehouse"), - function () { - convert_to_group_or_ledger(frm); - }, - ); if (!frm.doc.is_group && frm.doc.__onload && frm.doc.__onload.account) { frm.add_custom_button( diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index c695d541bf9..43b2ad2a69b 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -1,23 +1,21 @@ { "actions": [], "allow_import": 1, - "creation": "2013-03-07 18:50:32", + "creation": "2023-05-29 13:02:17.121296", "description": "A logical Warehouse against which stock entries are made.", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", "field_order": [ "warehouse_detail", + "disabled", "warehouse_name", "column_break_3", - "warehouse_type", - "parent_warehouse", - "default_in_transit_warehouse", "is_group", + "parent_warehouse", "column_break_4", "account", "company", - "disabled", "address_and_contact", "address_html", "column_break_10", @@ -32,6 +30,10 @@ "city", "state", "pin", + "transit_section", + "warehouse_type", + "column_break_qajx", + "default_in_transit_warehouse", "tree_details", "lft", "rgt", @@ -58,7 +60,7 @@ "fieldname": "is_group", "fieldtype": "Check", "in_list_view": 1, - "label": "Is Group" + "label": "Is Group Warehouse" }, { "fieldname": "company", @@ -78,7 +80,7 @@ "default": "0", "fieldname": "disabled", "fieldtype": "Check", - "in_list_view": 1, + "hidden": 1, "label": "Disabled" }, { @@ -164,7 +166,6 @@ { "fieldname": "city", "fieldtype": "Data", - "in_list_view": 1, "label": "City", "oldfieldname": "city", "oldfieldtype": "Data" @@ -238,13 +239,23 @@ "fieldtype": "Link", "label": "Default In-Transit Warehouse", "options": "Warehouse" + }, + { + "collapsible": 1, + "fieldname": "transit_section", + "fieldtype": "Section Break", + "label": "Transit" + }, + { + "fieldname": "column_break_qajx", + "fieldtype": "Column Break" } ], "icon": "fa fa-building", "idx": 1, "is_tree": 1, "links": [], - "modified": "2022-03-01 02:37:48.034944", + "modified": "2023-05-29 13:10:43.333160", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", @@ -261,7 +272,6 @@ "read": 1, "report": 1, "role": "Item Manager", - "set_user_permissions": 1, "share": 1, "write": 1 }, diff --git a/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json index 5b7fd72c082..07a511071f8 100644 --- a/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json @@ -2,54 +2,75 @@ "creation": "2021-08-24 14:44:46.770952", "docstatus": 0, "doctype": "Form Tour", + "first_document": 0, "idx": 0, + "include_name_field": 0, "is_standard": 1, - "modified": "2021-08-25 16:26:11.718664", + "list_name": "List", + "modified": "2023-05-29 13:38:27.192177", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation", + "new_document_form": 0, "owner": "Administrator", "reference_doctype": "Stock Reconciliation", "save_on_complete": 1, "steps": [ { "description": "Set Purpose to Opening Stock to set the stock opening balance.", - "field": "", "fieldname": "purpose", "fieldtype": "Select", "has_next_condition": 1, + "hide_buttons": 0, "is_table_field": 0, "label": "Purpose", + "modal_trigger": 0, + "next_on_click": 0, "next_step_condition": "eval: doc.purpose === \"Opening Stock\"", - "parent_field": "", + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, "position": "Top", - "title": "Purpose" - }, - { - "description": "Select the items for which the opening stock has to be set.", - "field": "", - "fieldname": "items", - "fieldtype": "Table", - "has_next_condition": 1, - "is_table_field": 0, - "label": "Items", - "next_step_condition": "eval: doc.items[0]?.item_code", - "parent_field": "", - "position": "Top", - "title": "Items" + "title": "Purpose", + "ui_tour": 0 }, { "description": "Edit the Posting Date by clicking on the Edit Posting Date and Time checkbox below.", - "field": "", "fieldname": "posting_date", "fieldtype": "Date", "has_next_condition": 0, + "hide_buttons": 0, "is_table_field": 0, "label": "Posting Date", - "parent_field": "", + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, "position": "Bottom", - "title": "Posting Date" + "title": "Posting Date", + "ui_tour": 0 + }, + { + "description": "Select the items for which the opening stock has to be set.", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 1, + "hide_buttons": 0, + "is_table_field": 0, + "label": "Items", + "modal_trigger": 0, + "next_on_click": 0, + "next_step_condition": "eval: doc.items[0]?.item_code", + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Top", + "title": "Items", + "ui_tour": 0 } ], - "title": "Stock Reconciliation" + "title": "Stock Reconciliation", + "track_steps": 0, + "ui_tour": 0 } \ No newline at end of file diff --git a/erpnext/stock/form_tour/stock_settings/stock_settings.json b/erpnext/stock/form_tour/stock_settings/stock_settings.json index 3d164e33b3b..adbd1597f39 100644 --- a/erpnext/stock/form_tour/stock_settings/stock_settings.json +++ b/erpnext/stock/form_tour/stock_settings/stock_settings.json @@ -2,88 +2,73 @@ "creation": "2021-08-20 15:20:59.336585", "docstatus": 0, "doctype": "Form Tour", + "first_document": 0, "idx": 0, + "include_name_field": 0, "is_standard": 1, - "modified": "2021-08-25 16:19:37.699528", + "list_name": "List", + "modified": "2023-05-29 12:33:19.142202", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", + "new_document_form": 0, "owner": "Administrator", "reference_doctype": "Stock Settings", "save_on_complete": 1, "steps": [ { "description": "By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a Naming Series choose the 'Naming Series' option.", - "field": "", "fieldname": "item_naming_by", "fieldtype": "Select", "has_next_condition": 0, + "hide_buttons": 0, "is_table_field": 0, "label": "Item Naming By", - "parent_field": "", + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, "position": "Bottom", - "title": "Item Naming By" + "title": "Item Naming By", + "ui_tour": 0 }, { "description": "Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.", - "field": "", "fieldname": "default_warehouse", "fieldtype": "Link", "has_next_condition": 0, + "hide_buttons": 0, "is_table_field": 0, "label": "Default Warehouse", - "parent_field": "", + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, "position": "Bottom", - "title": "Default Warehouse" - }, - { - "description": "Quality inspection is performed on the inward and outward movement of goods. Receipt and delivery transactions will be stopped or the user will be warned if the quality inspection is not performed.", - "field": "", - "fieldname": "action_if_quality_inspection_is_not_submitted", - "fieldtype": "Select", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Action If Quality Inspection Is Not Submitted", - "parent_field": "", - "position": "Bottom", - "title": "Action if Quality Inspection Is Not Submitted" - }, - { - "description": "Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.", - "field": "", - "fieldname": "automatically_set_serial_nos_based_on_fifo", - "fieldtype": "Check", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Automatically Set Serial Nos Based on FIFO", - "parent_field": "", - "position": "Bottom", - "title": "Automatically Set Serial Nos based on FIFO" - }, - { - "description": "Show 'Scan Barcode' field above every child table to insert Items with ease.", - "field": "", - "fieldname": "show_barcode_field", - "fieldtype": "Check", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Show Barcode Field in Stock Transactions", - "parent_field": "", - "position": "Bottom", - "title": "Show Barcode Field" + "title": "Default Warehouse", + "ui_tour": 0 }, { "description": "Choose between FIFO and Moving Average Valuation Methods. Click here to know more about them.", - "field": "", "fieldname": "valuation_method", "fieldtype": "Select", "has_next_condition": 0, + "hide_buttons": 0, "is_table_field": 0, "label": "Default Valuation Method", - "parent_field": "", + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, "position": "Bottom", - "title": "Default Valuation Method" + "title": "Default Valuation Method", + "ui_tour": 0 } ], - "title": "Stock Settings" + "title": "Stock Settings", + "track_steps": 0, + "ui_tour": 0 } \ No newline at end of file diff --git a/erpnext/stock/form_tour/warehouse/warehouse.json b/erpnext/stock/form_tour/warehouse/warehouse.json index 23ff2aebbaa..5897357bc73 100644 --- a/erpnext/stock/form_tour/warehouse/warehouse.json +++ b/erpnext/stock/form_tour/warehouse/warehouse.json @@ -2,53 +2,57 @@ "creation": "2021-08-24 14:43:44.465237", "docstatus": 0, "doctype": "Form Tour", + "first_document": 0, "idx": 0, + "include_name_field": 0, "is_standard": 1, - "modified": "2021-08-24 14:50:31.988256", + "list_name": "List", + "modified": "2023-05-29 13:09:49.920796", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", + "new_document_form": 0, "owner": "Administrator", "reference_doctype": "Warehouse", "save_on_complete": 1, "steps": [ { "description": "Select a name for the warehouse. This should reflect its location or purpose.", - "field": "", "fieldname": "warehouse_name", "fieldtype": "Data", "has_next_condition": 1, + "hide_buttons": 0, "is_table_field": 0, "label": "Warehouse Name", + "modal_trigger": 0, + "next_on_click": 0, "next_step_condition": "eval: doc.warehouse_name", - "parent_field": "", + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, "position": "Bottom", - "title": "Warehouse Name" - }, - { - "description": "Select a warehouse type to categorize the warehouse into a sub-group.", - "field": "", - "fieldname": "warehouse_type", - "fieldtype": "Link", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Warehouse Type", - "parent_field": "", - "position": "Top", - "title": "Warehouse Type" + "title": "Warehouse Name", + "ui_tour": 0 }, { "description": "Select an account to set a default account for all transactions with this warehouse.", - "field": "", "fieldname": "account", "fieldtype": "Link", "has_next_condition": 0, + "hide_buttons": 0, "is_table_field": 0, "label": "Account", - "parent_field": "", + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, "position": "Top", - "title": "Account" + "title": "Account", + "ui_tour": 0 } ], - "title": "Warehouse" + "title": "Warehouse", + "track_steps": 0, + "ui_tour": 0 } \ No newline at end of file diff --git a/erpnext/stock/module_onboarding/stock/stock.json b/erpnext/stock/module_onboarding/stock/stock.json index c246747a5b3..864ac4be3b4 100644 --- a/erpnext/stock/module_onboarding/stock/stock.json +++ b/erpnext/stock/module_onboarding/stock/stock.json @@ -19,7 +19,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock", "idx": 0, "is_complete": 0, - "modified": "2021-08-20 14:38:55.570067", + "modified": "2023-05-29 14:43:36.223302", "modified_by": "Administrator", "module": "Stock", "name": "Stock", @@ -35,10 +35,10 @@ "step": "Create a Stock Entry" }, { - "step": "Stock Opening Balance" + "step": "Check Stock Ledger Report" }, { - "step": "View Stock Projected Qty" + "step": "Stock Opening Balance" } ], "subtitle": "Inventory, Warehouses, Analysis, and more.", diff --git a/erpnext/stock/onboarding_step/check_stock_ledger_report/check_stock_ledger_report.json b/erpnext/stock/onboarding_step/check_stock_ledger_report/check_stock_ledger_report.json new file mode 100644 index 00000000000..cdbc0b759ba --- /dev/null +++ b/erpnext/stock/onboarding_step/check_stock_ledger_report/check_stock_ledger_report.json @@ -0,0 +1,24 @@ +{ + "action": "View Report", + "action_label": "Check Stock Ledger", + "creation": "2023-05-29 13:46:04.174565", + "description": "# Check Stock Reports\nBased on the various stock transactions, you can get a host of one-click Stock Reports in ERPNext like Stock Ledger, Stock Balance, Projected Quantity, and Ageing analysis.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2023-05-29 14:39:03.943244", + "modified_by": "Administrator", + "name": "Check Stock Ledger Report", + "owner": "Administrator", + "reference_report": "Stock Ledger", + "report_description": "Stock Ledger report contains every submitted stock transaction. You can use filter to narrow down ledger entries.", + "report_reference_doctype": "Stock Ledger Entry", + "report_type": "Script Report", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Check Stock Ledger", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json index 3cb522c893d..dea2aae9500 100644 --- a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json +++ b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-06-18 13:57:11.434063", + "modified": "2023-05-29 14:39:04.066547", "modified_by": "Administrator", "name": "Create a Stock Entry", "owner": "Administrator", diff --git a/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json index 22c88bf10ea..25926127a08 100644 --- a/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json +++ b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-18 12:23:36.675572", + "modified": "2023-05-29 14:39:04.074907", "modified_by": "Administrator", "name": "Create a Warehouse", "owner": "Administrator", diff --git a/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json b/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json index 48fd1fddee0..18c95505d72 100644 --- a/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json +++ b/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-06-18 13:59:36.021097", + "modified": "2023-05-29 14:39:08.825699", "modified_by": "Administrator", "name": "Stock Opening Balance", "owner": "Administrator", diff --git a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json index 2cf90e806cd..b48ac806a46 100644 --- a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json +++ b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 1, "is_skipped": 0, - "modified": "2021-08-18 12:06:51.139387", + "modified": "2023-05-29 14:39:04.083360", "modified_by": "Administrator", "name": "Stock Settings", "owner": "Administrator", From c17e537bb619788717d67d300f86635842a43de6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 30 May 2023 18:59:43 +0530 Subject: [PATCH 43/46] chore: trigger ci --- .github/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/stale.yml b/.github/stale.yml index da15d32680f..f0e384b497f 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -13,7 +13,7 @@ exemptProjects: true exemptMilestones: true pulls: - daysUntilStale: 15 + daysUntilStale: 14 daysUntilClose: 3 exemptLabels: - hotfix From 86801c29cb973c9f05de553cf7298a673fe94017 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 08:45:02 +0530 Subject: [PATCH 44/46] fix: incorrect transferred qty in the job card (#35478) fix: incorrect transfer quantity in the job card (cherry picked from commit d3a5e49db953744c986bb5027ab6caf9c43f6e7d) Co-authored-by: Rohit Waghchaure --- .../doctype/job_card/job_card.py | 22 ++++++++----------- .../doctype/job_card/test_job_card.py | 6 +++++ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index a7d0b29f83c..fcaa3fd276f 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -653,23 +653,19 @@ class JobCard(Document): exc=JobCardOverTransferError, ) - job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc) + job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc) or {} + allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") - if job_card_items_transferred_qty: - allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") + for row in ste_doc.items: + if not row.job_card_item: + continue - for row in ste_doc.items: - if not row.job_card_item: - continue + transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item, 0.0)) - transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item)) + if not allow_excess: + _validate_over_transfer(row, transferred_qty) - if not allow_excess: - _validate_over_transfer(row, transferred_qty) - - frappe.db.set_value( - "Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty) - ) + frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)) def set_transferred_qty(self, update_status=False): "Set total FG Qty in Job Card for which RM was transferred." diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 61766a67511..22177f44148 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -342,6 +342,12 @@ class TestJobCard(FrappeTestCase): job_card.reload() self.assertEqual(job_card.transferred_qty, 2) + transfer_entry_2.cancel() + transfer_entry.cancel() + + job_card.reload() + self.assertEqual(job_card.transferred_qty, 0.0) + def test_job_card_material_transfer_correctness(self): """ 1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card From 5fd00e7113ab7323ed2ab10338f6e2f99e8c8561 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 11:02:14 +0530 Subject: [PATCH 45/46] fix: Error while validating budget (#35487) fix: Error while validating budget (#35487) * fix: Error while validating budget * chore: remove print statement (cherry picked from commit 27d5e6a99bd72f2f44b31510da331d04fe3bc7bd) Co-authored-by: Deepesh Garg --- erpnext/accounts/doctype/budget/budget.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index ef2a7621f84..6cfd15d3ec8 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -125,14 +125,27 @@ def validate_expense_against_budget(args, expense_amount=0): if not args.account: return - for budget_against in ["project", "cost_center"] + get_accounting_dimensions(): + default_dimensions = [ + { + "fieldname": "project", + "document_type": "Project", + }, + { + "fieldname": "cost_center", + "document_type": "Cost Center", + }, + ] + + for dimension in default_dimensions + get_accounting_dimensions(as_list=False): + budget_against = dimension.get("fieldname") + if ( args.get(budget_against) and args.account and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"}) ): - doctype = frappe.unscrub(budget_against) + doctype = dimension.get("document_type") if frappe.get_cached_value("DocType", doctype, "is_tree"): lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"]) From 33e8d0571815cd20362d9ca65d6dd0de633daf21 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 11:03:49 +0530 Subject: [PATCH 46/46] fix: Billing Address display in buying transactions (#35451) fix: Billing Address display in buying transactions (#35451) (cherry picked from commit bb21c044f6fb99aca4338cde5978ae77524be2c5) Co-authored-by: Deepesh Garg --- erpnext/controllers/buying_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index ab29dd85829..fa566f62ef0 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -184,6 +184,7 @@ class BuyingController(SubcontractingController): address_dict = { "supplier_address": "address_display", "shipping_address": "shipping_address_display", + "billing_address": "billing_address_display", } for address_field, address_display_field in address_dict.items():