From fc8d451c55fd2a64715309306d965704caf2ee80 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 13 Jun 2025 12:24:50 +0530 Subject: [PATCH 01/58] fix: use `flt` value of bin qty (cherry picked from commit 0a8e42a358cb47e2e9f46b8d4c4cdfd81d5135c9) --- erpnext/stock/doctype/pick_list/pick_list.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 31bff657fd1..cc47ed17f75 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -113,13 +113,15 @@ class PickList(Document): continue - bin_qty = frappe.db.get_value( - "Bin", - {"item_code": row.item_code, "warehouse": row.warehouse}, - "actual_qty", + bin_qty = flt( + frappe.db.get_value( + "Bin", + {"item_code": row.item_code, "warehouse": row.warehouse}, + "actual_qty", + ) ) - if row.picked_qty > flt(bin_qty): + if row.picked_qty > bin_qty: frappe.throw( _( "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}." From 7a4c8d81e23a180481c714d5dade9e06f6710fc8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:23:30 +0530 Subject: [PATCH 02/58] fix: sort available batches based on expiry when merging SLEs with SABB and those without (backport #48471) (#48473) * fix: sort available batches based on expiry Co-authored-by: Mihir Kandoi --- erpnext/stock/doctype/batch/test_batch.py | 14 ++++++++++++-- .../serial_and_batch_bundle.py | 5 +++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 3ef0e57c25a..1bd202df28a 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -305,8 +305,18 @@ class TestBatch(FrappeTestCase): self.assertEqual( get_batch_qty(item_code="ITEM-BATCH-2", warehouse="_Test Warehouse - _TC"), [ - {"batch_no": "batch a", "qty": 90.0, "warehouse": "_Test Warehouse - _TC"}, - {"batch_no": "batch b", "qty": 90.0, "warehouse": "_Test Warehouse - _TC"}, + { + "batch_no": "batch a", + "qty": 90.0, + "warehouse": "_Test Warehouse - _TC", + "expiry_date": None, + }, + { + "batch_no": "batch b", + "qty": 90.0, + "warehouse": "_Test Warehouse - _TC", + "expiry_date": None, + }, ], ) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 11697409820..5f4fe99d1b9 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -2148,6 +2148,9 @@ def get_auto_batch_nos(kwargs): picked_batches, ) + if kwargs.based_on == "Expiry": + available_batches = sorted(available_batches, key=lambda x: (x.expiry_date or "9999-12-31")) + if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_date"): filter_zero_near_batches(available_batches, kwargs) @@ -2247,6 +2250,7 @@ def get_available_batches(kwargs): batch_ledger.batch_no, batch_ledger.warehouse, Sum(batch_ledger.qty).as_("qty"), + batch_table.expiry_date, ) .where(batch_table.disabled == 0) .where(stock_ledger_entry.is_cancelled == 0) @@ -2537,6 +2541,7 @@ def get_stock_ledgers_batches(kwargs): stock_ledger_entry.item_code, Sum(stock_ledger_entry.actual_qty).as_("qty"), stock_ledger_entry.batch_no, + batch_table.expiry_date, ) .where((stock_ledger_entry.is_cancelled == 0) & (stock_ledger_entry.batch_no.isnotnull())) .groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse) From 4eb9f73a522a4c7ea13430eb3e2080be26f55793 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 8 Jul 2025 21:58:31 +0530 Subject: [PATCH 03/58] fix: indicator in material_request_list.js (cherry picked from commit d10647a59253383f2ad3b038e7dc0700e26604c7) --- .../stock/doctype/material_request/material_request_list.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index b20ac15f136..87174bc7513 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -6,11 +6,11 @@ frappe.listview_settings["Material Request"] = { return [__("Stopped"), "red", "status,=,Stopped"]; } else if (doc.transfer_status && doc.docstatus != 2) { if (doc.transfer_status == "Not Started") { - return [__("Not Started"), "orange"]; + return [__("Not Started"), "orange", "transfer_status,=,Not Started"]; } else if (doc.transfer_status == "In Transit") { - return [__("In Transit"), "yellow"]; + return [__("In Transit"), "yellow", "transfer_status,=,In Transit"]; } else if (doc.transfer_status == "Completed") { - return [__("Completed"), "green"]; + return [__("Completed"), "green", "transfer_status,=,Completed"]; } } else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) { return [__("Pending"), "orange", "per_ordered,=,0|docstatus,=,1"]; From 243b5331503e8d337066177edee589d8dedb62da Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 23:50:20 +0530 Subject: [PATCH 04/58] fix: resolve sql error on dimension-wise accounts balance report (backport #48477) (#48478) fix: resolve sql error on dimension-wise accounts balance report (#48477) (cherry picked from commit c714b724da7fb06c4f66ad4f9745c8f3ee428e4f) Co-authored-by: Diptanil Saha --- .../dimension_wise_accounts_balance_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index db984e821a5..084ea9b80ea 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -179,7 +179,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list): def get_condition(dimension): conditions = [] - conditions.append(f"{frappe.scrub(dimension)} in %(dimensions)s") + conditions.append(f"{frappe.scrub(dimension)} in (%(dimensions)s)") return " and {}".format(" and ".join(conditions)) if conditions else "" From a5c49d1e08506819a0f60296cf4dfb8035b9b921 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 9 Jul 2025 11:40:43 +0530 Subject: [PATCH 05/58] fix: stock settings save issue (cherry picked from commit 64ae1ec36705d7cc3f26516ab3f6f2219a1a784d) --- erpnext/stock/doctype/stock_settings/stock_settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 1513f48eb9f..ed76d96fd2e 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -120,7 +120,11 @@ class StockSettings(Document): ) def cant_change_valuation_method(self): - previous_valuation_method = self.get_doc_before_save().get("valuation_method") + doc_before_save = self.get_doc_before_save() + if not doc_before_save: + return + + previous_valuation_method = doc_before_save.get("valuation_method") if previous_valuation_method and previous_valuation_method != self.valuation_method: # check if there are any stock ledger entries against items From facd2027c3e26ecb6a17a5ca29ad417b604c2948 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 9 Jul 2025 16:05:26 +0530 Subject: [PATCH 06/58] feat: batch rate (valuation) in Batch-Wise Balance History report (cherry picked from commit 8a2a845a16a00fb2d48664ca0b107cc6eabd7f3f) --- .../batch_wise_balance_history.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 99e0676eca3..f895947f503 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -56,6 +56,11 @@ def execute(filters=None): flt(qty_dict.in_qty, float_precision), flt(qty_dict.out_qty, float_precision), flt(qty_dict.bal_qty, float_precision), + flt( + (qty_dict.bal_value / qty_dict.bal_qty) if qty_dict.bal_qty else 0, + float_precision, + ), + flt(qty_dict.bal_value, float_precision), item_map[item]["stock_uom"], ] ) @@ -68,14 +73,16 @@ def get_columns(filters): columns = [ _("Item") + ":Link/Item:100", - _("Item Name") + "::150", - _("Description") + "::150", + _("Item Name") + "::120", + _("Description") + "::90", _("Warehouse") + ":Link/Warehouse:100", _("Batch") + ":Link/Batch:100", _("Opening Qty") + ":Float:90", _("In Qty") + ":Float:80", _("Out Qty") + ":Float:80", - _("Balance Qty") + ":Float:90", + _("Balance Qty") + ":Float:120", + _("Valuation Rate") + ":Float:120", + _("Balance Value") + ":Currency:120", _("UOM") + "::90", ] @@ -107,6 +114,7 @@ def get_stock_ledger_entries_for_batch_no(filters): sle.batch_no, sle.posting_date, fn.Sum(sle.actual_qty).as_("actual_qty"), + fn.Sum(sle.stock_value_difference).as_("stock_value_difference"), ) .where( (sle.docstatus < 2) @@ -151,6 +159,7 @@ def get_stock_ledger_entries_for_batch_bundle(filters): batch_package.batch_no, sle.posting_date, fn.Sum(batch_package.qty).as_("actual_qty"), + fn.Sum(batch_package.stock_value_difference).as_("stock_value_difference"), ) .where( (sle.docstatus < 2) @@ -191,7 +200,10 @@ def get_item_warehouse_batch_map(filters, float_precision): for d in sle: iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault( - d.batch_no, frappe._dict({"opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0}) + d.batch_no, + frappe._dict( + {"opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0, "bal_value": 0.0} + ), ) qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] if d.posting_date < from_date: @@ -207,6 +219,7 @@ def get_item_warehouse_batch_map(filters, float_precision): ) qty_dict.bal_qty = flt(qty_dict.bal_qty, float_precision) + flt(d.actual_qty, float_precision) + qty_dict.bal_value += flt(d.stock_value_difference, float_precision) return iwb_map From db525c2538dae80e2c04c3cf5be386e9ec293628 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 9 Jul 2025 16:16:43 +0530 Subject: [PATCH 07/58] feat: parent item group support in Stock Projected Qty report (cherry picked from commit 6e80d89d133b37d9be30c24550b838a227d9bac7) --- .../report/stock_projected_qty/stock_projected_qty.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index 9b4520064d6..3193ba3de51 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.utils import flt, today +from frappe.utils.nestedset import get_descendants_of from pypika.terms import ExistsCriterion from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_pos_reserved_qty @@ -21,6 +22,10 @@ def execute(filters=None): columns = get_columns() bin_list = get_bin_list(filters) item_map = get_item_map(filters.get("item_code"), include_uom) + item_groups = [] + if filters.get("item_group"): + item_groups.append(filters.item_group) + item_groups.extend(get_descendants_of("Item Group", filters.item_group)) warehouse_company = {} data = [] @@ -40,7 +45,7 @@ def execute(filters=None): if filters.brand and filters.brand != item.brand: continue - elif filters.item_group and filters.item_group != item.item_group: + elif item_groups and item.item_group not in item_groups: continue elif filters.company and filters.company != company: From fdd79c767780db2407a566b6bbb10e294b87bf3f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 14:13:24 +0200 Subject: [PATCH 08/58] feat(BOM): improve tree display with item_name and qty (backport #48176) (#48494) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrick Eißler <77415730+PatrickDEissler@users.noreply.github.com> Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- erpnext/manufacturing/doctype/bom/bom_tree.js | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index bf6bf2530ce..047b39df54f 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1274,7 +1274,7 @@ def get_children(parent=None, is_root=False, **filters): bom_items = frappe.get_all( "BOM Item", - fields=["item_code", "bom_no as value", "stock_qty"], + fields=["item_code", "bom_no as value", "stock_qty", "qty"], filters=[["parent", "=", frappe.form_dict.parent]], order_by="idx", ) diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js index 534de0e654b..9b4b3c8f96c 100644 --- a/erpnext/manufacturing/doctype/bom/bom_tree.js +++ b/erpnext/manufacturing/doctype/bom/bom_tree.js @@ -16,7 +16,14 @@ frappe.treeview_settings["BOM"] = { show_expand_all: false, get_label: function (node) { if (node.data.qty) { - return node.data.qty + " x " + node.data.item_code; + const escape = frappe.utils.escape_html; + let label = escape(node.data.item_code); + if (node.data.item_name && node.data.item_code !== node.data.item_name) { + label += `: ${escape(node.data.item_name)}`; + } + return `${label} ${node.data.qty} ${escape( + __(node.data.stock_uom) + )}`; } else { return node.data.item_code || node.data.value; } From 8c77ea16cf9c878f19c0252e98815234fb7bad73 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 9 Jul 2025 16:44:38 +0530 Subject: [PATCH 09/58] feat: update the modified date of the SLE after reposting (cherry picked from commit c2cd4934e77ec1cd276f3810625110d632404ff6) --- erpnext/stock/stock_ledger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f742f52daee..8e2319f7bd6 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -919,6 +919,7 @@ class update_entries_after: ) sle.doctype = "Stock Ledger Entry" + sle.modified = now() frappe.get_doc(sle).db_update() if not self.args.get("sle_id") or ( From 89660c90708e7cedecf99125ed267c2b83409c0d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 9 Jul 2025 20:59:11 +0530 Subject: [PATCH 10/58] fix: use planned_qty instead of pending_qty to check if WO should be created against PP (cherry picked from commit b11bf8eb795c101b090fa789ab4eb4fa26f361a3) --- .../manufacturing/doctype/production_plan/production_plan.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 268bf847a90..1d55c64663f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -203,8 +203,8 @@ frappe.ui.form.on("Production Plan", { let has_items = items.filter((item) => { - if (item.pending_qty) { - return item.pending_qty > item.ordered_qty; + if (item.planned_qty) { + return item.planned_qty > item.ordered_qty; } else { return item.qty > (item.received_qty || item.ordered_qty); } From 7ac546333a11934a7b181139d71c45692ebd120c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 10 Jul 2025 10:48:33 +0530 Subject: [PATCH 11/58] fix: invalid comparison error in sabb.py (cherry picked from commit ec1faf02ed6ca55c4c2e96810308a2ddde4fd8e6) --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 5f4fe99d1b9..ae66b0f01d4 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -11,11 +11,11 @@ from frappe.model.document import Document from frappe.model.naming import make_autoname from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import ( - add_days, cint, cstr, flt, get_link_to_form, + getdate, now, nowtime, parse_json, @@ -2149,7 +2149,7 @@ def get_auto_batch_nos(kwargs): ) if kwargs.based_on == "Expiry": - available_batches = sorted(available_batches, key=lambda x: (x.expiry_date or "9999-12-31")) + available_batches = sorted(available_batches, key=lambda x: (x.expiry_date or getdate("9999-12-31"))) if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_date"): filter_zero_near_batches(available_batches, kwargs) From 00d39eb208dc9b791dcb79a144b6703958a0b6ea Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Thu, 16 Jan 2025 17:40:54 -0300 Subject: [PATCH 12/58] fix: prevent unnecessary db.commit (cherry picked from commit 5f15b0b65b1950589d9e4e3b16e1d950cb67b459) --- erpnext/telephony/doctype/call_log/call_log.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 7afa06ff9a7..8093a6a0226 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -157,6 +157,8 @@ def link_existing_conversations(doc, state): """ Called from hooks on creation of Contact or Lead to link all the existing conversations. """ + if doc.flags.ignore_auto_link_call_log: + return if doc.doctype != "Contact": return try: @@ -183,12 +185,12 @@ def link_existing_conversations(doc, state): """, dict(phone_number=f"%{number}", docname=doc.name, doctype=doc.doctype), ) - - for log in logs: - call_log = frappe.get_doc("Call Log", log) - call_log.add_link(link_type=doc.doctype, link_name=doc.name) - call_log.save(ignore_permissions=True) - frappe.db.commit() + if logs: + for log in logs: + call_log = frappe.get_doc("Call Log", log) + call_log.add_link(link_type=doc.doctype, link_name=doc.name) + call_log.save(ignore_permissions=True) + frappe.db.commit() except Exception: frappe.log_error(title=_("Error during caller information update")) From 5cfeb2978b5a9913bf576946b2bed5efcf75b215 Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Thu, 16 Jan 2025 17:51:22 -0300 Subject: [PATCH 13/58] fix: prevent unnecessary db.commit for contact insert [Linters] (cherry picked from commit 87de5c7450da865f041e859da68baa2a6394cea2) --- erpnext/telephony/doctype/call_log/call_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 8093a6a0226..fd4a0dffd8d 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -185,7 +185,7 @@ def link_existing_conversations(doc, state): """, dict(phone_number=f"%{number}", docname=doc.name, doctype=doc.doctype), ) - if logs: + if logs: for log in logs: call_log = frappe.get_doc("Call Log", log) call_log.add_link(link_type=doc.doctype, link_name=doc.name) From 3a70b5d7fc276199d2394a032469217a76f8a8d6 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Thu, 10 Jul 2025 12:31:15 +0530 Subject: [PATCH 14/58] fix: pos adding item multiple times on item group filter (cherry picked from commit e9f99e5a3f38102615af1903874c7fb7888e50a4) --- .../selling/page/point_of_sale/pos_item_selector.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index 6ae0d675140..e930322a2c4 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -345,7 +345,10 @@ erpnext.PointOfSale.ItemSelector = class { const items = this.search_index[selling_price_list][search_term]; this.items = items; this.render_item_list(items); - this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart(); + this.auto_add_item && + this.search_field.$input[0].value && + this.items.length == 1 && + this.add_filtered_item_to_cart(); return; } } @@ -358,7 +361,10 @@ erpnext.PointOfSale.ItemSelector = class { } this.items = items; this.render_item_list(items); - this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart(); + this.auto_add_item && + this.search_field.$input[0].value && + this.items.length == 1 && + this.add_filtered_item_to_cart(); }); } From f80ad4ee589359ba83e1c16e7a9fc4d975a5933b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 10 Jul 2025 17:21:00 +0530 Subject: [PATCH 15/58] fix: missing parameter in precision function (cherry picked from commit 388664188730d100555c3b145d71558e13940bad) --- .../doctype/serial_and_batch_bundle/serial_and_batch_bundle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 5f4fe99d1b9..1f88453f874 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -804,7 +804,7 @@ class SerialandBatchBundle(Document): if qty_field == "qty" and row.get("stock_qty"): qty = row.get("stock_qty") - precision = row.precision + precision = row.precision(qty_field) if abs(abs(flt(self.total_qty, precision)) - abs(flt(qty, precision))) > 0.01: total_qty = frappe.format_value(abs(flt(self.total_qty)), "Float", row) set_qty = frappe.format_value(abs(flt(row.get(qty_field))), "Float", row) From f1ff5a39ae45ec043c446251d62e83e3bda99af4 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 10 Jul 2025 22:34:35 +0530 Subject: [PATCH 16/58] fix: error in available serial no report is no serial no present in company (cherry picked from commit 0ae60b8b619dd33f8fd28936c89b516596fae606) --- .../available_serial_no/available_serial_no.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/report/available_serial_no/available_serial_no.py b/erpnext/stock/report/available_serial_no/available_serial_no.py index 6911b979ae4..c7fd27020c2 100644 --- a/erpnext/stock/report/available_serial_no/available_serial_no.py +++ b/erpnext/stock/report/available_serial_no/available_serial_no.py @@ -19,21 +19,17 @@ def execute(filters=None): columns = get_columns(filters) items = get_items(filters) sl_entries = get_stock_ledger_entries(filters, items) + + if not sl_entries: + return columns, [] + item_details = get_item_details(items, sl_entries, False) - - opening_row = get_opening_balance_data(filters, columns, sl_entries) - + opening_row = get_opening_balance(filters, columns, sl_entries) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) data = process_stock_ledger_entries(sl_entries, item_details, opening_row, precision) - return columns, data -def get_opening_balance_data(filters, columns, sl_entries): - opening_row = get_opening_balance(filters, columns, sl_entries) - return opening_row - - def process_stock_ledger_entries(sl_entries, item_details, opening_row, precision): data = [] From 5fce8191f90375225ec128c0b1a94d28b5f529e2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 10 Jul 2025 22:46:56 +0530 Subject: [PATCH 17/58] fix: no attribute error in old subcontracting flow (cherry picked from commit 51751a7a055b8fd16c4a418894cc5698d4205477) --- erpnext/controllers/subcontracting_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 22a71734f3d..0a9999df9df 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -464,7 +464,7 @@ class SubcontractingController(StockController): i += 1 def __remove_serial_and_batch_bundle(self, item): - if item.serial_and_batch_bundle: + if item.get("serial_and_batch_bundle"): frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True) def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): From f2af2fe63bd467f0e871761dc4e58e223b1937ea Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 10 Jul 2025 21:39:23 +0530 Subject: [PATCH 18/58] fix: incorrect last sle for no batch wise valuation (cherry picked from commit 93d3eb662fbe9516de0ccf7b0d101d80fc1c2d57) --- erpnext/stock/deprecated_serial_batch.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index a8e17993c30..11e0d0964f0 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -284,6 +284,12 @@ class DeprecatedBatchNoValuation: if self.sle.name: query = query.where(sle.name != self.sle.name) + if self.sle.serial_and_batch_bundle: + query = query.where( + (sle.serial_and_batch_bundle != self.sle.serial_and_batch_bundle) + | (sle.serial_and_batch_bundle.isnull()) + ) + data = query.run(as_dict=True) return data[0] if data else frappe._dict() From c894b1816508e20cb218ecc3d1e4cc785ab471e7 Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Fri, 4 Jul 2025 08:26:52 +0200 Subject: [PATCH 19/58] fix: employee_exit_translatability (cherry picked from commit 80d677921059f3265ce2b265368b1483967821ef) --- erpnext/setup/doctype/employee/employee.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 30de87e151a..1c80fd84965 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -600,7 +600,7 @@ "collapsible": 1, "fieldname": "exit", "fieldtype": "Tab Break", - "label": "Exit", + "label": "Employee Exit", "oldfieldtype": "Section Break" }, { @@ -822,7 +822,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2025-02-07 13:54:40.122345", + "modified": "2025-07-04 08:29:34.347269", "modified_by": "Administrator", "module": "Setup", "name": "Employee", From f1cdd76fc1adf8fa7714f7824edef29839a393fd Mon Sep 17 00:00:00 2001 From: ljain112 Date: Wed, 9 Jul 2025 13:28:37 +0530 Subject: [PATCH 20/58] perf: use `cached_doc` for Account Settings (cherry picked from commit 751f3abd95d22f6ee0c1770e414ee1cfa589010a) --- erpnext/setup/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 9fb036a48d9..eae5c9f6c74 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -104,7 +104,7 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if not transaction_date: transaction_date = nowdate() - currency_settings = frappe.get_doc("Accounts Settings").as_dict() + currency_settings = frappe.get_cached_doc("Accounts Settings") allow_stale_rates = currency_settings.get("allow_stale") filters = [ From 567f7b4d71cb84e2951f64d55a098474c111f90e Mon Sep 17 00:00:00 2001 From: ljain112 Date: Sat, 12 Jul 2025 12:48:01 +0530 Subject: [PATCH 21/58] chore: fix flacky test and remove redundant code (cherry picked from commit de8c3ba968ce239db3a1e317254aa77852628dcc) # Conflicts: # erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py --- erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py | 7 +++++++ erpnext/stock/doctype/batch/test_batch.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index aa0a898bd42..1b72b15a3e3 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -1073,8 +1073,15 @@ def create_pos_invoice(**args): return pos_inv +<<<<<<< HEAD def make_batch_item(item_name): from erpnext.stock.doctype.item.test_item import make_item if not frappe.db.exists(item_name): return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) +======= +def set_allow_partial_payment(pos_profile, value): + pos_profile.reload() + pos_profile.allow_partial_payment = value + pos_profile.save() +>>>>>>> de8c3ba968 (chore: fix flacky test and remove redundant code) diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 1bd202df28a..e678f3be3e2 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -35,7 +35,7 @@ class TestBatch(FrappeTestCase): def make_batch_item(cls, item_name=None): from erpnext.stock.doctype.item.test_item import make_item - if not frappe.db.exists(item_name): + if not frappe.db.exists("Item", item_name): return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) def test_purchase_receipt(self, batch_qty=100): From 8f6cd40c7b289b20ef3295e6987beee777c78a3b Mon Sep 17 00:00:00 2001 From: ljain112 Date: Sat, 12 Jul 2025 13:07:52 +0530 Subject: [PATCH 22/58] chore: return doc if item already exists for test (cherry picked from commit e6b9e82b2fba369edabec85383680a6e4d2a3e1c) --- erpnext/stock/doctype/batch/test_batch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index e678f3be3e2..1d44d19ac81 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -38,6 +38,8 @@ class TestBatch(FrappeTestCase): if not frappe.db.exists("Item", item_name): return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) + return frappe.get_doc("Item", item_name) + def test_purchase_receipt(self, batch_qty=100): """Test automated batch creation from Purchase Receipt""" self.make_batch_item("ITEM-BATCH-1") From 153df4eca5901ddee6fdfcf019c4d3e2983bf07c Mon Sep 17 00:00:00 2001 From: ljain112 Date: Sun, 13 Jul 2025 18:16:49 +0530 Subject: [PATCH 23/58] chore: resolve conflicts --- .../doctype/pos_invoice/test_pos_invoice.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 1b72b15a3e3..2cc1d5f22bb 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -1071,17 +1071,3 @@ def create_pos_invoice(**args): pos_inv.payment_schedule = [] return pos_inv - - -<<<<<<< HEAD -def make_batch_item(item_name): - from erpnext.stock.doctype.item.test_item import make_item - - if not frappe.db.exists(item_name): - return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) -======= -def set_allow_partial_payment(pos_profile, value): - pos_profile.reload() - pos_profile.allow_partial_payment = value - pos_profile.save() ->>>>>>> de8c3ba968 (chore: fix flacky test and remove redundant code) From 72e154fbb761eafd0917c43291f470b998cb6a4f Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Fri, 11 Jul 2025 17:54:09 +0530 Subject: [PATCH 24/58] feat: add calculate_ageing_with option in summary reports (cherry picked from commit a3834eef460e5ac2bf5a086227c118ae1bc57b3e) --- .../accounts_payable_summary/accounts_payable_summary.js | 7 +++++++ .../accounts_receivable_summary.js | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index e46af2657b5..ac9d5bfbd01 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -23,6 +23,13 @@ frappe.query_reports["Accounts Payable Summary"] = { options: "Posting Date\nDue Date", default: "Due Date", }, + { + fieldname: "calculate_ageing_with", + label: __("Calculate Ageing With"), + fieldtype: "Select", + options: "Report Date\nToday Date", + default: "Report Date", + }, { fieldname: "range", label: __("Ageing Range"), diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 17ee5e0b323..ae0bddaa766 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -23,6 +23,13 @@ frappe.query_reports["Accounts Receivable Summary"] = { options: "Posting Date\nDue Date", default: "Due Date", }, + { + fieldname: "calculate_ageing_with", + label: __("Calculate Ageing With"), + fieldtype: "Select", + options: "Report Date\nToday Date", + default: "Report Date", + }, { fieldname: "range", label: __("Ageing Range"), From 817bcc78a0c3fe7c7f90aa7f83b1abb71b212d66 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 7 Jul 2025 19:00:42 +0530 Subject: [PATCH 25/58] fix: prevent creation of root accounts in account tree view (cherry picked from commit 3600f2f91bb2662e640234997d7ec6499bef6410) --- erpnext/accounts/doctype/account/account_tree.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index 0641612c615..183049c8dfc 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -252,6 +252,10 @@ frappe.treeview_settings["Account"] = { root_company, ]); } else { + const node = treeview.tree.get_selected_node(); + if (node.is_root) { + frappe.throw(__("Cannot create root account.")); + } treeview.new_node(); } }, @@ -270,7 +274,8 @@ frappe.treeview_settings["Account"] = { ].treeview.page.fields_dict.root_company.get_value() || frappe.flags.ignore_root_company_validation) && node.expandable && - !node.hide_add + !node.hide_add && + !node.is_root ); }, click: function () { From b5c4f61fef3752c3f0966bd2be9180276e95cea9 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 30 Jun 2025 12:31:39 +0530 Subject: [PATCH 26/58] fix: fetch item tax template after setting `base_net_rate` (cherry picked from commit db654d5e59ebc5e9598df5aea2309522bae7dc34) --- erpnext/public/js/controllers/transaction.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 76ff4e8c49d..26d0f67267e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -42,6 +42,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } item.base_rate_with_margin = item.rate_with_margin * flt(frm.doc.conversion_rate); + cur_frm.cscript.set_gross_profit(item); + cur_frm.cscript.calculate_taxes_and_totals(); + cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn); + if (item.item_code && item.rate) { frappe.call({ method: "erpnext.stock.get_item_details.get_item_tax_template", @@ -63,10 +67,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } }); } - - cur_frm.cscript.set_gross_profit(item); - cur_frm.cscript.calculate_taxes_and_totals(); - cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn); }); frappe.ui.form.on(this.frm.cscript.tax_table, "rate", function(frm, cdt, cdn) { From ee6ef03e24fecb134b35422b0d7d46a93e30dea9 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 11 Jul 2025 21:15:01 +0530 Subject: [PATCH 27/58] fix: field name of price_list in material request (cherry picked from commit adb9a6bc159b4f68e2acf2ba34e1325e8eb70f19) # Conflicts: # erpnext/patches.txt # erpnext/stock/doctype/material_request/material_request.py # erpnext/stock/doctype/packed_item/packed_item.py --- erpnext/patches.txt | 6 ++++++ .../v15_0/rename_price_list_to_buying_price_list.py | 11 +++++++++++ erpnext/public/js/controllers/buying.js | 1 - .../doctype/material_request/material_request.js | 8 ++++---- .../doctype/material_request/material_request.json | 6 +++--- .../doctype/material_request/material_request.py | 7 +++++++ erpnext/stock/doctype/packed_item/packed_item.py | 6 +++++- 7 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 erpnext/patches/v15_0/rename_price_list_to_buying_price_list.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 71cb012c1b8..acb4306cbb0 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -411,4 +411,10 @@ erpnext.patches.v14_0.update_full_name_in_contract erpnext.patches.v15_0.drop_sle_indexes erpnext.patches.v15_0.update_pick_list_fields erpnext.patches.v15_0.update_pegged_currencies +<<<<<<< HEAD erpnext.patches.v15_0.set_company_on_pos_inv_merge_log +======= +erpnext.patches.v15_0.set_status_cancelled_on_cancelled_pos_opening_entry_and_pos_closing_entry +erpnext.patches.v15_0.set_company_on_pos_inv_merge_log +erpnext.patches.v15_0.rename_price_list_to_buying_price_list +>>>>>>> adb9a6bc15 (fix: field name of price_list in material request) diff --git a/erpnext/patches/v15_0/rename_price_list_to_buying_price_list.py b/erpnext/patches/v15_0/rename_price_list_to_buying_price_list.py new file mode 100644 index 00000000000..2b6dbb77b9a --- /dev/null +++ b/erpnext/patches/v15_0/rename_price_list_to_buying_price_list.py @@ -0,0 +1,11 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + if frappe.db.has_column("Material Request", "price_list"): + rename_field( + "Material Request", + "price_list", + "buying_price_list", + ) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 2162c000221..8b5c8e0fe59 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -582,7 +582,6 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { ignore_pricing_rule: frm.doc.ignore_pricing_rule, doctype: frm.doc.doctype }, - price_list: frm.doc.price_list, }, freeze: true, callback: function(r) { diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 8f5d08fe946..5208b947288 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -43,7 +43,7 @@ frappe.ui.form.on("Material Request", { }; }); - frm.set_query("price_list", () => { + frm.set_query("buying_price_list", () => { return { filters: { buying: 1, @@ -79,7 +79,7 @@ frappe.ui.form.on("Material Request", { }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); - frm.doc.price_list = frappe.defaults.get_default("buying_price_list"); + frm.doc.buying_price_list = frappe.defaults.get_default("buying_price_list"); }, company: function (frm) { @@ -255,8 +255,8 @@ frappe.ui.form.on("Material Request", { from_warehouse: item.from_warehouse, warehouse: item.warehouse, doctype: frm.doc.doctype, - buying_price_list: frm.doc.price_list - ? frm.doc.price_list + buying_price_list: frm.doc.buying_price_list + ? frm.doc.buying_price_list : frappe.defaults.get_default("buying_price_list"), currency: frappe.defaults.get_default("Currency"), name: frm.doc.name, diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 4079288009a..43cf7a7e527 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -16,7 +16,7 @@ "column_break_2", "transaction_date", "schedule_date", - "price_list", + "buying_price_list", "amended_from", "warehouse_section", "scan_barcode", @@ -354,7 +354,7 @@ "fieldtype": "Column Break" }, { - "fieldname": "price_list", + "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List" @@ -364,7 +364,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2025-07-07 13:15:28.615984", + "modified": "2025-07-11 21:03:26.588307", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 8c80f907093..c73da1f4a5d 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -153,8 +153,15 @@ class MaterialRequest(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") +<<<<<<< HEAD if not self.price_list: self.price_list = frappe.defaults.get_defaults().buying_price_list +======= + self.validate_pp_qty() + + if not self.buying_price_list: + self.buying_price_list = frappe.defaults.get_defaults().buying_price_list +>>>>>>> adb9a6bc15 (fix: field name of price_list in material request) def before_update_after_submit(self): self.validate_schedule_date() diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 3d3bfc7e010..fd146b0d83b 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -340,8 +340,13 @@ def on_doctype_update(): @frappe.whitelist() +<<<<<<< HEAD def get_items_from_product_bundle(row, price_list): row, items = json.loads(row), [] +======= +def get_items_from_product_bundle(row): + row, items = ItemDetailsCtx(json.loads(row)), [] +>>>>>>> adb9a6bc15 (fix: field name of price_list in material request) bundled_items = get_product_bundle_items(row["item_code"]) for item in bundled_items: @@ -350,7 +355,6 @@ def get_items_from_product_bundle(row, price_list): "item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty), "conversion_rate": 1, - "price_list": price_list, "currency": frappe.defaults.get_defaults().currency, } ) From 1322cc1378be8a57a8451fef655dc7a5f5b77e5c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 14 Jul 2025 16:25:29 +0530 Subject: [PATCH 28/58] fix: incorrect stock reco sle (cherry picked from commit 597d5aff0246ca7876e2d25056a3ff8f293a033b) --- .../stock_reconciliation.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index fee71460ee5..d35666ba690 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -1089,17 +1089,14 @@ class StockReconciliation(StockController): } ) - if ( - add_new_sle - and not frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, - "name", - ) - and not row.current_serial_and_batch_bundle + if add_new_sle and not frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, + "name", ): - self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) - row.reload() + if not not row.current_serial_and_batch_bundle: + self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) + row.reload() self.add_missing_stock_ledger_entry(row, voucher_detail_no, sle_creation) From a195152cc8514547d50ff57c1f0cb03c38a81a40 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 14 Jul 2025 16:36:29 +0530 Subject: [PATCH 29/58] fix: incorrect if condition (cherry picked from commit 668574e4f05eaf6f8cb9d5f67d8d810f8cd4d609) --- .../stock/doctype/stock_reconciliation/stock_reconciliation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index d35666ba690..cf789d5bca2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -1094,7 +1094,7 @@ class StockReconciliation(StockController): {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, "name", ): - if not not row.current_serial_and_batch_bundle: + if not row.current_serial_and_batch_bundle: self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) row.reload() From 81e244be55b29ba8190b42390ecaac2e89533401 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 11 Jul 2025 11:04:29 +0530 Subject: [PATCH 30/58] fix: gross margin not set in project on submission of stock entry (cherry picked from commit ec578ba2310ccb1b480ce0987f490950ea11f9c1) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4c3ffc157c6..37c0340d2bf 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -435,7 +435,9 @@ class StockEntry(StockController): additional_cost_amt = additional_costs[0][0] if additional_costs else 0 amount += additional_cost_amt - frappe.db.set_value("Project", self.project, "total_consumed_material_cost", amount) + project = frappe.get_doc("Project", self.project) + project.total_consumed_material_cost = amount + project.save() def validate_item(self): stock_items = self.get_stock_items() From e3ba4320d6eb1752795dc6f02ddc2e4bc3a5df35 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 14 Jul 2025 17:19:40 +0530 Subject: [PATCH 31/58] chore: resolve conflicts --- erpnext/stock/doctype/material_request/material_request.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index c73da1f4a5d..c37a783ca4d 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -153,15 +153,8 @@ class MaterialRequest(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") -<<<<<<< HEAD - if not self.price_list: - self.price_list = frappe.defaults.get_defaults().buying_price_list -======= - self.validate_pp_qty() - if not self.buying_price_list: self.buying_price_list = frappe.defaults.get_defaults().buying_price_list ->>>>>>> adb9a6bc15 (fix: field name of price_list in material request) def before_update_after_submit(self): self.validate_schedule_date() From d8212d98ca642cdaf1df0b3aff1fc4732aac95d1 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 14 Jul 2025 17:20:28 +0530 Subject: [PATCH 32/58] chore: resolve conflicts --- erpnext/patches.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index acb4306cbb0..447e264ad75 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -411,10 +411,5 @@ erpnext.patches.v14_0.update_full_name_in_contract erpnext.patches.v15_0.drop_sle_indexes erpnext.patches.v15_0.update_pick_list_fields erpnext.patches.v15_0.update_pegged_currencies -<<<<<<< HEAD -erpnext.patches.v15_0.set_company_on_pos_inv_merge_log -======= -erpnext.patches.v15_0.set_status_cancelled_on_cancelled_pos_opening_entry_and_pos_closing_entry erpnext.patches.v15_0.set_company_on_pos_inv_merge_log erpnext.patches.v15_0.rename_price_list_to_buying_price_list ->>>>>>> adb9a6bc15 (fix: field name of price_list in material request) From c39993a3ba0939e709a502922d04e5eca729dd1d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 14 Jul 2025 17:22:00 +0530 Subject: [PATCH 33/58] chore: resolve conflicts --- erpnext/stock/doctype/packed_item/packed_item.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index fd146b0d83b..ceb2fdb0087 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -340,13 +340,8 @@ def on_doctype_update(): @frappe.whitelist() -<<<<<<< HEAD -def get_items_from_product_bundle(row, price_list): - row, items = json.loads(row), [] -======= def get_items_from_product_bundle(row): - row, items = ItemDetailsCtx(json.loads(row)), [] ->>>>>>> adb9a6bc15 (fix: field name of price_list in material request) + row, items = json.loads(row), [] bundled_items = get_product_bundle_items(row["item_code"]) for item in bundled_items: From c6d82b241e7d632e952ef48c996a454a2348fcbc Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 2 May 2025 13:46:46 +0530 Subject: [PATCH 34/58] refactor: using sql procedures for AR report - dynamic filters are passed (cherry picked from commit e5920c57aaf7d14e54b2d0fd36cf8796ef200a4c) --- .../accounts_receivable.py | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a2237ea9ac2..290424c2d87 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -128,6 +128,8 @@ class ReceivablePayableReport: elif self.ple_fetch_method == "UnBuffered Cursor": self.fetch_ple_in_unbuffered_cursor() + self.init_and_run_sql_procedures() + # Build delivery note map against all sales invoices self.build_delivery_note_map() @@ -318,6 +320,179 @@ class ReceivablePayableReport: row.paid -= amount row.paid_in_account_currency -= amount_in_account_currency + def init_and_run_sql_procedures(self): + # create in-memory temporary table for performance + frappe.db.sql("drop table if exists voucher_balance;") + # Memory storage engine doesn't support remarks - BLOB, TEXT types. + # Alternative? + frappe.db.sql( + """ + create temporary table voucher_balance( + name varchar(224), + voucher_type varchar(140), + voucher_no varchar(140), + party varchar(140), + party_account varchar(140), + posting_date date, + account_currency varchar(140), + invoiced decimal(21,9), + paid decimal(21,9), + credit_note decimal(21,9), + outstanding decimal(21,9), + invoiced_in_account_currency decimal(21,9), + paid_in_account_currency decimal(21,9), + credit_note_in_account_currency decimal(21,9), + outstanding_in_account_currency decimal(21,9)) engine=memory; + """ + ) + + # Only used for passing definitions to 'row type of' + frappe.db.sql("drop table if exists ple_row;") + frappe.db.sql( + """ + create temporary table ple_row( + name varchar(224), + account varchar(140), + voucher_type varchar(140), + voucher_no varchar(140), + against_voucher_type varchar(140), + against_voucher_no varchar(140), + party_type varchar(140), + cost_center varchar(140), + party varchar(140), + posting_date date, + due_date date, + account_currency varchar(140), + amount decimal(21,9), + amount_in_account_currency decimal(21,9)) engine=memory; + """ + ) + + # Generate hash from key + frappe.db.sql("drop function if exists genkey;") + frappe.db.sql( + """ + create function genkey(rec row type of ple_row, allocate bool) returns char(224) + begin + if allocate then + return sha2(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party), 224); + else + return sha2(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party), 224); + end if; + end + """ + ) + + # Init + frappe.db.sql("drop procedure if exists init;") + init_procedure = """ + create procedure init(in ple row type of `ple_row`) + begin + if not exists (select name from `voucher_balance` where name = genkey(ple, false)) + then + insert into `voucher_balance` values (genkey(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, 0, 0, 0, 0, 0, 0, 0, 0); + end if; + end; + """ + frappe.db.sql(init_procedure) + + # Allocate + frappe.db.sql("drop procedure if exists allocate;") + allocate_procedure = """ + create procedure allocate(in ple row type of `ple_row`) + begin + declare invoiced decimal(21,9) default 0; + declare invoiced_in_account_currency decimal(21,9) default 0; + declare paid decimal(21,9) default 0; + declare paid_in_account_currency decimal(21,9) default 0; + declare credit_note decimal(21,9) default 0; + declare credit_note_in_account_currency decimal(21,9) default 0; + + + if ple.amount > 0 then + if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + else + set invoiced = ple.amount; + set invoiced_in_account_currency = ple.amount_in_account_currency; + end if; + else + + if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then + if (ple.voucher_no = ple.against_voucher_no) then + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + else + set credit_note = -1 * ple.amount; + set credit_note_in_account_currency = -1 * ple.amount_in_account_currency; + end if; + else + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + end if; + + end if; + + insert into `voucher_balance` values (genkey(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, invoiced, paid, 0, 0, invoiced_in_account_currency, paid_in_account_currency, 0, 0); + end; + """ + frappe.db.sql(allocate_procedure) + + frappe.db.sql("drop procedure if exists build;") + build_balance = f""" + begin not atomic + declare done boolean default false; + declare rec1 row type of ple_row; + declare ple cursor for {self.ple_query.get_sql()}; + declare continue handler for not found set done = true; + + open ple; + fetch ple into rec1; + while not done do + call init(rec1); + fetch ple into rec1; + end while; + close ple; + + set done = false; + open ple; + fetch ple into rec1; + while not done do + call allocate(rec1); + fetch ple into rec1; + end while; + close ple; + end; + """ + frappe.db.sql(build_balance) + + res = frappe.db.sql( + """select + name, + voucher_type, + voucher_no, + party, + party_account, + posting_date, + account_currency, + sum(invoiced), + sum(paid), + sum(credit_note), + sum(invoiced) - sum(paid) - sum(credit_note), + sum(invoiced_in_account_currency), + sum(paid_in_account_currency), + sum(credit_note_in_account_currency), + sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) + from `voucher_balance` group by name;""" + ) + self.printv(res) + + def printv(self, res): + for x in res: + # if x[3] == "ACC-SINV-2025-00035": + print(x) + def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) From fee646fbe252f8b071ce65c9ab2b599f27a8e19a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Jun 2025 11:56:09 +0530 Subject: [PATCH 35/58] refactor: better readability (cherry picked from commit 097e74979f68b2361335e5d348085946841fa0be) --- .../accounts_receivable.py | 264 ++++++++++-------- 1 file changed, 140 insertions(+), 124 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 290424c2d87..46b23602e62 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -321,136 +321,19 @@ class ReceivablePayableReport: row.paid_in_account_currency -= amount_in_account_currency def init_and_run_sql_procedures(self): - # create in-memory temporary table for performance - frappe.db.sql("drop table if exists voucher_balance;") - # Memory storage engine doesn't support remarks - BLOB, TEXT types. - # Alternative? - frappe.db.sql( - """ - create temporary table voucher_balance( - name varchar(224), - voucher_type varchar(140), - voucher_no varchar(140), - party varchar(140), - party_account varchar(140), - posting_date date, - account_currency varchar(140), - invoiced decimal(21,9), - paid decimal(21,9), - credit_note decimal(21,9), - outstanding decimal(21,9), - invoiced_in_account_currency decimal(21,9), - paid_in_account_currency decimal(21,9), - credit_note_in_account_currency decimal(21,9), - outstanding_in_account_currency decimal(21,9)) engine=memory; - """ - ) + self.proc = InitSQLProceduresForAR() - # Only used for passing definitions to 'row type of' - frappe.db.sql("drop table if exists ple_row;") - frappe.db.sql( - """ - create temporary table ple_row( - name varchar(224), - account varchar(140), - voucher_type varchar(140), - voucher_no varchar(140), - against_voucher_type varchar(140), - against_voucher_no varchar(140), - party_type varchar(140), - cost_center varchar(140), - party varchar(140), - posting_date date, - due_date date, - account_currency varchar(140), - amount decimal(21,9), - amount_in_account_currency decimal(21,9)) engine=memory; - """ - ) - - # Generate hash from key - frappe.db.sql("drop function if exists genkey;") - frappe.db.sql( - """ - create function genkey(rec row type of ple_row, allocate bool) returns char(224) - begin - if allocate then - return sha2(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party), 224); - else - return sha2(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party), 224); - end if; - end - """ - ) - - # Init - frappe.db.sql("drop procedure if exists init;") - init_procedure = """ - create procedure init(in ple row type of `ple_row`) - begin - if not exists (select name from `voucher_balance` where name = genkey(ple, false)) - then - insert into `voucher_balance` values (genkey(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, 0, 0, 0, 0, 0, 0, 0, 0); - end if; - end; - """ - frappe.db.sql(init_procedure) - - # Allocate - frappe.db.sql("drop procedure if exists allocate;") - allocate_procedure = """ - create procedure allocate(in ple row type of `ple_row`) - begin - declare invoiced decimal(21,9) default 0; - declare invoiced_in_account_currency decimal(21,9) default 0; - declare paid decimal(21,9) default 0; - declare paid_in_account_currency decimal(21,9) default 0; - declare credit_note decimal(21,9) default 0; - declare credit_note_in_account_currency decimal(21,9) default 0; - - - if ple.amount > 0 then - if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then - set paid = -1 * ple.amount; - set paid_in_account_currency = -1 * ple.amount_in_account_currency; - else - set invoiced = ple.amount; - set invoiced_in_account_currency = ple.amount_in_account_currency; - end if; - else - - if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then - if (ple.voucher_no = ple.against_voucher_no) then - set paid = -1 * ple.amount; - set paid_in_account_currency = -1 * ple.amount_in_account_currency; - else - set credit_note = -1 * ple.amount; - set credit_note_in_account_currency = -1 * ple.amount_in_account_currency; - end if; - else - set paid = -1 * ple.amount; - set paid_in_account_currency = -1 * ple.amount_in_account_currency; - end if; - - end if; - - insert into `voucher_balance` values (genkey(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, invoiced, paid, 0, 0, invoiced_in_account_currency, paid_in_account_currency, 0, 0); - end; - """ - frappe.db.sql(allocate_procedure) - - frappe.db.sql("drop procedure if exists build;") build_balance = f""" begin not atomic declare done boolean default false; - declare rec1 row type of ple_row; + declare rec1 row type of `{self.proc._row_def_table_name}`; declare ple cursor for {self.ple_query.get_sql()}; declare continue handler for not found set done = true; open ple; fetch ple into rec1; while not done do - call init(rec1); + call {self.proc.init_procedure_name}(rec1); fetch ple into rec1; end while; close ple; @@ -459,7 +342,7 @@ class ReceivablePayableReport: open ple; fetch ple into rec1; while not done do - call allocate(rec1); + call {self.proc.allocate_procedure_name}(rec1); fetch ple into rec1; end while; close ple; @@ -468,7 +351,7 @@ class ReceivablePayableReport: frappe.db.sql(build_balance) res = frappe.db.sql( - """select + f"""select name, voucher_type, voucher_no, @@ -484,13 +367,12 @@ class ReceivablePayableReport: sum(paid_in_account_currency), sum(credit_note_in_account_currency), sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) - from `voucher_balance` group by name;""" + from `{self.proc._voucher_balance_name}` group by name;""" ) self.printv(res) def printv(self, res): for x in res: - # if x[3] == "ACC-SINV-2025-00035": print(x) def update_sub_total_row(self, row, party): @@ -1428,3 +1310,137 @@ def get_customer_group_with_children(customer_groups): frappe.throw(_("Customer Group: {0} does not exist").format(d)) return list(set(all_customer_groups)) + + +class InitSQLProceduresForAR: + """ + Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report + """ + + # Temporary Tables + _voucher_balance_name = "_voucher_balance" + _voucher_balance_definition = f""" + create temporary table `{_voucher_balance_name}`( + name varchar(224), + voucher_type varchar(140), + voucher_no varchar(140), + party varchar(140), + party_account varchar(140), + posting_date date, + account_currency varchar(140), + invoiced decimal(21,9), + paid decimal(21,9), + credit_note decimal(21,9), + outstanding decimal(21,9), + invoiced_in_account_currency decimal(21,9), + paid_in_account_currency decimal(21,9), + credit_note_in_account_currency decimal(21,9), + outstanding_in_account_currency decimal(21,9)) engine=memory; + """ + _row_def_table_name = "_ple_row" + _row_def_table_definition = f""" + create temporary table `{_row_def_table_name}`( + name varchar(224), + account varchar(140), + voucher_type varchar(140), + voucher_no varchar(140), + against_voucher_type varchar(140), + against_voucher_no varchar(140), + party_type varchar(140), + cost_center varchar(140), + party varchar(140), + posting_date date, + due_date date, + account_currency varchar(140), + amount decimal(21,9), + amount_in_account_currency decimal(21,9)) engine=memory; + """ + + # Function + genkey_function_name = "genkey" + genkey_function_sql = f""" + create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(224) + begin + if allocate then + return sha2(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party), 224); + else + return sha2(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party), 224); + end if; + end + """ + + # Procedures + init_procedure_name = "ar_init_tmp_table" + init_procedure_sql = f""" + create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`) + begin + if not exists (select name from `{_voucher_balance_name}` where name = genkey(ple, false)) + then + insert into `{_voucher_balance_name}` values (genkey(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, 0, 0, 0, 0, 0, 0, 0, 0); + end if; + end; + """ + + allocate_procedure_name = "ar_allocate_to_tmp_table" + allocate_procedure_sql = f""" + create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`) + begin + declare invoiced decimal(21,9) default 0; + declare invoiced_in_account_currency decimal(21,9) default 0; + declare paid decimal(21,9) default 0; + declare paid_in_account_currency decimal(21,9) default 0; + declare credit_note decimal(21,9) default 0; + declare credit_note_in_account_currency decimal(21,9) default 0; + + + if ple.amount > 0 then + if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + else + set invoiced = ple.amount; + set invoiced_in_account_currency = ple.amount_in_account_currency; + end if; + else + + if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then + if (ple.voucher_no = ple.against_voucher_no) then + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + else + set credit_note = -1 * ple.amount; + set credit_note_in_account_currency = -1 * ple.amount_in_account_currency; + end if; + else + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + end if; + + end if; + + insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, invoiced, paid, 0, 0, invoiced_in_account_currency, paid_in_account_currency, 0, 0); + end; + """ + + def __init__(self): + existing_procedures = frappe.db.sql( + f"select routine_name from information_schema.routines where routine_type in ('FUNCTION','PROCEDURE') and routine_schema='{frappe.conf.db_name}';" + ) + if existing_procedures: + # normalize + existing_procedures = [x[0] for x in existing_procedures] + + if self.genkey_function_name not in existing_procedures: + frappe.db.sql(self.genkey_function_sql) + + if self.init_procedure_name not in existing_procedures: + frappe.db.sql(self.init_procedure_sql) + + if self.allocate_procedure_name not in existing_procedures: + frappe.db.sql(self.allocate_procedure_sql) + + frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`") + frappe.db.sql(self._voucher_balance_definition) + + frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`") + frappe.db.sql(self._row_def_table_definition) From 92d58a4e4cf2cc0b0230c40953a6ef822630aa8a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Jun 2025 14:12:18 +0530 Subject: [PATCH 36/58] refactor: introduce sql option for data fetch (cherry picked from commit 8cf8f6abadc53132cc35ee00454994740aad0981) --- .../accounts/doctype/accounts_settings/accounts_settings.json | 2 +- erpnext/accounts/doctype/accounts_settings/accounts_settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 80b7d996101..abaabf9e6f0 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -552,7 +552,7 @@ "fieldname": "receivable_payable_fetch_method", "fieldtype": "Select", "label": "Data Fetch Method", - "options": "Buffered Cursor\nUnBuffered Cursor" + "options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL" }, { "fieldname": "accounts_receivable_payable_tuning_section", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 7959f163871..56105b1bf12 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -58,7 +58,7 @@ class AccountsSettings(Document): merge_similar_account_heads: DF.Check over_billing_allowance: DF.Currency post_change_gl_entries: DF.Check - receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"] + receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"] receivable_payable_remarks_length: DF.Int reconciliation_queue_size: DF.Int role_allowed_to_over_bill: DF.Link | None From 5d0d0c310200d4c24709a139345a82ff18c9ced8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Jun 2025 14:26:15 +0530 Subject: [PATCH 37/58] refactor: call procedures based on config (cherry picked from commit e90c6a33bd81227e610f051c3558bf29b6d979d9) --- .../accounts_receivable.py | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 46b23602e62..9afbd072d47 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -127,8 +127,8 @@ class ReceivablePayableReport: self.fetch_ple_in_buffered_cursor() elif self.ple_fetch_method == "UnBuffered Cursor": self.fetch_ple_in_unbuffered_cursor() - - self.init_and_run_sql_procedures() + elif self.ple_fetch_method == "Raw SQL": + self.init_and_run_sql_procedures() # Build delivery note map against all sales invoices self.build_delivery_note_map() @@ -356,24 +356,40 @@ class ReceivablePayableReport: voucher_type, voucher_no, party, - party_account, + party_account `account`, posting_date, account_currency, - sum(invoiced), - sum(paid), - sum(credit_note), - sum(invoiced) - sum(paid) - sum(credit_note), - sum(invoiced_in_account_currency), - sum(paid_in_account_currency), - sum(credit_note_in_account_currency), - sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) - from `{self.proc._voucher_balance_name}` group by name;""" + sum(invoiced) `invoiced`, + sum(paid) `paid`, + sum(credit_note) `credit_note`, + sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`, + sum(invoiced_in_account_currency) `invoiced_in_account_currency`, + sum(paid_in_account_currency) `paid_in_account_currency`, + sum(credit_note_in_account_currency) `credit_note_in_account_currency`, + sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency` + from `{self.proc._voucher_balance_name}` group by name;""", + as_dict=True, ) - self.printv(res) - - def printv(self, res): for x in res: - print(x) + if self.filters.get("ignore_accounts"): + key = (x.voucher_type, x.voucher_no, x.party) + else: + key = (x.account, x.voucher_type, x.voucher_no, x.party) + + _d = self.build_voucher_dict(x) + for field in [ + "invoiced", + "paid", + "credit_note", + "outstanding", + "invoiced_in_account_currency", + "paid_in_account_currency", + "credit_note_in_account_currency", + "outstanding_in_account_currency", + ]: + _d[field] = x.get(field) + + self.voucher_balance[key] = _d def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) From a173c778591ee341b1e6eb1b786601940d96923d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Jun 2025 14:30:31 +0530 Subject: [PATCH 38/58] refactor: order by posting date (cherry picked from commit 7b7440d44a74a0d9aa035a2aed9f32a49f5ef5da) --- .../accounts/report/accounts_receivable/accounts_receivable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 9afbd072d47..f0273a05e70 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -367,7 +367,7 @@ class ReceivablePayableReport: sum(paid_in_account_currency) `paid_in_account_currency`, sum(credit_note_in_account_currency) `credit_note_in_account_currency`, sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency` - from `{self.proc._voucher_balance_name}` group by name;""", + from `{self.proc._voucher_balance_name}` group by name order by posting_date;""", as_dict=True, ) for x in res: From 1afb27231c6f8b3a71567b6ba48103d992827d57 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Jun 2025 14:47:01 +0530 Subject: [PATCH 39/58] refactor: utility to drop existing procedures and include cost center (cherry picked from commit da32bb5f514783a9b8e350375d958ede39ca6b72) --- .../accounts_settings/accounts_settings.js | 11 +++++++++ .../accounts_settings/accounts_settings.json | 13 +++++++++++ .../accounts_settings/accounts_settings.py | 8 +++++++ .../accounts_receivable.py | 23 +++++++++++-------- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js index ba577f2b8c9..931e05a716b 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js @@ -26,9 +26,20 @@ frappe.ui.form.on("Accounts Settings", { add_taxes_from_taxes_and_charges_template(frm) { toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template"); }, + add_taxes_from_item_tax_template(frm) { toggle_tax_settings(frm, "add_taxes_from_item_tax_template"); }, + + drop_ar_procedures: function (frm) { + frm.call({ + doc: frm.doc, + method: "drop_ar_sql_procedures", + callback: function (r) { + frappe.show_alert(__("Procedures dropped"), 5); + }, + }); + }, }); function toggle_tax_settings(frm, field_name) { diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index abaabf9e6f0..946e8d1a5cc 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -88,6 +88,8 @@ "receivable_payable_remarks_length", "accounts_receivable_payable_tuning_section", "receivable_payable_fetch_method", + "column_break_ntmi", + "drop_ar_procedures", "legacy_section", "ignore_is_opening_check_for_reporting", "payment_request_settings", @@ -609,6 +611,17 @@ "fieldname": "add_taxes_from_taxes_and_charges_template", "fieldtype": "Check", "label": "Automatically Add Taxes from Taxes and Charges Template" + }, + { + "fieldname": "column_break_ntmi", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"", + "description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report", + "fieldname": "drop_ar_procedures", + "fieldtype": "Button", + "label": "Drop Procedures" } ], "icon": "icon-cog", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 56105b1bf12..362b235b2f6 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -152,3 +152,11 @@ class AccountsSettings(Document): ), title=_("Auto Tax Settings Error"), ) + + @frappe.whitelist() + def drop_ar_sql_procedures(self): + from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR + + frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}") + frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}") + frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}") diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index f0273a05e70..7b0f071b223 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -359,6 +359,7 @@ class ReceivablePayableReport: party_account `account`, posting_date, account_currency, + cost_center, sum(invoiced) `invoiced`, sum(paid) `paid`, sum(credit_note) `credit_note`, @@ -386,6 +387,7 @@ class ReceivablePayableReport: "paid_in_account_currency", "credit_note_in_account_currency", "outstanding_in_account_currency", + "cost_center", ]: _d[field] = x.get(field) @@ -1344,14 +1346,13 @@ class InitSQLProceduresForAR: party_account varchar(140), posting_date date, account_currency varchar(140), + cost_center varchar(140), invoiced decimal(21,9), paid decimal(21,9), credit_note decimal(21,9), - outstanding decimal(21,9), invoiced_in_account_currency decimal(21,9), paid_in_account_currency decimal(21,9), - credit_note_in_account_currency decimal(21,9), - outstanding_in_account_currency decimal(21,9)) engine=memory; + credit_note_in_account_currency decimal(21,9)) engine=memory; """ _row_def_table_name = "_ple_row" _row_def_table_definition = f""" @@ -1392,7 +1393,7 @@ class InitSQLProceduresForAR: begin if not exists (select name from `{_voucher_balance_name}` where name = genkey(ple, false)) then - insert into `{_voucher_balance_name}` values (genkey(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, 0, 0, 0, 0, 0, 0, 0, 0); + insert into `{_voucher_balance_name}` values (genkey(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0); end if; end; """ @@ -1434,17 +1435,21 @@ class InitSQLProceduresForAR: end if; - insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, invoiced, paid, 0, 0, invoiced_in_account_currency, paid_in_account_currency, 0, 0); + insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0); end; """ - def __init__(self): - existing_procedures = frappe.db.sql( + def get_existing_procedures(self): + procedures = frappe.db.sql( f"select routine_name from information_schema.routines where routine_type in ('FUNCTION','PROCEDURE') and routine_schema='{frappe.conf.db_name}';" ) - if existing_procedures: + if procedures: # normalize - existing_procedures = [x[0] for x in existing_procedures] + procedures = [x[0] for x in procedures] + return procedures + + def __init__(self): + existing_procedures = self.get_existing_procedures() if self.genkey_function_name not in existing_procedures: frappe.db.sql(self.genkey_function_sql) From 2d2ca049fadc30653ba83a081c956ef1e9434107 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 5 Jun 2025 11:24:28 +0530 Subject: [PATCH 40/58] refactor: prefix-ed names for easy distinction (cherry picked from commit c5e35cc330b18534a32b5452e77e6adec67ff3e4) --- .../report/accounts_receivable/accounts_receivable.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 7b0f071b223..5214d6c9acf 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1336,7 +1336,7 @@ class InitSQLProceduresForAR: """ # Temporary Tables - _voucher_balance_name = "_voucher_balance" + _voucher_balance_name = "_ar_voucher_balance" _voucher_balance_definition = f""" create temporary table `{_voucher_balance_name}`( name varchar(224), @@ -1354,7 +1354,7 @@ class InitSQLProceduresForAR: paid_in_account_currency decimal(21,9), credit_note_in_account_currency decimal(21,9)) engine=memory; """ - _row_def_table_name = "_ple_row" + _row_def_table_name = "_ar_ple_row" _row_def_table_definition = f""" create temporary table `{_row_def_table_name}`( name varchar(224), @@ -1374,7 +1374,7 @@ class InitSQLProceduresForAR: """ # Function - genkey_function_name = "genkey" + genkey_function_name = "ar_genkey" genkey_function_sql = f""" create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(224) begin @@ -1391,9 +1391,9 @@ class InitSQLProceduresForAR: init_procedure_sql = f""" create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`) begin - if not exists (select name from `{_voucher_balance_name}` where name = genkey(ple, false)) + if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false)) then - insert into `{_voucher_balance_name}` values (genkey(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0); + insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0); end if; end; """ From d9b36ea37c76a68e8c0bf1dadd1fb14d920c53ee Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 5 Jun 2025 11:29:48 +0530 Subject: [PATCH 41/58] refactor: better variable name (cherry picked from commit 1a90c0d031b01d462c87d8448e73af9e89df29a6) --- .../report/accounts_receivable/accounts_receivable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 5214d6c9acf..d5071f72e72 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -350,7 +350,7 @@ class ReceivablePayableReport: """ frappe.db.sql(build_balance) - res = frappe.db.sql( + balances = frappe.db.sql( f"""select name, voucher_type, @@ -371,7 +371,7 @@ class ReceivablePayableReport: from `{self.proc._voucher_balance_name}` group by name order by posting_date;""", as_dict=True, ) - for x in res: + for x in balances: if self.filters.get("ignore_accounts"): key = (x.voucher_type, x.voucher_no, x.party) else: From 0bf5d3dae31ce20b5d1aae552a33e79b8274d93d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Jun 2025 14:49:03 +0530 Subject: [PATCH 42/58] refactor: dynamic DB field types (cherry picked from commit 9d0ebe3427b0a0bf7af686c02177e20649cbbbc5) --- .../accounts_receivable.py | 72 ++++++++++--------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index d5071f72e72..454c7e1fb20 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -6,6 +6,7 @@ from collections import OrderedDict import frappe from frappe import _, qb, query_builder, scrub +from frappe.database.schema import get_definition from frappe.desk.reportview import build_match_conditions from frappe.query_builder import Criterion from frappe.query_builder.functions import Date, Substring, Sum @@ -1335,53 +1336,56 @@ class InitSQLProceduresForAR: Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report """ + _varchar_type = get_definition("Data") + _currency_type = get_definition("Currency") # Temporary Tables _voucher_balance_name = "_ar_voucher_balance" _voucher_balance_definition = f""" create temporary table `{_voucher_balance_name}`( - name varchar(224), - voucher_type varchar(140), - voucher_no varchar(140), - party varchar(140), - party_account varchar(140), + name {_varchar_type}, + voucher_type {_varchar_type}, + voucher_no {_varchar_type}, + party {_varchar_type}, + party_account {_varchar_type}, posting_date date, - account_currency varchar(140), - cost_center varchar(140), - invoiced decimal(21,9), - paid decimal(21,9), - credit_note decimal(21,9), - invoiced_in_account_currency decimal(21,9), - paid_in_account_currency decimal(21,9), - credit_note_in_account_currency decimal(21,9)) engine=memory; + account_currency {_varchar_type}, + cost_center {_varchar_type}, + invoiced {_currency_type}, + paid {_currency_type}, + credit_note {_currency_type}, + invoiced_in_account_currency {_currency_type}, + paid_in_account_currency {_currency_type}, + credit_note_in_account_currency {_currency_type}) engine=memory; """ + _row_def_table_name = "_ar_ple_row" _row_def_table_definition = f""" create temporary table `{_row_def_table_name}`( - name varchar(224), - account varchar(140), - voucher_type varchar(140), - voucher_no varchar(140), - against_voucher_type varchar(140), - against_voucher_no varchar(140), - party_type varchar(140), - cost_center varchar(140), - party varchar(140), + name {_varchar_type}, + account {_varchar_type}, + voucher_type {_varchar_type}, + voucher_no {_varchar_type}, + against_voucher_type {_varchar_type}, + against_voucher_no {_varchar_type}, + party_type {_varchar_type}, + cost_center {_varchar_type}, + party {_varchar_type}, posting_date date, due_date date, - account_currency varchar(140), - amount decimal(21,9), - amount_in_account_currency decimal(21,9)) engine=memory; + account_currency {_varchar_type}, + amount {_currency_type}, + amount_in_account_currency {_currency_type}) engine=memory; """ # Function genkey_function_name = "ar_genkey" genkey_function_sql = f""" - create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(224) + create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40) begin if allocate then - return sha2(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party), 224); + return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party)); else - return sha2(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party), 224); + return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party)); end if; end """ @@ -1402,12 +1406,12 @@ class InitSQLProceduresForAR: allocate_procedure_sql = f""" create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`) begin - declare invoiced decimal(21,9) default 0; - declare invoiced_in_account_currency decimal(21,9) default 0; - declare paid decimal(21,9) default 0; - declare paid_in_account_currency decimal(21,9) default 0; - declare credit_note decimal(21,9) default 0; - declare credit_note_in_account_currency decimal(21,9) default 0; + declare invoiced {_currency_type} default 0; + declare invoiced_in_account_currency {_currency_type} default 0; + declare paid {_currency_type} default 0; + declare paid_in_account_currency {_currency_type} default 0; + declare credit_note {_currency_type} default 0; + declare credit_note_in_account_currency {_currency_type} default 0; if ple.amount > 0 then From 1d18a3b1a38f4fb0c36761133d29896ae0a5223d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 3 Jul 2025 15:18:53 +0530 Subject: [PATCH 43/58] chore: drop unused utility method (cherry picked from commit 52c0df24e3caf58ac81e87dc91930a394983c041) --- .../report/accounts_receivable/accounts_receivable.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 454c7e1fb20..8d7e1e270e0 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1443,17 +1443,8 @@ class InitSQLProceduresForAR: end; """ - def get_existing_procedures(self): - procedures = frappe.db.sql( - f"select routine_name from information_schema.routines where routine_type in ('FUNCTION','PROCEDURE') and routine_schema='{frappe.conf.db_name}';" - ) - if procedures: - # normalize - procedures = [x[0] for x in procedures] - return procedures - def __init__(self): - existing_procedures = self.get_existing_procedures() + existing_procedures = frappe.db.get_routines() if self.genkey_function_name not in existing_procedures: frappe.db.sql(self.genkey_function_sql) From 72b968474285390ff39dd6be1f6cc231a73a9ca3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 3 Jul 2025 15:41:14 +0530 Subject: [PATCH 44/58] refactor: build and pass match conditions as qb criterion (cherry picked from commit 7efeed54de47a0b52b1a8081ea2aee7c2831277d) --- .../accounts_receivable.py | 21 +++++++------------ erpnext/accounts/utils.py | 17 +++++++++++++++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 8d7e1e270e0..b1c2c10441c 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -7,7 +7,6 @@ from collections import OrderedDict import frappe from frappe import _, qb, query_builder, scrub from frappe.database.schema import get_definition -from frappe.desk.reportview import build_match_conditions from frappe.query_builder import Criterion from frappe.query_builder.functions import Date, Substring, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate @@ -17,6 +16,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_dimension_with_children, ) from erpnext.accounts.utils import ( + build_qb_match_conditions, get_advance_payment_doctypes, get_currency_precision, get_party_types_from_account_type, @@ -137,8 +137,7 @@ class ReceivablePayableReport: self.build_data() def fetch_ple_in_buffered_cursor(self): - query, param = self.ple_query - self.ple_entries = frappe.db.sql(query, param, as_dict=True) + self.ple_entries = self.ple_query.run(as_dict=True) for ple in self.ple_entries: self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding @@ -151,9 +150,8 @@ class ReceivablePayableReport: def fetch_ple_in_unbuffered_cursor(self): self.ple_entries = [] - query, param = self.ple_query with frappe.db.unbuffered_cursor(): - for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True): + for ple in self.ple_query.run(as_dict=True, as_iterator=True): self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding self.ple_entries.append(ple) @@ -937,18 +935,15 @@ class ReceivablePayableReport: else: query = query.select(ple.remarks) - query, param = query.walk() - - match_conditions = build_match_conditions("Payment Ledger Entry") - if match_conditions: - query += " AND " + match_conditions + if match_conditions := build_qb_match_conditions("Payment Ledger Entry"): + query = query.where(Criterion.all(match_conditions)) if self.filters.get("group_by_party"): - query += f" ORDER BY `{self.ple.party.name}`, `{self.ple.posting_date.name}`" + query = query.orderby(self.ple.party, self.ple.posting_date) else: - query += f" ORDER BY `{self.ple.posting_date.name}`, `{self.ple.party.name}`" + query = query.orderby(self.ple.posting_date, self.ple.party) - self.ple_query = (query, param) + self.ple_query = query def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index c775773cd71..9132cb15fd9 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Optional import frappe import frappe.defaults from frappe import _, qb, throw +from frappe.desk.reportview import build_match_conditions from frappe.model.meta import get_field_precision from frappe.query_builder import AliasedQuery, Case, Criterion, Table from frappe.query_builder.functions import Count, Max, Sum @@ -2347,3 +2348,19 @@ def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15): "frequency": "Cron", } ).save() + + +def build_qb_match_conditions(doctype, user=None) -> list: + match_filters = build_match_conditions(doctype, user, False) + criterion = [] + if match_filters: + from frappe import qb + + _dt = qb.DocType(doctype) + + for filter in match_filters: + for d, names in filter.items(): + fieldname = d.lower().replace(" ", "_") + criterion.append(_dt[fieldname].isin(names)) + + return criterion From 3b38c6708c5df6e2c1be38813de199a580fd0a3f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 9 Jul 2025 10:45:58 +0530 Subject: [PATCH 45/58] chore: rename method (cherry picked from commit fc8ca7d82cbc1bd68d9574fba20f8a608907804c) --- .../report/accounts_receivable/accounts_receivable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index b1c2c10441c..6061be9f3a8 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -129,7 +129,7 @@ class ReceivablePayableReport: elif self.ple_fetch_method == "UnBuffered Cursor": self.fetch_ple_in_unbuffered_cursor() elif self.ple_fetch_method == "Raw SQL": - self.init_and_run_sql_procedures() + self.fetch_ple_in_sql_procedures() # Build delivery note map against all sales invoices self.build_delivery_note_map() @@ -319,7 +319,7 @@ class ReceivablePayableReport: row.paid -= amount row.paid_in_account_currency -= amount_in_account_currency - def init_and_run_sql_procedures(self): + def fetch_ple_in_sql_procedures(self): self.proc = InitSQLProceduresForAR() build_balance = f""" From 0e67487508d74a34cc315683d050cf88bd76e9be Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:42:30 +0200 Subject: [PATCH 46/58] fix(Employee): add context to status in List View (backport #48576) (#48577) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> fix(Employee): add context to status in List View (#48576) --- erpnext/setup/doctype/employee/employee_list.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee_list.js b/erpnext/setup/doctype/employee/employee_list.js index 0c97b626954..b50eb381c95 100644 --- a/erpnext/setup/doctype/employee/employee_list.js +++ b/erpnext/setup/doctype/employee/employee_list.js @@ -2,8 +2,10 @@ frappe.listview_settings["Employee"] = { add_fields: ["status", "branch", "department", "designation", "image"], filters: [["status", "=", "Active"]], get_indicator: function (doc) { - var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; - indicator[1] = { Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status]; - return indicator; + return [ + __(doc.status, null, "Employee"), + { Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status], + "status,=," + doc.status, + ]; }, }; From c20a5b01b4df7d36154023a5592a4384eda25aed Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 14 Jul 2025 21:47:00 +0200 Subject: [PATCH 47/58] fix: make labels in serial_batch_prompt translatable (cherry picked from commit 8757800888722d005e76f83d0d9f274ab6440d02) --- .../serial_and_batch_bundle/serial_and_batch_bundle.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js index 404abbd21bc..a250df28350 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js @@ -55,14 +55,14 @@ frappe.ui.form.on("Serial and Batch Bundle", { let fields = frm.events.get_prompt_fields(frm); - frm.add_custom_button(__("Make " + label), () => { + frm.add_custom_button(__("Make {0}", [label]), () => { frappe.prompt( fields, (data) => { frm.events.add_serial_batch(frm, data); }, - "Add " + label, - "Make " + label + __("Add {0}", [label]), + __("Make {0}", [label]) ); }); } From 3488ba05eb2b3d32083b15c699893f46a136fe6b Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 15 Jul 2025 12:22:50 +0530 Subject: [PATCH 48/58] fix: split and set value after depreciation --- erpnext/assets/doctype/asset/asset.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 616680d07ba..649678d879f 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1215,6 +1215,10 @@ def update_existing_asset(asset, remaining_qty, new_asset_name): opening_accumulated_depreciation = flt( (asset.opening_accumulated_depreciation * remaining_qty) / asset.asset_quantity ) + value_after_depreciation = flt( + (asset.value_after_depreciation * remaining_qty) / asset.asset_quantity, + asset.precision("gross_purchase_amount"), + ) frappe.db.set_value( "Asset", @@ -1222,6 +1226,7 @@ def update_existing_asset(asset, remaining_qty, new_asset_name): { "opening_accumulated_depreciation": opening_accumulated_depreciation, "gross_purchase_amount": remaining_gross_purchase_amount, + "value_after_depreciation": value_after_depreciation, "asset_quantity": remaining_qty, }, ) @@ -1283,6 +1288,9 @@ def create_new_asset_after_split(asset, split_qty): new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation new_asset.asset_quantity = split_qty new_asset.split_from = asset.name + new_asset.value_after_depreciation = flt( + (asset.value_after_depreciation * split_qty) / asset.asset_quantity, asset.precision("gross_purchase_amount") + ) for row in new_asset.get("finance_books"): row.value_after_depreciation = flt((row.value_after_depreciation * split_qty) / asset.asset_quantity) From f35fd9842e3f285b607d1bb69b0d819a29c729cd Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 15 Jul 2025 13:30:35 +0530 Subject: [PATCH 49/58] fix: updated test --- erpnext/assets/doctype/asset/test_asset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 9c8db82f41b..5108f0931b7 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -487,6 +487,7 @@ class TestAsset(AssetSetup): self.assertEqual(new_asset.gross_purchase_amount, 24000) self.assertEqual(new_asset.opening_accumulated_depreciation, 4000) self.assertEqual(new_asset.split_from, asset.name) + self.assertEqual(new_asset.value_after_depreciation, 16000) self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 4000) self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 4000) From 68162f79a1e891b80b750b45912319c611ccfce2 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 15 Jul 2025 13:40:00 +0530 Subject: [PATCH 50/58] chore: run pre-commit --- erpnext/assets/doctype/asset/asset.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 649678d879f..019c97114fa 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1289,7 +1289,8 @@ def create_new_asset_after_split(asset, split_qty): new_asset.asset_quantity = split_qty new_asset.split_from = asset.name new_asset.value_after_depreciation = flt( - (asset.value_after_depreciation * split_qty) / asset.asset_quantity, asset.precision("gross_purchase_amount") + (asset.value_after_depreciation * split_qty) / asset.asset_quantity, + asset.precision("gross_purchase_amount"), ) for row in new_asset.get("finance_books"): From 9aef3058a6fd9e22e8a43b064e3d71b60d847e3a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 15 Jul 2025 11:55:03 +0530 Subject: [PATCH 51/58] perf: optimize code for subcontracting (cherry picked from commit bc6f69ad540b0ea205e808ec87fa7522b3ff5625) --- erpnext/controllers/subcontracting_controller.py | 6 +++--- .../subcontracting_order/subcontracting_order.py | 12 ++++++++---- .../subcontracting_receipt/subcontracting_receipt.py | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 0a9999df9df..ab5c1b3c69d 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -953,7 +953,7 @@ class SubcontractingController(StockController): ) sco_doc.update_ordered_qty_for_subcontracting(sco_item_rows) - sco_doc.update_reserved_qty_for_subcontracting() + sco_doc.update_reserved_qty_for_subcontracting(sco_item_rows) def make_sl_entries_for_supplier_warehouse(self, sl_entries): if hasattr(self, "supplied_items"): @@ -1046,7 +1046,7 @@ class SubcontractingController(StockController): return supplied_items_cost - def set_subcontracting_order_status(self): + def set_subcontracting_order_status(self, update_bin=True): if self.doctype == "Subcontracting Order": self.update_status() elif self.doctype == "Subcontracting Receipt": @@ -1055,7 +1055,7 @@ class SubcontractingController(StockController): if self.subcontract_orders: for sco in set(self.subcontract_orders): sco_doc = frappe.get_doc("Subcontracting Order", sco) - sco_doc.update_status() + sco_doc.update_status(update_bin=update_bin) def calculate_additional_costs(self): self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs")) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index e8bee897840..e9848c88952 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -236,8 +236,11 @@ class SubcontractingOrder(SubcontractingController): return flt(query[0][0]) if query else 0 - def update_reserved_qty_for_subcontracting(self): + def update_reserved_qty_for_subcontracting(self, sco_item_rows=None): for item in self.supplied_items: + if sco_item_rows and item.reference_name not in sco_item_rows: + continue + if item.rm_item_code: stock_bin = get_bin(item.rm_item_code, item.reserve_warehouse) stock_bin.update_reserved_qty_for_sub_contracting() @@ -299,7 +302,7 @@ class SubcontractingOrder(SubcontractingController): self.set_missing_values() - def update_status(self, status=None, update_modified=True): + def update_status(self, status=None, update_modified=True, update_bin=True): if self.status == "Closed" and self.status != status: check_on_hold_or_closed_status("Purchase Order", self.purchase_order) @@ -329,8 +332,9 @@ class SubcontractingOrder(SubcontractingController): self.db_set("status", status, update_modified=update_modified) self.update_requested_qty() - self.update_ordered_qty_for_subcontracting() - self.update_reserved_qty_for_subcontracting() + if update_bin: + self.update_ordered_qty_for_subcontracting() + self.update_reserved_qty_for_subcontracting() def update_subcontracted_quantity_in_po(self, cancel=False): for service_item in self.service_items: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 01f5df3b684..00c1c08cd5c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -152,7 +152,7 @@ class SubcontractingReceipt(SubcontractingController): self.validate_available_qty_for_consumption() self.update_status_updater_args() self.update_prevdoc_status() - self.set_subcontracting_order_status() + self.set_subcontracting_order_status(update_bin=False) self.set_consumed_qty_in_subcontract_order() for table_name in ["items", "supplied_items"]: @@ -179,7 +179,7 @@ class SubcontractingReceipt(SubcontractingController): self.update_status_updater_args() self.update_prevdoc_status() self.set_consumed_qty_in_subcontract_order() - self.set_subcontracting_order_status() + self.set_subcontracting_order_status(update_bin=False) self.update_stock_ledger() self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() From 4383d29d7b2a3cad3d88c82ecdc049da14c51126 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 15 Jul 2025 14:34:00 +0530 Subject: [PATCH 52/58] fix: set value after depreciation when creating test asset --- erpnext/assets/doctype/asset/test_asset.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 5108f0931b7..571b8734991 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1707,6 +1707,9 @@ def create_asset(**args): "is_composite_asset": args.is_composite_asset or 0, "asset_quantity": args.get("asset_quantity") or 1, "depr_entry_posting_status": args.depr_entry_posting_status or "", + "value_after_depreciation": ( + (args.gross_purchase_amount or 100000) - (args.opening_accumulated_depreciation or 0) + ), } ) From 4b6444e93bf39029f943a3fc5c8ffa9430646bd0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 15 Jul 2025 14:00:04 +0530 Subject: [PATCH 53/58] fix: system was allowing credit notes with serial numbers for any customer (cherry picked from commit e0730758343f3a53e58b7874e891369d62f2174f) # Conflicts: # erpnext/stock/doctype/delivery_note/delivery_note.py # erpnext/stock/doctype/serial_no/serial_no.json --- .../doctype/sales_invoice/sales_invoice.py | 1 + .../controllers/sales_and_purchase_return.py | 11 +++++++ erpnext/controllers/selling_controller.py | 29 +++++++++++++++++++ .../doctype/delivery_note/delivery_note.py | 6 ++++ .../stock/doctype/serial_no/serial_no.json | 15 ++++++++++ erpnext/stock/doctype/serial_no/serial_no.py | 1 + erpnext/stock/serial_batch_bundle.py | 7 ++++- 7 files changed, 69 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index b12f51e8a4b..829428eec86 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -461,6 +461,7 @@ class SalesInvoice(SellingController): self.make_bundle_for_sales_purchase_return(table_name) self.make_bundle_using_old_serial_batch_fields(table_name) + self.validate_standalone_serial_nos_customer() self.update_stock_reservation_entries() self.update_stock_ledger() diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index cb359dc5a71..011f21fe388 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -36,6 +36,17 @@ def validate_return_against(doc): party_type = "customer" if doc.doctype in ("Sales Invoice", "Delivery Note") else "supplier" + if ref_doc.get(party_type) != doc.get(party_type): + frappe.throw( + _("The {0} {1} does not match with the {0} {2} in the {3} {4}").format( + doc.meta.get_label(party_type), + doc.get(party_type), + ref_doc.get(party_type), + ref_doc.doctype, + ref_doc.name, + ) + ) + if ( ref_doc.company == doc.company and ref_doc.get(party_type) == doc.get(party_type) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 9089b5a2829..5f7cfb165d4 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -57,6 +57,35 @@ class SellingController(StockController): if self.get(table_field): self.set_serial_and_batch_bundle(table_field) + def validate_standalone_serial_nos_customer(self): + if not self.is_return or self.return_against: + return + + if self.doctype in ["Sales Invoice", "Delivery Note"]: + bundle_ids = [d.serial_and_batch_bundle for d in self.get("items") if d.serial_and_batch_bundle] + if not bundle_ids: + return + + serial_nos = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", bundle_ids)}, + pluck="serial_no", + ) + + if serial_nos := frappe.get_all( + "Serial No", + filters={"name": ("in", serial_nos), "customer": ("is", "set")}, + fields=["name", "customer"], + ): + for sn in serial_nos: + if sn.customer and sn.customer != self.customer: + frappe.throw( + _( + "Serial No {0} is already assigned to customer {1}. Can only be returned against the customer {1}" + ).format(frappe.bold(sn.name), frappe.bold(sn.customer)), + title=_("Serial No Already Assigned"), + ) + def set_missing_values(self, for_validate=False): super().set_missing_values(for_validate) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 37e2737391a..605cf2edb85 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -466,6 +466,12 @@ class DeliveryNote(SellingController): self.make_bundle_for_sales_purchase_return(table_name) self.make_bundle_using_old_serial_batch_fields(table_name) +<<<<<<< HEAD +======= + self.validate_standalone_serial_nos_customer() + self.update_stock_reservation_entries() + +>>>>>>> e073075834 (fix: system was allowing credit notes with serial numbers for any customer) # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 89c77d46b1e..021847d5fa5 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -15,6 +15,7 @@ "batch_no", "warehouse", "purchase_rate", + "customer", "column_break1", "status", "item_name", @@ -267,12 +268,25 @@ "label": "Creation Document No", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "no_copy": 1, + "options": "Customer", + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-barcode", "idx": 1, "links": [], +<<<<<<< HEAD "modified": "2025-01-15 16:22:49.873889", +======= + "modified": "2025-07-15 13:36:21.938700", +>>>>>>> e073075834 (fix: system was allowing credit notes with serial numbers for any customer) "modified_by": "Administrator", "module": "Stock", "name": "Serial No", @@ -310,6 +324,7 @@ "role": "Stock User" } ], + "row_format": "Dynamic", "search_fields": "item_code", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 928313576f1..896323d6529 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -40,6 +40,7 @@ class SerialNo(StockController): batch_no: DF.Link | None brand: DF.Link | None company: DF.Link + customer: DF.Link | None description: DF.Text | None employee: DF.Link | None item_code: DF.Link diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 8d9634db965..0fbf8475103 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -377,6 +377,10 @@ class SerialBatchBundle: ]: status = "Consumed" + customer = None + if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0: + customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer") + sn_table = frappe.qb.DocType("Serial No") query = ( @@ -387,10 +391,11 @@ class SerialBatchBundle: "Active" if warehouse else status - if (sn_table.purchase_document_no != sle.voucher_no and sle.is_cancelled != 1) + if (sn_table.purchase_document_no != sle.voucher_no or sle.is_cancelled != 1) else "Inactive", ) .set(sn_table.company, sle.company) + .set(sn_table.customer, customer) .where(sn_table.name.isin(serial_nos)) ) From 9aeb08f968888bdb9a73ca2832a5d4b303da5fe3 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 15 Jul 2025 15:03:41 +0530 Subject: [PATCH 54/58] chore: fix conflicts --- erpnext/stock/doctype/serial_no/serial_no.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 021847d5fa5..924277b30ea 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -282,11 +282,7 @@ "icon": "fa fa-barcode", "idx": 1, "links": [], -<<<<<<< HEAD - "modified": "2025-01-15 16:22:49.873889", -======= - "modified": "2025-07-15 13:36:21.938700", ->>>>>>> e073075834 (fix: system was allowing credit notes with serial numbers for any customer) + "modified": "2025-07-15 13:40:21.938700", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", From 3100099cfad1c6ad44b935a27d629329e45ac4ff Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 15 Jul 2025 15:04:18 +0530 Subject: [PATCH 55/58] chore: fix conflicts --- erpnext/stock/doctype/delivery_note/delivery_note.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 605cf2edb85..8aeb56e5d7c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -466,12 +466,8 @@ class DeliveryNote(SellingController): self.make_bundle_for_sales_purchase_return(table_name) self.make_bundle_using_old_serial_batch_fields(table_name) -<<<<<<< HEAD -======= self.validate_standalone_serial_nos_customer() - self.update_stock_reservation_entries() ->>>>>>> e073075834 (fix: system was allowing credit notes with serial numbers for any customer) # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() From 78df52606f7fe990610acf09bc6e3c23a76876c0 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 14 Jul 2025 14:56:12 +0530 Subject: [PATCH 56/58] fix: handle cases where distributed discount amount is not set (cherry picked from commit 816b84be0292ea6565747d15c85ad7aa9c528824) --- .../sales_invoice/test_sales_invoice.py | 22 +++++++++++++++++++ erpnext/controllers/accounts_controller.py | 9 ++++++++ 2 files changed, 31 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index bae372250ea..70d09ab70ed 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3055,6 +3055,28 @@ class TestSalesInvoice(FrappeTestCase): check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + # cases where distributed discount amount is not set + frappe.db.set_value( + "Sales Invoice Item", + {"name": ["in", [d.name for d in si.items]]}, + "distributed_discount_amount", + 0, + ) + + si.load_from_db() + si.additional_discount_account = additional_discount_account + # Ledger reposted implicitly upon 'Update After Submit' + si.save() + + expected_gle = [ + ["Debtors - _TC", 88, 0.0, nowdate()], + ["Discount Account - _TC", 22.0, 0.0, nowdate()], + ["Service - _TC", 0.0, 100.0, nowdate()], + ["TDS Payable - _TC", 0.0, 10.0, nowdate()], + ] + + check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + def test_asset_depreciation_on_sale_with_pro_rata(self): """ Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 33c5783861d..e0e53adeebd 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1946,6 +1946,15 @@ class AccountsController(TransactionBase): and self.get("discount_amount") and self.get("additional_discount_account") ): + # cases where distributed_discount_amount is not patched + if not hasattr(self, "__has_distributed_discount_set"): + self.__has_distributed_discount_set = any( + i.distributed_discount_amount for i in self.get("items") + ) + + if not self.__has_distributed_discount_set: + return item.amount, item.base_amount + amount += item.distributed_discount_amount base_amount += flt( item.distributed_discount_amount * self.get("conversion_rate"), From c57ca1ae29028d9e366159bd88d0d4cd99e54481 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 15 Jul 2025 15:36:13 +0530 Subject: [PATCH 57/58] fix: incorrect test --- erpnext/assets/doctype/asset/test_asset.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 571b8734991..9c8db82f41b 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -487,7 +487,6 @@ class TestAsset(AssetSetup): self.assertEqual(new_asset.gross_purchase_amount, 24000) self.assertEqual(new_asset.opening_accumulated_depreciation, 4000) self.assertEqual(new_asset.split_from, asset.name) - self.assertEqual(new_asset.value_after_depreciation, 16000) self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 4000) self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 4000) @@ -1707,9 +1706,6 @@ def create_asset(**args): "is_composite_asset": args.is_composite_asset or 0, "asset_quantity": args.get("asset_quantity") or 1, "depr_entry_posting_status": args.depr_entry_posting_status or "", - "value_after_depreciation": ( - (args.gross_purchase_amount or 100000) - (args.opening_accumulated_depreciation or 0) - ), } ) From 54275dbe38fded7675bd0eaaa50f94c4df617470 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 15 Jul 2025 16:45:58 +0530 Subject: [PATCH 58/58] fix: fix party account field access (cherry picked from commit 0da8ed2daa9ae2a2b5d6c37c957bbd78cfe4e8e5) --- 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 26d0f67267e..c7260ccc722 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -989,7 +989,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } var party = me.frm.doc[frappe.model.scrub(party_type)]; - if(party && me.frm.doc.company && (!me.frm.doc.__onload?.load_after_mapping || !me.frm.doc.get(party_account_field))) { + if(party && me.frm.doc.company && (!me.frm.doc.__onload?.load_after_mapping || !me.frm.doc[party_account_field])) { return frappe.call({ method: "erpnext.accounts.party.get_party_account", args: {