From d9b24a30ebb6bf1a4d5e5d8f6902153972b13248 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 16 Jul 2025 13:12:51 +0530 Subject: [PATCH 01/67] fix: update asset value after revaluation cancellation --- .../asset_value_adjustment.py | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 2c1d7d9fcab..31fd62095df 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -48,7 +48,6 @@ class AssetValueAdjustment(Document): def on_submit(self): self.make_depreciation_entry() - self.set_value_after_depreciation() self.update_asset(self.new_asset_value) add_asset_activity( self.asset, @@ -80,9 +79,6 @@ class AssetValueAdjustment(Document): def set_difference_amount(self): self.difference_amount = flt(self.new_asset_value - self.current_asset_value) - def set_value_after_depreciation(self): - frappe.db.set_value("Asset", self.asset, "value_after_depreciation", self.new_asset_value) - def set_current_asset_value(self): if not self.current_asset_value and self.asset: self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book) @@ -164,12 +160,8 @@ class AssetValueAdjustment(Document): self.db_set("journal_entry", je.name) def update_asset(self, asset_value=None): - asset = frappe.get_doc("Asset", self.asset) - - if not asset.calculate_depreciation: - asset.value_after_depreciation = asset_value - asset.save() - return + difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount + asset = self.update_asset_value_after_depreciation(difference_amount) asset.flags.decrease_in_asset_value_due_to_value_adjustment = True @@ -188,19 +180,6 @@ class AssetValueAdjustment(Document): get_link_to_form(self.get("doctype"), self.get("name")), ) - difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount - if asset.calculate_depreciation: - for row in asset.finance_books: - if cstr(row.finance_book) == cstr(self.finance_book): - salvage_value_adjustment = ( - self.get_adjusted_salvage_value_amount(row, difference_amount) or 0 - ) - row.expected_value_after_useful_life += salvage_value_adjustment - row.value_after_depreciation += flt(difference_amount) - row.db_update() - - asset.db_update() - make_new_active_asset_depr_schedules_and_cancel_current_ones( asset, notes, @@ -212,6 +191,23 @@ class AssetValueAdjustment(Document): asset.save() asset.set_status() + def update_asset_value_after_depreciation(self, difference_amount): + asset = frappe.get_doc("Asset", self.asset) + + if asset.calculate_depreciation: + for row in asset.finance_books: + if cstr(row.finance_book) == cstr(self.finance_book): + salvage_value_adjustment = ( + self.get_adjusted_salvage_value_amount(row, difference_amount) or 0 + ) + row.expected_value_after_useful_life += salvage_value_adjustment + row.value_after_depreciation = row.value_after_depreciation + flt(difference_amount) + row.db_update() + + asset.value_after_depreciation += flt(difference_amount) + asset.db_update() + return asset + def get_adjusted_salvage_value_amount(self, row, difference_amount): if row.expected_value_after_useful_life: salvage_value_adjustment = (difference_amount * row.salvage_value_percentage) / 100 From 3ab6a256e0a73637f1b5f22005553d86eeffb3a3 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 16 Jul 2025 13:33:31 +0530 Subject: [PATCH 02/67] test: updated test case --- .../asset_value_adjustment/test_asset_value_adjustment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index dfde9a7d885..23cdc25ecf8 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -267,6 +267,7 @@ class TestAssetValueAdjustment(unittest.TestCase): asset_doc.calculate_depreciation = 1 asset_doc.available_for_use_date = "2023-01-15" asset_doc.purchase_date = "2023-01-15" + asset_doc.value_after_depreciation = 54000.0 asset_doc.append( "finance_books", From 20bbfc504fbcf2708aa15341cedfe6ae5b7d0e65 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 16 Jul 2025 14:33:35 +0530 Subject: [PATCH 03/67] fix: do not set value after depreciation as zero --- erpnext/assets/doctype/asset/asset.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 019c97114fa..01a34a33177 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -249,14 +249,11 @@ class Asset(AccountsController): frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) def prepare_depreciation_data(self): + self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( + self.opening_accumulated_depreciation + ) if self.calculate_depreciation: - self.value_after_depreciation = 0 self.set_depreciation_rate() - else: - self.finance_books = [] - self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( - self.opening_accumulated_depreciation - ) def validate_item(self): item = frappe.get_cached_value( From c7dcbed16f92ad9d2e6f501f29a40e43fb65e7c0 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 16 Jul 2025 15:41:42 +0530 Subject: [PATCH 04/67] fix: test case --- .../asset_value_adjustment/test_asset_value_adjustment.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index 23cdc25ecf8..fa292f73fa6 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -267,7 +267,6 @@ class TestAssetValueAdjustment(unittest.TestCase): asset_doc.calculate_depreciation = 1 asset_doc.available_for_use_date = "2023-01-15" asset_doc.purchase_date = "2023-01-15" - asset_doc.value_after_depreciation = 54000.0 asset_doc.append( "finance_books", @@ -283,13 +282,13 @@ class TestAssetValueAdjustment(unittest.TestCase): adj_doc = make_asset_value_adjustment( asset=asset_doc.name, - current_asset_value=54000, + current_asset_value=120000.0, new_asset_value=50000.0, date="2023-08-21", ) adj_doc.submit() difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value - self.assertEqual(difference_amount, -4000) + self.assertEqual(difference_amount, -70000) asset_doc.load_from_db() self.assertEqual(asset_doc.value_after_depreciation, 50000.0) From 9fe1e6d0bdf4197ccab26e3a6ca5a922902b496f Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 16 Jul 2025 16:03:07 +0530 Subject: [PATCH 05/67] fix: test case --- erpnext/assets/doctype/asset/test_asset.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 9c8db82f41b..1ae96fe75dd 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1492,7 +1492,6 @@ class TestDepreciationBasics(AssetSetup): ) self.assertSequenceEqual(gle, expected_gle) - self.assertEqual(asset.get("value_after_depreciation"), 0) def test_expected_value_change(self): """ From 6adc8a09c0adfc57b8a80a991867956c97a93b5e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 17 Jul 2025 14:35:06 +0530 Subject: [PATCH 06/67] feat: option to recalculate costing and billing fields in project (cherry picked from commit dd23d4c81b7d1eb6988128619b871a0a4cccdf19) --- erpnext/projects/doctype/project/project.js | 12 ++++++------ erpnext/projects/doctype/project/project.py | 13 ++++--------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 449a9d87ff2..1012b093107 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -88,9 +88,9 @@ frappe.ui.form.on("Project", { ); frm.add_custom_button( - __("Update Total Purchase Cost"), + __("Recalculate Costing and Billing"), () => { - frm.events.update_total_purchase_cost(frm); + frm.events.recalculate_costing_and_billing(frm); }, __("Actions") ); @@ -129,15 +129,15 @@ frappe.ui.form.on("Project", { } }, - update_total_purchase_cost: function (frm) { + recalculate_costing_and_billing: function (frm) { frappe.call({ - method: "erpnext.projects.doctype.project.project.recalculate_project_total_purchase_cost", + method: "erpnext.projects.doctype.project.project.recalculate_costing_and_billing", args: { project: frm.doc.name }, freeze: true, - freeze_message: __("Recalculating Purchase Cost against this Project..."), + freeze_message: __("Recalculating Costing and Billing fields against this Project..."), callback: function (r) { if (r && !r.exc) { - frappe.msgprint(__("Total Purchase Cost has been updated")); + frappe.msgprint(__("Costing and Billing fields has been updated")); frm.refresh(); } }, diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index ebfff79531f..992617d52fb 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -749,12 +749,7 @@ def calculate_total_purchase_cost(project: str | None = None): @frappe.whitelist() -def recalculate_project_total_purchase_cost(project: str | None = None): - if project: - total_purchase_cost = calculate_total_purchase_cost(project) - frappe.db.set_value( - "Project", - project, - "total_purchase_cost", - (total_purchase_cost and total_purchase_cost[0][0] or 0), - ) +def recalculate_costing_and_billing(project: str | None = None): + project = frappe.get_doc("Project", project) + project.update_costing() + project.db_update() From 36a9f3b3e9e91e2acacba8a2f544d45ae4ef22d6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 18 Jul 2025 16:06:14 +0530 Subject: [PATCH 07/67] chore: rename recalculating to updating (cherry picked from commit f6e16c118081955127bb7d7548e968f9d3d89145) --- erpnext/projects/doctype/project/project.js | 10 +++++----- erpnext/projects/doctype/project/project.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 1012b093107..4d4c4ece745 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -88,9 +88,9 @@ frappe.ui.form.on("Project", { ); frm.add_custom_button( - __("Recalculate Costing and Billing"), + __("Update Costing and Billing"), () => { - frm.events.recalculate_costing_and_billing(frm); + frm.events.update_costing_and_billing(frm); }, __("Actions") ); @@ -129,12 +129,12 @@ frappe.ui.form.on("Project", { } }, - recalculate_costing_and_billing: function (frm) { + update_costing_and_billing: function (frm) { frappe.call({ - method: "erpnext.projects.doctype.project.project.recalculate_costing_and_billing", + method: "erpnext.projects.doctype.project.project.update_costing_and_billing", args: { project: frm.doc.name }, freeze: true, - freeze_message: __("Recalculating Costing and Billing fields against this Project..."), + freeze_message: __("Updating Costing and Billing fields against this Project..."), callback: function (r) { if (r && !r.exc) { frappe.msgprint(__("Costing and Billing fields has been updated")); diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 992617d52fb..cc8f2434513 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -749,7 +749,7 @@ def calculate_total_purchase_cost(project: str | None = None): @frappe.whitelist() -def recalculate_costing_and_billing(project: str | None = None): +def update_costing_and_billing(project: str | None = None): project = frappe.get_doc("Project", project) project.update_costing() project.db_update() From fd1c213a8d64492cc756517f884555459d893435 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 18 Jul 2025 16:43:27 +0530 Subject: [PATCH 08/67] fix: patch to set default buying price list in material request (#48680) * fix: patch to set default buying price list in material request (cherry picked from commit 446264e496df671c4cede0464aa5e256979370e3) --- erpnext/patches.txt | 1 + ...missing_buying_price_list_in_material_request.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 erpnext/patches/v15_0/patch_missing_buying_price_list_in_material_request.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5f4c3672228..16390f19780 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -413,4 +413,5 @@ erpnext.patches.v15_0.update_pick_list_fields erpnext.patches.v15_0.update_pegged_currencies erpnext.patches.v15_0.set_company_on_pos_inv_merge_log erpnext.patches.v15_0.rename_price_list_to_buying_price_list +erpnext.patches.v15_0.patch_missing_buying_price_list_in_material_request erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice diff --git a/erpnext/patches/v15_0/patch_missing_buying_price_list_in_material_request.py b/erpnext/patches/v15_0/patch_missing_buying_price_list_in_material_request.py new file mode 100644 index 00000000000..48f85335dd2 --- /dev/null +++ b/erpnext/patches/v15_0/patch_missing_buying_price_list_in_material_request.py @@ -0,0 +1,13 @@ +import frappe +import frappe.defaults + + +def execute(): + if frappe.db.has_column("Material Request", "buying_price_list") and ( + default_buying_price_list := frappe.defaults.get_defaults().buying_price_list + ): + docs = frappe.get_all( + "Material Request", filters={"buying_price_list": ["is", "not set"], "docstatus": 1}, pluck="name" + ) + for doc in docs: + frappe.db.set_value("Material Request", doc, "buying_price_list", default_buying_price_list) From 2b08c5b76970c7a6f233a21c0f25cd2eeb130bcb Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Thu, 3 Jul 2025 12:59:00 +0530 Subject: [PATCH 09/67] feat: update stock balance report to support multi-select for items and warehouses (cherry picked from commit 0d2a88bafc4a88f193e6eec1d196ee6c2b1446f8) --- .../report/stock_balance/stock_balance.js | 33 +++++++-------- .../report/stock_balance/stock_balance.py | 42 ++++++++++++++----- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index 0d68caa7e09..a8b2bd1d782 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -36,38 +36,37 @@ frappe.query_reports["Stock Balance"] = { }, { fieldname: "item_code", - label: __("Item"), - fieldtype: "Link", + label: __("Items"), + fieldtype: "MultiSelectList", width: "80", options: "Item", - get_query: function () { + get_data: function (txt) { let item_group = frappe.query_report.get_filter_value("item_group"); - return { - query: "erpnext.controllers.queries.item_query", - filters: { - ...(item_group && { item_group }), - is_stock_item: 1, - }, + let filters = { + ...(item_group && { item_group }), + is_stock_item: 1, }; + + return frappe.db.get_link_options("Item", txt, filters); }, }, { fieldname: "warehouse", - label: __("Warehouse"), - fieldtype: "Link", + label: __("Warehouses"), + fieldtype: "MultiSelectList", width: "80", options: "Warehouse", - get_query: () => { + get_data: (txt) => { let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); let company = frappe.query_report.get_filter_value("company"); - return { - filters: { - ...(warehouse_type && { warehouse_type }), - ...(company && { company }), - }, + let filters = { + ...(warehouse_type && { warehouse_type }), + ...(company && { company }), }; + + return frappe.db.get_link_options("Warehouse", txt, filters); }, }, { diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index b6587cd7b37..05a35d64520 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -11,6 +11,7 @@ from frappe.query_builder import Order from frappe.query_builder.functions import Coalesce from frappe.utils import add_days, cint, date_diff, flt, getdate from frappe.utils.nestedset import get_descendants_of +from pypika.terms import ExistsCriterion import erpnext from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions @@ -24,8 +25,8 @@ class StockBalanceFilter(TypedDict): from_date: str to_date: str item_group: str | None - item: str | None - warehouse: str | None + item: list[str] | None + warehouse: list[str] | None warehouse_type: str | None include_uom: str | None # include extra info in converted UOM show_stock_ageing_data: bool @@ -345,8 +346,29 @@ class StockBalanceReport: def apply_warehouse_filters(self, query, sle) -> str: warehouse_table = frappe.qb.DocType("Warehouse") - if self.filters.get("warehouse"): - query = apply_warehouse_filter(query, sle, self.filters) + if warehouses := self.filters.get("warehouse"): + warehouse_range = frappe.get_all( + "Warehouse", + filters={ + "name": ("in", warehouses), + }, + fields=["lft", "rgt"], + as_list=True, + ) + + child_query = frappe.qb.from_(warehouse_table).select(warehouse_table.name) + + range_conditions = [ + (warehouse_table.lft >= lft) & (warehouse_table.rgt <= rgt) for lft, rgt in warehouse_range + ] + + combined_condition = range_conditions[0] + for condition in range_conditions[1:]: + combined_condition = combined_condition | condition + + child_query = child_query.where(combined_condition & (warehouse_table.name == sle.warehouse)) + query = query.where(ExistsCriterion(child_query)) + elif warehouse_type := self.filters.get("warehouse_type"): query = ( query.join(warehouse_table) @@ -361,13 +383,11 @@ class StockBalanceReport: children = get_descendants_of("Item Group", item_group, ignore_permissions=True) query = query.where(item_table.item_group.isin([*children, item_group])) - for field in ["item_code", "brand"]: - if not self.filters.get(field): - continue - elif field == "item_code": - query = query.where(item_table.name == self.filters.get(field)) - else: - query = query.where(item_table[field] == self.filters.get(field)) + if item_codes := self.filters.get("item_code"): + query = query.where(item_table.name.isin(item_codes)) + + if brand := self.filters.get("brand"): + query = query.where(item_table.brand == brand) return query From 801cda38136fc7c95419092137ed2780d70fc2f1 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Thu, 3 Jul 2025 16:24:27 +0530 Subject: [PATCH 10/67] feat: enhance apply_warehouse_filter to support multiple warehouses in filters (cherry picked from commit 2ff1dcc3911f5387a065eb36d1bca3094c69dd40) --- erpnext/stock/doctype/warehouse/warehouse.py | 42 ++++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index aa9dfd2048e..e38c44032ad 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -242,19 +242,35 @@ def get_warehouses_based_on_account(account, company=None): # Will be use for frappe.qb def apply_warehouse_filter(query, sle, filters): - if warehouse := filters.get("warehouse"): - warehouse_table = frappe.qb.DocType("Warehouse") + if not (warehouses := filters.get("warehouse")): + return query - lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - chilren_subquery = ( - frappe.qb.from_(warehouse_table) - .select(warehouse_table.name) - .where( - (warehouse_table.lft >= lft) - & (warehouse_table.rgt <= rgt) - & (warehouse_table.name == sle.warehouse) - ) - ) - query = query.where(ExistsCriterion(chilren_subquery)) + warehouse_table = frappe.qb.DocType("Warehouse") + + if isinstance(warehouses, str): + warehouses = [warehouses] + + warehouse_range = frappe.get_all( + "Warehouse", + filters={ + "name": ("in", warehouses), + }, + fields=["lft", "rgt"], + as_list=True, + ) + + child_query = frappe.qb.from_(warehouse_table).select(warehouse_table.name) + + range_conditions = [ + (warehouse_table.lft >= lft) & (warehouse_table.rgt <= rgt) for lft, rgt in warehouse_range + ] + + combined_condition = range_conditions[0] + for condition in range_conditions[1:]: + combined_condition = combined_condition | condition + + child_query = child_query.where(combined_condition & (warehouse_table.name == sle.warehouse)) + + query = query.where(ExistsCriterion(child_query)) return query From 72e8ce044939158e2228e0b885ffd4d368b2858e Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Thu, 3 Jul 2025 16:26:58 +0530 Subject: [PATCH 11/67] refactor: use existing functionality (cherry picked from commit 2882576479a129a9445b5da13d46c986351f2919) --- .../report/stock_balance/stock_balance.py | 24 ++----------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 05a35d64520..aee641c67c1 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -346,28 +346,8 @@ class StockBalanceReport: def apply_warehouse_filters(self, query, sle) -> str: warehouse_table = frappe.qb.DocType("Warehouse") - if warehouses := self.filters.get("warehouse"): - warehouse_range = frappe.get_all( - "Warehouse", - filters={ - "name": ("in", warehouses), - }, - fields=["lft", "rgt"], - as_list=True, - ) - - child_query = frappe.qb.from_(warehouse_table).select(warehouse_table.name) - - range_conditions = [ - (warehouse_table.lft >= lft) & (warehouse_table.rgt <= rgt) for lft, rgt in warehouse_range - ] - - combined_condition = range_conditions[0] - for condition in range_conditions[1:]: - combined_condition = combined_condition | condition - - child_query = child_query.where(combined_condition & (warehouse_table.name == sle.warehouse)) - query = query.where(ExistsCriterion(child_query)) + if self.filters.get("warehouse"): + apply_warehouse_filter(query, sle, self.filters) elif warehouse_type := self.filters.get("warehouse_type"): query = ( From ecf9e6e74834d59510cfb7962831ddd407188e1f Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Thu, 3 Jul 2025 18:04:05 +0530 Subject: [PATCH 12/67] feat: update stock ledger report to support multi-select for warehouses and items (cherry picked from commit f2afd98725e3c0f242309e7486dbba37a4db5c3f) --- .../stock/report/stock_ledger/stock_ledger.js | 23 +++--- .../stock/report/stock_ledger/stock_ledger.py | 61 ++++++++++++---- erpnext/stock/stock_ledger.py | 71 +++++++++++-------- 3 files changed, 98 insertions(+), 57 deletions(-) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index d4c11de74e6..28bf54437a6 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -27,25 +27,24 @@ frappe.query_reports["Stock Ledger"] = { }, { fieldname: "warehouse", - label: __("Warehouse"), - fieldtype: "Link", + label: __("Warehouses"), + fieldtype: "MultiSelectList", options: "Warehouse", - get_query: function () { + get_data: function (txt) { const company = frappe.query_report.get_filter_value("company"); - return { - filters: { company: company }, - }; + + return frappe.db.get_link_options("Warehouse", txt, { + company: company, + }); }, }, { fieldname: "item_code", - label: __("Item"), - fieldtype: "Link", + label: __("Items"), + fieldtype: "MultiSelectList", options: "Item", - get_query: function () { - return { - query: "erpnext.controllers.queries.item_query", - }; + get_data: function (txt) { + return frappe.db.get_link_options("Item", txt, {}); }, }, { diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 391395503b0..54335ff6507 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -456,19 +456,23 @@ def get_items(filters): query = frappe.qb.from_(item).select(item.name) conditions = [] - if item_code := filters.get("item_code"): - conditions.append(item.name == item_code) + if item_codes := filters.get("item_code"): + conditions.append(item.name.isin(item_codes)) + else: if brand := filters.get("brand"): conditions.append(item.brand == brand) - if item_group := filters.get("item_group"): - if condition := get_item_group_condition(item_group, item): - conditions.append(condition) + + if filters.get("item_group") and ( + condition := get_item_group_condition(filters.get("item_group"), item) + ): + conditions.append(condition) items = [] if conditions: for condition in conditions: query = query.where(condition) + items = [r[0] for r in query.run()] return items @@ -505,6 +509,7 @@ def get_item_details(items, sl_entries, include_uom): return item_details +# TODO: THIS IS NOT USED def get_sle_conditions(filters): conditions = [] if filters.get("warehouse"): @@ -535,8 +540,8 @@ def get_opening_balance_from_batch(filters, columns, sl_entries): } for fields in ["item_code", "warehouse"]: - if filters.get(fields): - query_filters[fields] = filters.get(fields) + value = filters.get(fields) + query_filters[fields] = ("in", value) opening_data = frappe.get_all( "Stock Ledger Entry", @@ -567,8 +572,16 @@ def get_opening_balance_from_batch(filters, columns, sl_entries): ) for field in ["item_code", "warehouse", "company"]: - if filters.get(field): - query = query.where(table[field] == filters.get(field)) + value = filters.get(field) + + if not value: + continue + + if isinstance(value, list | tuple): + query = query.where(table[field].isin(value)) + + else: + query = query.where(table[field] == value) bundle_data = query.run(as_dict=True) @@ -623,13 +636,31 @@ def get_opening_balance(filters, columns, sl_entries): return row -def get_warehouse_condition(warehouse): - warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) - if warehouse_details: - return f" exists (select name from `tabWarehouse` wh \ - where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)" +def get_warehouse_condition(warehouses): + if isinstance(warehouses, str): + warehouses = [warehouses] - return "" + warehouse_range = frappe.get_all( + "Warehouse", + filters={ + "name": ("in", warehouses), + }, + fields=["lft", "rgt"], + as_list=True, + ) + + if not warehouse_range: + return "" + + alias = "wh" + condtions = [] + for lft, rgt in warehouse_range: + condtions.append(f"({alias}.lft >= {lft} and {alias}.rgt <= {rgt})") + + condtions = " or ".join(condtions) + + return f" exists (select name from `tabWarehouse` {alias} \ + where ({condtions}) and warehouse = {alias}.name)" def get_item_group_condition(item_group, item_table=None): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 8e2319f7bd6..202c77faf90 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -56,12 +56,12 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc """Create SL entries from SL entry dicts args: - - allow_negative_stock: disable negative stock valiations if true - - via_landed_cost_voucher: landed cost voucher cancels and reposts - entries of purchase document. This flag is used to identify if - cancellation and repost is happening via landed cost voucher, in - such cases certain validations need to be ignored (like negative - stock) + - allow_negative_stock: disable negative stock valiations if true + - via_landed_cost_voucher: landed cost voucher cancels and reposts + entries of purchase document. This flag is used to identify if + cancellation and repost is happening via landed cost voucher, in + such cases certain validations need to be ignored (like negative + stock) """ from erpnext.controllers.stock_controller import future_sle_exists @@ -526,12 +526,12 @@ class update_entries_after: :param args: args as dict - args = { - "item_code": "ABC", - "warehouse": "XYZ", - "posting_date": "2012-12-12", - "posting_time": "12:00" - } + args = { + "item_code": "ABC", + "warehouse": "XYZ", + "posting_date": "2012-12-12", + "posting_time": "12:00" + } """ def __init__( @@ -599,15 +599,15 @@ class update_entries_after: :Data Structure: self.data = { - warehouse1: { - 'previus_sle': {}, - 'qty_after_transaction': 10, - 'valuation_rate': 100, - 'stock_value': 1000, - 'prev_stock_value': 1000, - 'stock_queue': '[[10, 100]]', - 'stock_value_difference': 1000 - } + warehouse1: { + 'previus_sle': {}, + 'qty_after_transaction': 10, + 'valuation_rate': 100, + 'stock_value': 1000, + 'prev_stock_value': 1000, + 'stock_queue': '[[10, 100]]', + 'stock_value_difference': 1000 + } } """ @@ -1644,11 +1644,11 @@ def get_previous_sle(args, for_update=False, extra_cond=None): is called from various transaction like stock entry, reco etc args = { - "item_code": "ABC", - "warehouse": "XYZ", - "posting_date": "2012-12-12", - "posting_time": "12:00", - "sle": "name of reference Stock Ledger Entry" + "item_code": "ABC" or ["ABC", "XYZ"], + "warehouse": "XYZ" or ["XYZ", "ABC"], + "posting_date": "2012-12-12", + "posting_time": "12:00", + "sle": "name of reference Stock Ledger Entry" } """ args["name"] = args.get("sle", None) or "" @@ -1670,8 +1670,20 @@ def get_stock_ledger_entries( ): """get stock ledger entries filtered by specific posting datetime conditions""" conditions = f" and posting_datetime {operator} %(posting_datetime)s" - if previous_sle.get("warehouse"): - conditions += " and warehouse = %(warehouse)s" + + if item_code := previous_sle.get("item_code"): + if isinstance(item_code, list | tuple): + conditions += " and item_code in %(item_code)s" + else: + conditions += " and item_code = %(item_code)s" + + if warehouse := previous_sle.get("warehouse"): + if isinstance(warehouse, list | tuple): + conditions += " and warehouse in %(warehouse)s" + + else: + conditions += " and warehouse = %(warehouse)s" + elif previous_sle.get("warehouse_condition"): conditions += " and " + previous_sle.get("warehouse_condition") @@ -1714,8 +1726,7 @@ def get_stock_ledger_entries( """ select *, posting_datetime as "timestamp" from `tabStock Ledger Entry` - where item_code = %(item_code)s - and is_cancelled = 0 + where is_cancelled = 0 {conditions} order by posting_datetime {order}, creation {order} {limit} {for_update}""".format( From 62033b5c7a14c25828d32ba82a55ae21ad651557 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Sat, 5 Jul 2025 10:04:34 +0530 Subject: [PATCH 13/67] fix(test): update tests (cherry picked from commit 0a71ca6739b011c88ad1388f99c808747a62b037) --- .../doctype/purchase_receipt/test_purchase_receipt.py | 2 +- .../stock/report/stock_balance/test_stock_balance.py | 4 ++-- .../report/stock_ledger/test_stock_ledger_report.py | 2 +- erpnext/stock/report/test_reports.py | 11 +++++++++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ae762093182..b9725b67316 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3669,7 +3669,7 @@ class TestPurchaseReceipt(FrappeTestCase): columns, data = execute( filters=frappe._dict( - {"item_code": item_code, "warehouse": pr.items[0].warehouse, "company": pr.company} + {"item_code": [item_code], "warehouse": [pr.items[0].warehouse], "company": pr.company} ) ) diff --git a/erpnext/stock/report/stock_balance/test_stock_balance.py b/erpnext/stock/report/stock_balance/test_stock_balance.py index 8b3dbee7c8d..0985e4783c3 100644 --- a/erpnext/stock/report/stock_balance/test_stock_balance.py +++ b/erpnext/stock/report/stock_balance/test_stock_balance.py @@ -23,7 +23,7 @@ class TestStockBalance(FrappeTestCase): self.filters = _dict( { "company": "_Test Company", - "item_code": self.item.name, + "item_code": [self.item.name], "from_date": "2020-01-01", "to_date": str(today()), } @@ -165,6 +165,6 @@ class TestStockBalance(FrappeTestCase): variant.save() self.generate_stock_ledger(variant.name, [_dict(qty=5, rate=10)]) - rows = stock_balance(self.filters.update({"show_variant_attributes": 1, "item_code": variant.name})) + rows = stock_balance(self.filters.update({"show_variant_attributes": 1, "item_code": [variant.name]})) self.assertPartialDictEq(attributes, rows[0]) self.assertInvariants(rows) diff --git a/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py b/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py index 12800f2216a..d57052e905f 100644 --- a/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py +++ b/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py @@ -17,7 +17,7 @@ class TestStockLedgerReeport(FrappeTestCase): company="_Test Company", from_date=today(), to_date=add_days(today(), 30), - item_code="_Test Stock Report Serial Item", + item_code=["_Test Stock Report Serial Item"], ) def tearDown(self) -> None: diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py index ad2b46b393f..ba3a8c7e02a 100644 --- a/erpnext/stock/report/test_reports.py +++ b/erpnext/stock/report/test_reports.py @@ -17,8 +17,15 @@ batch = get_random("Batch") REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ ("Stock Ledger", {"_optional": True}), ("Stock Ledger", {"batch_no": batch}), - ("Stock Ledger", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}), - ("Stock Balance", {"_optional": True}), + ("Stock Ledger", {"item_code": ["_Test Item"], "warehouse": ["_Test Warehouse - _TC"]}), + ( + "Stock Balance", + { + "item_code": ["_Test Item"], + "warehouse": ["_Test Warehouse - _TC"], + "item_group": "_Test Item Group", + }, + ), ("Stock Projected Qty", {"_optional": True}), ("Batch-Wise Balance History", {}), ("Itemwise Recommended Reorder Level", {"item_group": "All Item Groups"}), From fa01bdc4907f3f0542eab81bae7712012602ddd9 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Mon, 7 Jul 2025 11:06:46 +0530 Subject: [PATCH 14/67] fix: correct query filter assignment in stock ledger and balance reports (cherry picked from commit e60c711fdc13bd02a9235780ae926bd54b50a52b) --- erpnext/stock/report/stock_balance/stock_balance.py | 2 +- erpnext/stock/report/stock_ledger/stock_ledger.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index aee641c67c1..8957739b31a 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -347,7 +347,7 @@ class StockBalanceReport: warehouse_table = frappe.qb.DocType("Warehouse") if self.filters.get("warehouse"): - apply_warehouse_filter(query, sle, self.filters) + query = apply_warehouse_filter(query, sle, self.filters) elif warehouse_type := self.filters.get("warehouse_type"): query = ( diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 54335ff6507..addf938ec61 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -540,8 +540,8 @@ def get_opening_balance_from_batch(filters, columns, sl_entries): } for fields in ["item_code", "warehouse"]: - value = filters.get(fields) - query_filters[fields] = ("in", value) + if value := filters.get(fields): + query_filters[fields] = ("in", value) opening_data = frappe.get_all( "Stock Ledger Entry", From a7e8f404f7a17310dd1fbf8570d58dec3b5d8742 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Tue, 15 Jul 2025 13:23:09 +0530 Subject: [PATCH 15/67] fix: use the item_query for get_data (cherry picked from commit 169caaf66f122cd9350414b748ac3c53cf4c8548) --- .../stock/report/stock_ledger/stock_ledger.js | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index 28bf54437a6..db6c0c06281 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -43,8 +43,28 @@ frappe.query_reports["Stock Ledger"] = { label: __("Items"), fieldtype: "MultiSelectList", options: "Item", - get_data: function (txt) { - return frappe.db.get_link_options("Item", txt, {}); + get_data: async function (txt) { + let { message: data } = await frappe.call({ + method: "erpnext.controllers.queries.item_query", + args: { + doctype: "Item", + txt: txt, + searchfield: "name", + start: 0, + page_len: 10, + filters: {}, + as_dict: 1, + }, + }); + + data = data.map(({ name, description }) => { + return { + value: name, + description: description, + }; + }); + + return data || []; }, }, { From 1d52a8fb69844e01de73146927609db4cf4bbe4b Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Tue, 15 Jul 2025 13:39:39 +0530 Subject: [PATCH 16/67] fix: handle empty warehouse condition in get_warehouse_condition function; typo; (cherry picked from commit fca9843fc28f3901358c873c28c9129ae0230525) --- erpnext/stock/report/stock_ledger/stock_ledger.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index addf938ec61..bbc85c5a4ad 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -637,6 +637,9 @@ def get_opening_balance(filters, columns, sl_entries): def get_warehouse_condition(warehouses): + if not warehouses: + return "" + if isinstance(warehouses, str): warehouses = [warehouses] @@ -653,14 +656,14 @@ def get_warehouse_condition(warehouses): return "" alias = "wh" - condtions = [] + conditions = [] for lft, rgt in warehouse_range: - condtions.append(f"({alias}.lft >= {lft} and {alias}.rgt <= {rgt})") + conditions.append(f"({alias}.lft >= {lft} and {alias}.rgt <= {rgt})") - condtions = " or ".join(condtions) + conditions = " or ".join(conditions) return f" exists (select name from `tabWarehouse` {alias} \ - where ({condtions}) and warehouse = {alias}.name)" + where ({conditions}) and warehouse = {alias}.name)" def get_item_group_condition(item_group, item_table=None): From b57163b7be46bc2e3641b236ea23e79763b39c5f Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Tue, 15 Jul 2025 15:15:31 +0530 Subject: [PATCH 17/67] fix: warehouse filter query by chaining conditions (cherry picked from commit 7a266113edf7caee28cddc341e2243931c640340) --- erpnext/stock/doctype/warehouse/warehouse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index e38c44032ad..4600c2bbaae 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -269,7 +269,7 @@ def apply_warehouse_filter(query, sle, filters): for condition in range_conditions[1:]: combined_condition = combined_condition | condition - child_query = child_query.where(combined_condition & (warehouse_table.name == sle.warehouse)) + child_query = child_query.where(combined_condition).where(warehouse_table.name == sle.warehouse) query = query.where(ExistsCriterion(child_query)) From a1aee44014a55563e4eaefe8af67838cf0fc46fa Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Wed, 16 Jul 2025 16:12:43 +0530 Subject: [PATCH 18/67] refactor: remove unused imports in stock_balance.py (cherry picked from commit bc46045cc751d651f6fb4d57e74e2ba7efae362b) --- erpnext/stock/report/stock_balance/stock_balance.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 8957739b31a..08133583e2e 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -7,11 +7,9 @@ from typing import Any, TypedDict import frappe from frappe import _ -from frappe.query_builder import Order from frappe.query_builder.functions import Coalesce from frappe.utils import add_days, cint, date_diff, flt, getdate from frappe.utils.nestedset import get_descendants_of -from pypika.terms import ExistsCriterion import erpnext from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions From fb81202830eb3efee2b7180badc24d3488ce1cc1 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Thu, 17 Jul 2025 15:39:12 +0530 Subject: [PATCH 19/67] refactor: revert indentation (cherry picked from commit 063c4e97202fbb57e14cd1059f8cd332765ec8da) --- erpnext/stock/stock_ledger.py | 52 +++++++++++++++++------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 202c77faf90..61a19a95bd4 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -56,12 +56,12 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc """Create SL entries from SL entry dicts args: - - allow_negative_stock: disable negative stock valiations if true - - via_landed_cost_voucher: landed cost voucher cancels and reposts - entries of purchase document. This flag is used to identify if - cancellation and repost is happening via landed cost voucher, in - such cases certain validations need to be ignored (like negative - stock) + - allow_negative_stock: disable negative stock valiations if true + - via_landed_cost_voucher: landed cost voucher cancels and reposts + entries of purchase document. This flag is used to identify if + cancellation and repost is happening via landed cost voucher, in + such cases certain validations need to be ignored (like negative + stock) """ from erpnext.controllers.stock_controller import future_sle_exists @@ -526,12 +526,12 @@ class update_entries_after: :param args: args as dict - args = { - "item_code": "ABC", - "warehouse": "XYZ", - "posting_date": "2012-12-12", - "posting_time": "12:00" - } + args = { + "item_code": "ABC", + "warehouse": "XYZ", + "posting_date": "2012-12-12", + "posting_time": "12:00" + } """ def __init__( @@ -599,15 +599,15 @@ class update_entries_after: :Data Structure: self.data = { - warehouse1: { - 'previus_sle': {}, - 'qty_after_transaction': 10, - 'valuation_rate': 100, - 'stock_value': 1000, - 'prev_stock_value': 1000, - 'stock_queue': '[[10, 100]]', - 'stock_value_difference': 1000 - } + warehouse1: { + 'previus_sle': {}, + 'qty_after_transaction': 10, + 'valuation_rate': 100, + 'stock_value': 1000, + 'prev_stock_value': 1000, + 'stock_queue': '[[10, 100]]', + 'stock_value_difference': 1000 + } } """ @@ -1644,11 +1644,11 @@ def get_previous_sle(args, for_update=False, extra_cond=None): is called from various transaction like stock entry, reco etc args = { - "item_code": "ABC" or ["ABC", "XYZ"], - "warehouse": "XYZ" or ["XYZ", "ABC"], - "posting_date": "2012-12-12", - "posting_time": "12:00", - "sle": "name of reference Stock Ledger Entry" + "item_code": "ABC", + "warehouse": "XYZ", + "posting_date": "2012-12-12", + "posting_time": "12:00", + "sle": "name of reference Stock Ledger Entry" } """ args["name"] = args.get("sle", None) or "" From 9bf0d852ee3daf548eea812a715f6872cdaecb43 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Fri, 18 Jul 2025 16:10:32 +0530 Subject: [PATCH 20/67] fix: update get_data function to use item_query (cherry picked from commit 8a97b39028dcb20502aa1b1584c1be1c55f99503) --- .../report/stock_balance/stock_balance.js | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index a8b2bd1d782..c53e80096d3 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -40,7 +40,7 @@ frappe.query_reports["Stock Balance"] = { fieldtype: "MultiSelectList", width: "80", options: "Item", - get_data: function (txt) { + get_data: async function (txt) { let item_group = frappe.query_report.get_filter_value("item_group"); let filters = { @@ -48,7 +48,27 @@ frappe.query_reports["Stock Balance"] = { is_stock_item: 1, }; - return frappe.db.get_link_options("Item", txt, filters); + let { message: data } = await frappe.call({ + method: "erpnext.controllers.queries.item_query", + args: { + doctype: "Item", + txt: txt, + searchfield: "name", + start: 0, + page_len: 10, + filters: filters, + as_dict: 1, + }, + }); + + data = data.map(({ name, description }) => { + return { + value: name, + description: description, + }; + }); + + return data || []; }, }, { From d378e514926fe6b292cf6e92c0824211435cbb0f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Jul 2025 17:35:56 +0530 Subject: [PATCH 21/67] fix: valuation for rejected materials (cherry picked from commit b7039cc506b61249e4667b8f38ec80dd2928fb6c) # Conflicts: # erpnext/controllers/buying_controller.py --- erpnext/controllers/buying_controller.py | 42 +++++++++++++++++++ .../purchase_receipt/purchase_receipt.py | 18 +++++--- .../purchase_receipt/test_purchase_receipt.py | 41 ++++++++++++++++++ 3 files changed, 96 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 73d8a42c505..18c8e0243e7 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -324,11 +324,41 @@ class BuyingController(SubcontractingController): valuation_amount_adjustment = total_valuation_amount for i, item in enumerate(self.get("items")): +<<<<<<< HEAD if item.item_code and item.qty and item.item_code in stock_and_asset_items: item_proportion = ( flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount else flt(item.qty) / stock_and_asset_items_qty +======= + if item.item_code and (item.qty or item.get("rejected_qty")): + item_tax_amount, actual_tax_amount = 0.0, 0.0 + if i == (last_item_idx - 1): + item_tax_amount = total_valuation_amount + actual_tax_amount = total_actual_tax_amount + else: + # calculate item tax amount + item_tax_amount = self.get_item_tax_amount(item, tax_accounts) + total_valuation_amount -= item_tax_amount + + if total_actual_tax_amount: + actual_tax_amount = self.get_item_actual_tax_amount( + item, + total_actual_tax_amount, + stock_and_asset_items_amount, + stock_and_asset_items_qty, + ) + total_actual_tax_amount -= actual_tax_amount + + # This code is required here to calculate the correct valuation for stock items + if item.item_code not in stock_and_asset_items: + item.valuation_rate = 0.0 + continue + + # Item tax amount is the total tax amount applied on that item and actual tax type amount + item.item_tax_amount = flt( + item_tax_amount + actual_tax_amount, self.precision("item_tax_amount", item) +>>>>>>> b7039cc506 (fix: valuation for rejected materials) ) if i == (last_item_idx - 1): @@ -351,7 +381,19 @@ class BuyingController(SubcontractingController): if item.sales_incoming_rate: # for internal transfer net_rate = item.qty * item.sales_incoming_rate + if ( + not net_rate + and item.get("rejected_qty") + and frappe.get_single_value( + "Buying Settings", "set_valuation_rate_for_rejected_materials" + ) + ): + net_rate = item.rejected_qty * item.net_rate + qty_in_stock_uom = flt(item.qty * item.conversion_factor) + if not qty_in_stock_uom and item.get("rejected_qty"): + qty_in_stock_uom = flt(item.rejected_qty * item.conversion_factor) + if self.get("is_old_subcontracting_flow"): item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate) item.valuation_rate = ( diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index f38375f943f..d9597fe413a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -668,6 +668,10 @@ class PurchaseReceipt(BuyingController): warehouse_with_no_account = [] for d in self.get("items"): + remarks = self.get("remarks") or _("Accounting Entry for {0}").format( + "Asset" if d.is_fixed_asset else "Stock" + ) + if ( provisional_accounting_for_non_stock_items and d.item_code not in stock_items @@ -679,10 +683,6 @@ class PurchaseReceipt(BuyingController): d, gl_entries, self.posting_date, d.get("provisional_expense_account") ) elif flt(d.qty) and (flt(d.valuation_rate) or self.is_return): - remarks = self.get("remarks") or _("Accounting Entry for {0}").format( - "Asset" if d.is_fixed_asset else "Stock" - ) - if not ( (erpnext.is_perpetual_inventory_enabled(self.company) and d.item_code in stock_items) or (d.is_fixed_asset and not d.purchase_invoice) @@ -737,7 +737,7 @@ class PurchaseReceipt(BuyingController): make_amount_difference_entry(d) make_sub_contracting_gl_entries(d) make_divisional_loss_gl_entry(d, outgoing_amount) - elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or ( + elif (d.warehouse and d.qty and d.warehouse not in warehouse_with_no_account) or ( not frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials") and d.rejected_warehouse and d.rejected_warehouse not in warehouse_with_no_account @@ -750,10 +750,18 @@ class PurchaseReceipt(BuyingController): if d.rejected_qty and frappe.db.get_single_value( "Buying Settings", "set_valuation_rate_for_rejected_materials" ): + stock_asset_rbnb = ( + self.get_company_default("asset_received_but_not_billed") + if d.is_fixed_asset + else self.get_company_default("stock_received_but_not_billed") + ) + stock_value_diff = get_stock_value_difference(self.name, d.name, d.rejected_warehouse) stock_asset_account_name = warehouse_account[d.rejected_warehouse]["account"] make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name) + if not d.qty: + make_stock_received_but_not_billed_entry(d) if warehouse_with_no_account: frappe.msgprint( diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ae762093182..00fab425397 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4261,6 +4261,47 @@ class TestPurchaseReceipt(FrappeTestCase): frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 0) + def test_valuation_rate_for_rejected_materials_withoout_accepted_materials(self): + item = make_item("Test Item with Rej Material Valuation WO Accepted", {"is_stock_item": 1}) + company = "_Test Company with perpetual inventory" + + warehouse = create_warehouse( + "_Test In-ward Warehouse", + company="_Test Company with perpetual inventory", + ) + + rej_warehouse = create_warehouse( + "_Test Warehouse - Rejected Material", + company="_Test Company with perpetual inventory", + ) + + frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 1) + + frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 1) + + pr = make_purchase_receipt( + item_code=item.name, + qty=0, + rate=100, + company=company, + warehouse=warehouse, + rejected_qty=5, + rejected_warehouse=rej_warehouse, + ) + + gl_entry = frappe.get_all( + "GL Entry", filters={"debit": (">", 0), "voucher_no": pr.name}, pluck="name" + ) + + stock_value_diff = frappe.db.get_value( + "Stock Ledger Entry", + {"warehouse": rej_warehouse, "voucher_no": pr.name}, + "stock_value_difference", + ) + + self.assertTrue(gl_entry) + self.assertEqual(stock_value_diff, 500.00) + def test_no_valuation_rate_for_rejected_materials(self): item = make_item("Test Item with Rej Material No Valuation", {"is_stock_item": 1}) company = "_Test Company with perpetual inventory" From 4453e447dc143baa08ed9ed23bb2ae00318fb497 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 22 Jul 2025 09:57:07 +0530 Subject: [PATCH 22/67] chore: fix conflicts --- erpnext/controllers/buying_controller.py | 30 ------------------------ 1 file changed, 30 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 18c8e0243e7..6c51dddedb2 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -324,41 +324,11 @@ class BuyingController(SubcontractingController): valuation_amount_adjustment = total_valuation_amount for i, item in enumerate(self.get("items")): -<<<<<<< HEAD if item.item_code and item.qty and item.item_code in stock_and_asset_items: item_proportion = ( flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount else flt(item.qty) / stock_and_asset_items_qty -======= - if item.item_code and (item.qty or item.get("rejected_qty")): - item_tax_amount, actual_tax_amount = 0.0, 0.0 - if i == (last_item_idx - 1): - item_tax_amount = total_valuation_amount - actual_tax_amount = total_actual_tax_amount - else: - # calculate item tax amount - item_tax_amount = self.get_item_tax_amount(item, tax_accounts) - total_valuation_amount -= item_tax_amount - - if total_actual_tax_amount: - actual_tax_amount = self.get_item_actual_tax_amount( - item, - total_actual_tax_amount, - stock_and_asset_items_amount, - stock_and_asset_items_qty, - ) - total_actual_tax_amount -= actual_tax_amount - - # This code is required here to calculate the correct valuation for stock items - if item.item_code not in stock_and_asset_items: - item.valuation_rate = 0.0 - continue - - # Item tax amount is the total tax amount applied on that item and actual tax type amount - item.item_tax_amount = flt( - item_tax_amount + actual_tax_amount, self.precision("item_tax_amount", item) ->>>>>>> b7039cc506 (fix: valuation for rejected materials) ) if i == (last_item_idx - 1): From 176a124f1a8927c4fa5a4c4f3ee425956a0c2e38 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 22 Jul 2025 15:50:18 +0530 Subject: [PATCH 23/67] chore: restore removed import as it's used in v15 --- erpnext/stock/report/stock_balance/stock_balance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 08133583e2e..f828026f18d 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -7,6 +7,7 @@ from typing import Any, TypedDict import frappe from frappe import _ +from frappe.query_builder import Order from frappe.query_builder.functions import Coalesce from frappe.utils import add_days, cint, date_diff, flt, getdate from frappe.utils.nestedset import get_descendants_of From c2140625f572d6bb728b3cc4a01454778757a7da Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 22 Jul 2025 12:51:29 +0530 Subject: [PATCH 24/67] chore: fixed test case --- erpnext/controllers/buying_controller.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6c51dddedb2..fb5cc130f0b 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -310,9 +310,12 @@ class BuyingController(SubcontractingController): stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0 last_item_idx = 1 + + total_rejected_qty = 0 for d in self.get("items"): if d.item_code and d.item_code in stock_and_asset_items: stock_and_asset_items_qty += flt(d.qty) + total_rejected_qty += flt(d.get("rejected_qty", 0)) stock_and_asset_items_amount += flt(d.base_net_amount) last_item_idx = d.idx @@ -324,12 +327,19 @@ class BuyingController(SubcontractingController): valuation_amount_adjustment = total_valuation_amount for i, item in enumerate(self.get("items")): - if item.item_code and item.qty and item.item_code in stock_and_asset_items: - item_proportion = ( - flt(item.base_net_amount) / stock_and_asset_items_amount - if stock_and_asset_items_amount - else flt(item.qty) / stock_and_asset_items_qty - ) + if ( + item.item_code + and (item.qty or item.get("rejected_qty")) + and item.item_code in stock_and_asset_items + ): + if stock_and_asset_items_qty: + item_proportion = ( + flt(item.base_net_amount) / stock_and_asset_items_amount + if stock_and_asset_items_amount + else flt(item.qty) / stock_and_asset_items_qty + ) + elif total_rejected_qty: + item_proportion = flt(item.get("rejected_qty")) / flt(total_rejected_qty) if i == (last_item_idx - 1): item.item_tax_amount = flt( From fd441e1effa45acc5a64d85ca87ea108e03b3626 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 18:37:14 +0530 Subject: [PATCH 25/67] refactor: call hooks after gle & sle rename (backport #48706) (#48754) refactor: call hooks after gle & sle rename (#48706) (cherry picked from commit ed79adebc46d5088d6e569b63e18e9d7fb465e5d) Co-authored-by: Kitti U. @ Ecosoft --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 89b184e89d0..2e4f9db3539 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -462,4 +462,9 @@ def rename_temporarily_named_docs(doctype): f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s", (newname, now(), oldname), ) + + for hook_type in ("on_gle_rename", "on_sle_rename"): + for hook in frappe.get_hooks(hook_type): + frappe.call(hook, newname=newname, oldname=oldname) + frappe.db.commit() From 23180dad421a6bca46039d6ab4db63ad4ff68266 Mon Sep 17 00:00:00 2001 From: Shreyas Sojitra Date: Thu, 24 Jul 2025 10:05:03 +0000 Subject: [PATCH 26/67] fix: create job card for selected operations only (cherry picked from commit 27e534418843cacb1727fbcc5b77092f9a400bc4) --- .../manufacturing/doctype/work_order/work_order.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 3d2d5417604..92080c05baf 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -325,13 +325,18 @@ frappe.ui.form.on("Work Order", { return operations_data; }, }, - function (data) { + function () { + const selected_rows = dialog.fields_dict["operations"].grid.get_selected_children(); + if (selected_rows.length == 0) { + frappe.msgprint(__("Please select atleast one operation to create Job Card")); + return; + } frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", freeze: true, args: { work_order: frm.doc.name, - operations: data.operations, + operations: selected_rows, }, callback: function () { frm.reload_doc(); @@ -342,7 +347,7 @@ frappe.ui.form.on("Work Order", { __("Create") ); - dialog.fields_dict["operations"].grid.wrapper.find(".grid-add-row").hide(); + dialog.fields_dict["operations"].grid.grid_buttons.hide(); var pending_qty = 0; frm.doc.operations.forEach((data) => { From 0cb2c41cba2c83ba91be48b1f976788e1f019c55 Mon Sep 17 00:00:00 2001 From: Soni Karm <93865733+karm1000@users.noreply.github.com> Date: Sat, 26 Jul 2025 11:32:23 +0530 Subject: [PATCH 27/67] fix: enhance warehouse filter to support list and tuple values (#48755) --- erpnext/stock/report/stock_balance/stock_balance.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index f828026f18d..2535fc4096f 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -283,8 +283,11 @@ class StockBalanceReport: ) for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]: - if self.filters.get(fieldname): - query = query.where(table[fieldname] == self.filters.get(fieldname)) + if value := self.filters.get(fieldname): + if isinstance(value, list | tuple): + query = query.where(table[fieldname].isin(value)) + else: + query = query.where(table[fieldname] == value) return query.run(as_dict=True) From bd7de515b16bcd45e619caa3f3f929339a04ed9a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 28 Jul 2025 11:42:13 +0530 Subject: [PATCH 28/67] fix: error when trying to edit quantity of top most FG in bom creator --- erpnext/public/js/bom_configurator/bom_configurator.bundle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index 6d24751792c..d25ca212b41 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -417,7 +417,7 @@ class BOMConfigurator { doctype: doctype, docname: docname, qty: data.qty, - parent: node.data.parent_id, + parent: node.data.parent_id ? node.data.parent_id : this.frm.doc.name, }, callback: (r) => { node.data.qty = data.qty; From 01fcd98c84f0c5014b36a79bdca7db10aea63132 Mon Sep 17 00:00:00 2001 From: Assem Bahnasy Date: Mon, 28 Jul 2025 12:05:59 +0300 Subject: [PATCH 29/67] fix: Misclassification of Journal Voucher Entries in Customer Ledger Summary (#48041) * fix: miscalculation of Invoiced Amount, Paid Amount, and Credit Amount in Customer Ledger Summary * style: Apply ruff-format to customer_ledger_summary.py and ignore .venv/ * fix: Ensure .venv/ is ignored in .gitignore * chore: removing backportrc line * test: adding test_journal_voucher_against_return_invoice() * fix: fixed test_journal_voucher_against_return_invoice function * Revert .gitignore changes --------- Co-authored-by: ruthra kumar --- .../customer_ledger_summary.py | 26 ++- .../test_customer_ledger_summary.py | 154 ++++++++++++++++++ 2 files changed, 174 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index b90f922d82b..6f5fe349dd2 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -275,12 +275,25 @@ class PartyLedgerSummaryReport: if gle.posting_date < self.filters.from_date or gle.is_opening == "Yes": self.party_data[gle.party].opening_balance += amount else: - if amount > 0: - self.party_data[gle.party].invoiced_amount += amount - elif gle.voucher_no in self.return_invoices: - self.party_data[gle.party].return_amount -= amount + # Cache the party data reference to avoid repeated dictionary lookups + party_data = self.party_data[gle.party] + + # Check if this is a direct return invoice (most specific condition first) + if gle.voucher_no in self.return_invoices: + party_data.return_amount -= amount + # Check if this entry is against a return invoice + elif gle.against_voucher in self.return_invoices: + # For entries against return invoices, positive amounts are payments + if amount > 0: + party_data.paid_amount -= amount + else: + party_data.invoiced_amount += amount + # Normal transaction logic else: - self.party_data[gle.party].paid_amount -= amount + if amount > 0: + party_data.invoiced_amount += amount + else: + party_data.paid_amount -= amount out = [] for party, row in self.party_data.items(): @@ -289,7 +302,7 @@ class PartyLedgerSummaryReport: or row.invoiced_amount or row.paid_amount or row.return_amount - or row.closing_amount + or row.closing_balance # Fixed typo from closing_amount to closing_balance ): total_party_adjustment = sum( amount for amount in self.party_adjustment_details.get(party, {}).values() @@ -313,6 +326,7 @@ class PartyLedgerSummaryReport: gle.party, gle.voucher_type, gle.voucher_no, + gle.against_voucher, # For handling returned invoices (Credit/Debit Notes) gle.debit, gle.credit, gle.is_opening, diff --git a/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py index ce47793bbd5..ca9c62dac6c 100644 --- a/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py @@ -150,3 +150,157 @@ class TestCustomerLedgerSummary(FrappeTestCase, AccountsTestMixin): for field in expected_after_cr_and_payment: with self.subTest(field=field): self.assertEqual(report[0].get(field), expected_after_cr_and_payment.get(field)) + + def test_journal_voucher_against_return_invoice(self): + filters = {"company": self.company, "from_date": today(), "to_date": today()} + + # Create Sales Invoice of 10 qty at rate 100 (Amount: 1000.0) + si1 = self.create_sales_invoice(do_not_submit=True) + si1.save().submit() + + expected = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 1000.0, + "paid_amount": 0, + "return_amount": 0, + "closing_balance": 1000.0, + "currency": "INR", + "customer_name": "_Test Customer", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected: + with self.subTest(field=field): + actual_value = report[0].get(field) + expected_value = expected.get(field) + self.assertEqual( + actual_value, + expected_value, + f"Field {field} does not match expected value. " + f"Expected: {expected_value}, Got: {actual_value}", + ) + + # Create Payment Entry (Receive) for the first invoice + pe1 = self.create_payment_entry(si1.name, True) + pe1.paid_amount = 1000 # Full payment 1000.0 + pe1.save().submit() + + expected_after_payment = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 1000.0, + "paid_amount": 1000.0, + "return_amount": 0, + "closing_balance": 0.0, + "currency": "INR", + "customer_name": "_Test Customer", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected_after_payment: + with self.subTest(field=field): + actual_value = report[0].get(field) + expected_value = expected_after_payment.get(field) + self.assertEqual( + actual_value, + expected_value, + f"Field {field} does not match expected value. " + f"Expected: {expected_value}, Got: {actual_value}", + ) + + # Create Credit Note (return invoice) for first invoice (1000.0) + cr_note = self.create_credit_note(si1.name, do_not_submit=True) + cr_note.items[0].qty = -10 # 1 item of qty 10 at rate 100 (Amount: 1000.0) + cr_note.save().submit() + + expected_after_cr_note = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 1000.0, + "paid_amount": 1000.0, + "return_amount": 1000.0, + "closing_balance": -1000.0, + "currency": "INR", + "customer_name": "_Test Customer", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected_after_cr_note: + with self.subTest(field=field): + actual_value = report[0].get(field) + expected_value = expected_after_cr_note.get(field) + self.assertEqual( + actual_value, + expected_value, + f"Field {field} does not match expected value. " + f"Expected: {expected_value}, Got: {actual_value}", + ) + + # Create Payment Entry for the returned amount (1000.0) - Pay the customer back + pe2 = get_payment_entry("Sales Invoice", cr_note.name, bank_account=self.cash) + pe2.insert().submit() + + expected_after_cr_and_return_payment = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 1000.0, + "paid_amount": 0, + "return_amount": 1000.0, + "closing_balance": 0, + "currency": "INR", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected_after_cr_and_return_payment: + with self.subTest(field=field): + actual_value = report[0].get(field) + expected_value = expected_after_cr_and_return_payment.get(field) + self.assertEqual( + actual_value, + expected_value, + f"Field {field} does not match expected value. " + f"Expected: {expected_value}, Got: {actual_value}", + ) + + # Create second Sales Invoice of 10 qty at rate 100 (Amount: 1000.0) + si2 = self.create_sales_invoice(do_not_submit=True) + si2.save().submit() + + # Create Payment Entry (Receive) for the second invoice - payment (500.0) + pe3 = self.create_payment_entry(si2.name, True) + pe3.paid_amount = 500 # Partial payment 500.0 + pe3.save().submit() + + expected_after_cr_and_payment = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0.0, + "invoiced_amount": 2000.0, + "paid_amount": 500.0, + "return_amount": 1000.0, + "closing_balance": 500.0, + "currency": "INR", + "customer_name": "_Test Customer", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected_after_cr_and_payment: + with self.subTest(field=field): + actual_value = report[0].get(field) + expected_value = expected_after_cr_and_payment.get(field) + self.assertEqual( + actual_value, + expected_value, + f"Field {field} does not match expected value. " + f"Expected: {expected_value}, Got: {actual_value}", + ) From 584b442824861503c3deb564dfa417eb985d866e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 28 Jul 2025 12:52:48 +0530 Subject: [PATCH 30/67] fix: status in MR (material transfer) when using transit stock entries (cherry picked from commit baa612bc72bf16969601c74b0cd113a62834d225) --- .../stock/doctype/material_request/material_request.json | 3 ++- erpnext/stock/doctype/material_request/material_request.py | 2 +- .../stock/doctype/material_request/material_request_list.js | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 43cf7a7e527..60725b9ce9c 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -307,6 +307,7 @@ "fieldname": "transfer_status", "fieldtype": "Select", "label": "Transfer Status", + "no_copy": 1, "options": "\nNot Started\nIn Transit\nCompleted", "read_only": 1 }, @@ -364,7 +365,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2025-07-11 21:03:26.588307", + "modified": "2025-07-28 15:13:49.000037", "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 c37a783ca4d..409623a348f 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -35,6 +35,7 @@ class MaterialRequest(BuyingController): from erpnext.stock.doctype.material_request_item.material_request_item import MaterialRequestItem amended_from: DF.Link | None + buying_price_list: DF.Link | None company: DF.Link customer: DF.Link | None items: DF.Table[MaterialRequestItem] @@ -46,7 +47,6 @@ class MaterialRequest(BuyingController): naming_series: DF.Literal["MAT-MR-.YYYY.-"] per_ordered: DF.Percent per_received: DF.Percent - price_list: DF.Link | None scan_barcode: DF.Data | None schedule_date: DF.Date | None select_print_heading: DF.Link | None diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index 87174bc7513..ff928ea6a22 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -10,7 +10,11 @@ frappe.listview_settings["Material Request"] = { } else if (doc.transfer_status == "In Transit") { return [__("In Transit"), "yellow", "transfer_status,=,In Transit"]; } else if (doc.transfer_status == "Completed") { - return [__("Completed"), "green", "transfer_status,=,Completed"]; + if (doc.status == "Transferred") { + return [__("Completed"), "green", "transfer_status,=,Completed"]; + } else { + return [__("Partially Received"), "yellow", "per_ordered,<,100"]; + } } } else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) { return [__("Pending"), "orange", "per_ordered,=,0|docstatus,=,1"]; From 207d2ac63c1d400588eae00b4d3dcd9182968c57 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 28 Jul 2025 14:56:17 +0530 Subject: [PATCH 31/67] fix: incorrect GL entries (cherry picked from commit 4c273fcc99f5601abbe854ed903abc479e2642c2) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 9ec3555b49a..abbdd5523c8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -970,7 +970,7 @@ class PurchaseInvoice(BuyingController): self.get_provisional_accounts() for item in self.get("items"): - if flt(item.base_net_amount): + if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate): if item.item_code: frappe.get_cached_value("Item", item.item_code, "asset_category") From ad75754ca6e642a69c172bca862e8dd32b119bab Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 27 Jul 2025 18:45:23 +0530 Subject: [PATCH 32/67] fix: prevent concurrency issues (cherry picked from commit a186b1266d9f451025e5043cd73c886e7034c284) --- erpnext/stock/deprecated_serial_batch.py | 4 ++++ erpnext/stock/serial_batch_bundle.py | 1 + 2 files changed, 5 insertions(+) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 11e0d0964f0..825b1fe8d7e 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -141,6 +141,7 @@ class DeprecatedBatchNoValuation: & (sle.batch_no.isnotnull()) & (sle.is_cancelled == 0) ) + .for_update() .groupby(sle.batch_no) ) @@ -232,6 +233,7 @@ class DeprecatedBatchNoValuation: & (sle.is_cancelled == 0) & (sle.batch_no.isin(self.non_batchwise_valuation_batches)) ) + .for_update() .where(timestamp_condition) .groupby(sle.batch_no) ) @@ -278,6 +280,7 @@ class DeprecatedBatchNoValuation: .where(timestamp_condition) .orderby(sle.posting_datetime, order=Order.desc) .orderby(sle.creation, order=Order.desc) + .for_update() .limit(1) ) @@ -330,6 +333,7 @@ class DeprecatedBatchNoValuation: & (bundle.type_of_transaction.isin(["Inward", "Outward"])) & (bundle_child.batch_no.isin(self.non_batchwise_valuation_batches)) ) + .for_update() .where(timestamp_condition) .groupby(bundle_child.batch_no) ) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 0fbf8475103..21a45f352cd 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -723,6 +723,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation): & (parent.is_cancelled == 0) & (parent.type_of_transaction.isin(["Inward", "Outward"])) ) + .for_update() .groupby(child.batch_no) ) From 3e92ae8bd01feaed0e3eebcce54014080c98fd1a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 28 Jul 2025 15:40:13 +0530 Subject: [PATCH 33/67] fix: sql error in quality inspection (cherry picked from commit 062b245e3ffca8430157eeea637a6f602ad52843) --- erpnext/stock/doctype/quality_inspection/quality_inspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 58aa18359df..c7a315d2298 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -387,7 +387,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql( f""" - SELECT distinct item_code, item_name, item_group + SELECT distinct item_code, item_name FROM `tab{from_doctype}` WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s {qi_condition} {cond} {mcond} From 49befc1dfd8be8738145f49acb6b16289c9eab96 Mon Sep 17 00:00:00 2001 From: mithili Date: Tue, 8 Jul 2025 19:19:59 +0530 Subject: [PATCH 34/67] fix: set company as mandatory (cherry picked from commit 2de2ea9f58e7709b3cc40e609ee6b7c7db71c3bd) --- .../sales_partner_commission_summary.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js index 1150de86b80..97680bce435 100644 --- a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js +++ b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js @@ -3,6 +3,14 @@ frappe.query_reports["Sales Partner Commission Summary"] = { filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, { fieldname: "sales_partner", label: __("Sales Partner"), @@ -28,13 +36,6 @@ frappe.query_reports["Sales Partner Commission Summary"] = { fieldtype: "Date", default: frappe.datetime.get_today(), }, - { - fieldname: "company", - label: __("Company"), - fieldtype: "Link", - options: "Company", - default: frappe.defaults.get_user_default("Company"), - }, { fieldname: "customer", label: __("Customer"), From 622052b950e045a8c98c1030e5da85c17c6f7154 Mon Sep 17 00:00:00 2001 From: mithili Date: Tue, 8 Jul 2025 19:22:23 +0530 Subject: [PATCH 35/67] fix: get default company currency (cherry picked from commit 984947f333511922b9ca34e96986adec2dd9b37d) --- .../sales_partner_commission_summary.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py index 844aa86b52e..940f7db7dd8 100644 --- a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py +++ b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py @@ -35,6 +35,12 @@ def get_columns(filters): "fieldtype": "Link", "width": 140, }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Data", + "width": 80, + }, { "label": _("Territory"), "options": "Territory", @@ -43,7 +49,7 @@ def get_columns(filters): "width": 100, }, {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, - {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "options": "currency", "width": 120}, { "label": _("Sales Partner"), "options": "Sales Partner", @@ -61,6 +67,7 @@ def get_columns(filters): "label": _("Total Commission"), "fieldname": "total_commission", "fieldtype": "Currency", + "options": "currency", "width": 120, }, ] @@ -86,6 +93,9 @@ def get_entries(filters): filters, as_dict=1, ) + currency_company = frappe.get_cached_value("Company", filters.get("company"), "default_currency") + for row in entries: + row["currency"] = currency_company return entries From 8314059bf7051f21b463d4df984ec66ddaa467bb Mon Sep 17 00:00:00 2001 From: mithili Date: Wed, 9 Jul 2025 13:31:48 +0530 Subject: [PATCH 36/67] chore: update query to fetch company currency (cherry picked from commit 998617879caa2b6a4ad5433a986db8dee23fe52c) --- .../sales_partner_commission_summary.py | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py index 940f7db7dd8..e92b865fccd 100644 --- a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py +++ b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py @@ -36,10 +36,10 @@ def get_columns(filters): "width": 140, }, { - "label": _("Currency"), - "fieldname": "currency", - "fieldtype": "Data", - "width": 80, + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Data", + "width": 80, }, { "label": _("Territory"), @@ -49,7 +49,13 @@ def get_columns(filters): "width": 100, }, {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, - {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "options": "currency", "width": 120}, + { + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, { "label": _("Sales Partner"), "options": "Sales Partner", @@ -82,20 +88,19 @@ def get_entries(filters): entries = frappe.db.sql( """ SELECT - name, customer, territory, {} as posting_date, base_net_total as amount, - sales_partner, commission_rate, total_commission + doc.name, doc.customer, doc.territory, doc.{} as posting_date, doc.base_net_total as amount, + doc.sales_partner, doc.commission_rate, doc.total_commission, company.default_currency as currency FROM - `tab{}` + `tab{}` as doc + JOIN + `tabCompany` as company ON company.name = doc.company WHERE - {} and docstatus = 1 and sales_partner is not null - and sales_partner != '' order by name desc, sales_partner + {} and doc.docstatus = 1 and doc.sales_partner is not null + and doc.sales_partner != '' order by doc.name desc, doc.sales_partner """.format(date_field, filters.get("doctype"), conditions), filters, as_dict=1, ) - currency_company = frappe.get_cached_value("Company", filters.get("company"), "default_currency") - for row in entries: - row["currency"] = currency_company return entries @@ -105,15 +110,15 @@ def get_conditions(filters, date_field): for field in ["company", "customer", "territory"]: if filters.get(field): - conditions += f" and {field} = %({field})s" + conditions += f" and doc.{field} = %({field})s" if filters.get("sales_partner"): - conditions += " and sales_partner = %(sales_partner)s" + conditions += " and doc.sales_partner = %(sales_partner)s" if filters.get("from_date"): - conditions += f" and {date_field} >= %(from_date)s" + conditions += f" and doc.{date_field} >= %(from_date)s" if filters.get("to_date"): - conditions += f" and {date_field} <= %(to_date)s" + conditions += f" and doc.{date_field} <= %(to_date)s" return conditions From 8d25269de6a2b15aacab96886dc3ba798e8e7b27 Mon Sep 17 00:00:00 2001 From: mithili Date: Thu, 24 Jul 2025 18:40:34 +0530 Subject: [PATCH 37/67] refactor: remove join in sql (cherry picked from commit 9638151f9d1ef1e46e801d634d9080fc4021611c) --- .../sales_partner_commission_summary.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py index e92b865fccd..5e07eb5d8a8 100644 --- a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py +++ b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py @@ -83,21 +83,19 @@ def get_columns(filters): def get_entries(filters): date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date" - + company_currency = frappe.db.get_value("Company", filters.get("company"), "default_currency") conditions = get_conditions(filters, date_field) entries = frappe.db.sql( """ SELECT - doc.name, doc.customer, doc.territory, doc.{} as posting_date, doc.base_net_total as amount, - doc.sales_partner, doc.commission_rate, doc.total_commission, company.default_currency as currency + name, customer, territory, {} as posting_date, base_net_total as amount, + sales_partner, commission_rate, total_commission, '{}' as currency FROM - `tab{}` as doc - JOIN - `tabCompany` as company ON company.name = doc.company + `tab{}` WHERE - {} and doc.docstatus = 1 and doc.sales_partner is not null - and doc.sales_partner != '' order by doc.name desc, doc.sales_partner - """.format(date_field, filters.get("doctype"), conditions), + {} and docstatus = 1 and sales_partner is not null + and sales_partner != '' order by name desc, sales_partner + """.format(date_field, company_currency, filters.get("doctype"), conditions), filters, as_dict=1, ) @@ -110,15 +108,15 @@ def get_conditions(filters, date_field): for field in ["company", "customer", "territory"]: if filters.get(field): - conditions += f" and doc.{field} = %({field})s" + conditions += f" and {field} = %({field})s" if filters.get("sales_partner"): - conditions += " and doc.sales_partner = %(sales_partner)s" + conditions += " and sales_partner = %(sales_partner)s" if filters.get("from_date"): - conditions += f" and doc.{date_field} >= %(from_date)s" + conditions += f" and {date_field} >= %(from_date)s" if filters.get("to_date"): - conditions += f" and doc.{date_field} <= %(to_date)s" + conditions += f" and {date_field} <= %(to_date)s" return conditions From 9b59fb659b9a47d7c626dd34c4ad7d85552511c4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 17:26:17 +0530 Subject: [PATCH 38/67] fix: use db_set in email_campaign (backport #45679) (#48806) Bug fix in email_campaign's update_status function. (#45679) During the scheduler event of set_email_campaign_status, the function calling update_status isn't saving the modified status field. (cherry picked from commit 88e68bb803963d7d44c724c6dbf0be97a5037531) Co-authored-by: harshpwctech <84438948+harshpwctech@users.noreply.github.com> --- erpnext/crm/doctype/email_campaign/email_campaign.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index f8fce7ab697..6bfa4f8b3cb 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -78,11 +78,11 @@ class EmailCampaign(Document): end_date = getdate(self.end_date) today_date = getdate(today()) if start_date > today_date: - self.status = "Scheduled" + self.db_set("status", "Scheduled", update_modified=False) elif end_date >= today_date: - self.status = "In Progress" + self.db_set("status", "In Progress", update_modified=False) elif end_date < today_date: - self.status = "Completed" + self.db_set("status", "Completed", update_modified=False) # called through hooks to send campaign mails to leads From 193fbcba11646b21c6bf98b37072f7af875c0abc Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Mon, 28 Jul 2025 12:57:06 +0530 Subject: [PATCH 39/67] fix: remove alias for order by field (cherry picked from commit 048b87328bb7575a9b56805f1b4eb77c6aa483e5) --- .../item_wise_purchase_register.py | 6 +++--- .../item_wise_sales_register.py | 21 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 559ba4a70ab..c9d37b9ad1a 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -310,8 +310,8 @@ def apply_conditions(query, pi, pii, filters): def get_items(filters, additional_table_columns): doctype = "Purchase Invoice" - pi = frappe.qb.DocType(doctype).as_("invoice") - pii = frappe.qb.DocType(f"{doctype} Item").as_("invoice_item") + pi = frappe.qb.DocType("Purchase Invoice") + pii = frappe.qb.DocType("Purchase Invoice Item") Item = frappe.qb.DocType("Item") query = ( frappe.qb.from_(pi) @@ -375,7 +375,7 @@ def get_items(filters, additional_table_columns): if match_conditions: query += " and " + match_conditions - query = apply_order_by_conditions(query, filters) + query = apply_order_by_conditions(doctype, query, filters) return frappe.db.sql(query, params, as_dict=True) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index b4a72c5374f..5d853027a2b 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -394,15 +394,18 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None): return query -def apply_order_by_conditions(query, filters): +def apply_order_by_conditions(doctype, query, filters): + i = f"`tab{doctype}`" + ii = f"`tab{doctype} Item`" + if not filters.get("group_by"): - query += "order by invoice.posting_date desc, invoice_item.item_group desc" + query += f" order by {i}.posting_date desc, {ii}.item_group desc" elif filters.get("group_by") == "Invoice": - query += "order by invoice_item.parent desc" + query += f" order by {ii}.parent desc" elif filters.get("group_by") == "Item": - query += "order by invoice_item.item_code" + query += f" order by {ii}.item_code" elif filters.get("group_by") == "Item Group": - query += "order by invoice_item.item_group" + query += f" order by {ii}.item_group" elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"): filter_field = frappe.scrub(filters.get("group_by")) query += f" order by {filter_field} desc" @@ -412,8 +415,8 @@ def apply_order_by_conditions(query, filters): def get_items(filters, additional_query_columns, additional_conditions=None): doctype = "Sales Invoice" - si = frappe.qb.DocType("Sales Invoice").as_("invoice") - sii = frappe.qb.DocType("Sales Invoice Item").as_("invoice_item") + si = frappe.qb.DocType("Sales Invoice") + sii = frappe.qb.DocType("Sales Invoice Item") sip = frappe.qb.DocType("Sales Invoice Payment") item = frappe.qb.DocType("Item") @@ -487,12 +490,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None): from frappe.desk.reportview import build_match_conditions query, params = query.walk() - match_conditions = build_match_conditions("Sales Invoice") + match_conditions = build_match_conditions(doctype) if match_conditions: query += " and " + match_conditions - query = apply_order_by_conditions(query, filters) + query = apply_order_by_conditions(doctype, query, filters) return frappe.db.sql(query, params, as_dict=True) From 1ca81887ca029ac86f1b3071f895d9da6fe9cfa6 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Mon, 28 Jul 2025 14:52:03 +0530 Subject: [PATCH 40/67] chore: rename variable (cherry picked from commit 8fdda31e45e70141339aacdb3b9d62ab127b9bdc) --- .../item_wise_sales_register.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 5d853027a2b..073efdadb6a 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -395,17 +395,17 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None): def apply_order_by_conditions(doctype, query, filters): - i = f"`tab{doctype}`" - ii = f"`tab{doctype} Item`" + invoice = f"`tab{doctype}`" + invoice_item = f"`tab{doctype} Item`" if not filters.get("group_by"): - query += f" order by {i}.posting_date desc, {ii}.item_group desc" + query += f" order by {invoice}.posting_date desc, {invoice_item}.item_group desc" elif filters.get("group_by") == "Invoice": - query += f" order by {ii}.parent desc" + query += f" order by {invoice_item}.parent desc" elif filters.get("group_by") == "Item": - query += f" order by {ii}.item_code" + query += f" order by {invoice_item}.item_code" elif filters.get("group_by") == "Item Group": - query += f" order by {ii}.item_group" + query += f" order by {invoice_item}.item_group" elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"): filter_field = frappe.scrub(filters.get("group_by")) query += f" order by {filter_field} desc" From f8d1e5a0d383e44d137c71597ed218d86cde1662 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Fri, 18 Jul 2025 14:07:25 +0530 Subject: [PATCH 41/67] feat: add fetch_valuation_rate_for_internal_transaction in accounts settings --- .../doctype/accounts_settings/accounts_settings.json | 9 ++++++++- .../doctype/accounts_settings/accounts_settings.py | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 946e8d1a5cc..abb9b96020a 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -41,6 +41,7 @@ "show_payment_schedule_in_print", "item_price_settings_section", "maintain_same_internal_transaction_rate", + "fetch_valuation_rate_for_internal_transaction", "column_break_feyo", "maintain_same_rate_action", "role_to_override_stop_action", @@ -622,6 +623,12 @@ "fieldname": "drop_ar_procedures", "fieldtype": "Button", "label": "Drop Procedures" + }, + { + "default": "0", + "fieldname": "fetch_valuation_rate_for_internal_transaction", + "fieldtype": "Check", + "label": "Fetch Valuation Rate for Internal Transaction" } ], "icon": "icon-cog", @@ -629,7 +636,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-06-23 15:55:33.346398", + "modified": "2025-07-18 13:56:47.192437", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 362b235b2f6..f5a5eb70f96 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -48,6 +48,7 @@ class AccountsSettings(Document): enable_immutable_ledger: DF.Check enable_party_matching: DF.Check exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"] + fetch_valuation_rate_for_internal_transaction: DF.Check frozen_accounts_modifier: DF.Link | None general_ledger_remarks_length: DF.Int ignore_account_closing_balance: DF.Check From b23f7a9d913656560d3f4c781c277f3705c54f72 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Mon, 28 Jul 2025 12:48:57 +0530 Subject: [PATCH 42/67] fix: fetch item valuation rate for internal transactions --- erpnext/public/js/controllers/transaction.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 03cb670c4df..c9c589b6599 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -652,9 +652,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe me.apply_product_discount(d); } }, - () => { + async () => { // for internal customer instead of pricing rule directly apply valuation rate on item - if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) && me.frm.doc.represents_company === me.frm.doc.company) { + const fetch_valuation_rate_for_internal_transactions = await frappe.db.get_single_value( + "Accounts Settings", "fetch_valuation_rate_for_internal_transaction" + ); + if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) && fetch_valuation_rate_for_internal_transactions) { me.get_incoming_rate(item, me.frm.posting_date, me.frm.posting_time, me.frm.doc.doctype, me.frm.doc.company); } else { From 65a27066cc53ae5c71fbc246c57dacaf45b8b478 Mon Sep 17 00:00:00 2001 From: mithili Date: Mon, 28 Jul 2025 12:30:14 +0530 Subject: [PATCH 43/67] fix: avoid auto_repeat on duplicate (cherry picked from commit 2c54f49cbc530a9d258ef53a9430b6ccba80c44d) --- erpnext/selling/doctype/sales_order/sales_order.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 801648b4479..4a641fec492 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1462,6 +1462,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "Auto Repeat", + "no_copy": 1, "options": "Auto Repeat" }, { @@ -1664,7 +1665,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2025-03-03 16:49:00.676927", + "modified": "2025-07-28 12:14:29.760988", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", From bf5b6a540f660ef66041214a23559400bc350018 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Mon, 28 Jul 2025 18:25:16 +0530 Subject: [PATCH 44/67] fix: patch to enable fetch_valuation_rate_for_internal_transaction --- erpnext/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5f4c3672228..abad865a1ed 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -414,3 +414,4 @@ erpnext.patches.v15_0.update_pegged_currencies erpnext.patches.v15_0.set_company_on_pos_inv_merge_log erpnext.patches.v15_0.rename_price_list_to_buying_price_list erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice +execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction", 1) From ee8eb368e7a5ae4fa8bfcd3abb4d85068d1546a4 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Fri, 25 Jul 2025 13:30:05 +0530 Subject: [PATCH 45/67] fix: fetch payment term template from order (cherry picked from commit 5ed34d6ff9f50a25bde9bb6888463d2086d211f5) --- .../doctype/delivery_note/delivery_note.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 8aeb56e5d7c..299f36a6ab7 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -10,6 +10,7 @@ from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values from frappe.utils import cint, flt +from erpnext.accounts.party import get_due_date from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes from erpnext.controllers.selling_controller import SellingController @@ -917,8 +918,25 @@ def make_sales_invoice(source_name, target_doc=None, args=None): automatically_fetch_payment_terms = cint( frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") ) - if automatically_fetch_payment_terms and not doc.is_return: - doc.set_payment_schedule() + + if not doc.is_return: + so, doctype, fieldname = doc.get_order_details() + if ( + doc.linked_order_has_payment_terms(so, fieldname, doctype) + and not automatically_fetch_payment_terms + ): + payment_terms_template = frappe.db.get_value(doctype, so, "payment_terms_template") + doc.payment_terms_template = payment_terms_template + doc.due_date = get_due_date( + doc.posting_date, + "Customer", + doc.customer, + doc.company, + template_name=doc.payment_terms_template, + ) + + elif automatically_fetch_payment_terms: + doc.set_payment_schedule() return doc From 4fd32fc3bd499ee9caf9835573435238930f7758 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 25 Jan 2024 14:18:27 +0530 Subject: [PATCH 46/67] ci: Add fake passing tests when CI is skipped (#39555) (cherry picked from commit dfda5ad67320c000afdbcc7facde03a7cf695f6f) --- .github/workflows/patch_faux.yml | 22 +++++++++++++++++ .../workflows/server-tests-mariadb-faux.yml | 24 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/workflows/patch_faux.yml create mode 100644 .github/workflows/server-tests-mariadb-faux.yml diff --git a/.github/workflows/patch_faux.yml b/.github/workflows/patch_faux.yml new file mode 100644 index 00000000000..93d88bdd991 --- /dev/null +++ b/.github/workflows/patch_faux.yml @@ -0,0 +1,22 @@ +# Tests are skipped for these files but github doesn't allow "passing" hence this is required. + +name: Skipped Patch Test + +on: + pull_request: + paths: + - "**.js" + - "**.css" + - "**.md" + - "**.html" + - "**.csv" + +jobs: + test: + runs-on: ubuntu-latest + + name: Patch Test + + steps: + - name: Pass skipped tests unconditionally + run: "echo Skipped" diff --git a/.github/workflows/server-tests-mariadb-faux.yml b/.github/workflows/server-tests-mariadb-faux.yml new file mode 100644 index 00000000000..8334661cb0c --- /dev/null +++ b/.github/workflows/server-tests-mariadb-faux.yml @@ -0,0 +1,24 @@ +# Tests are skipped for these files but github doesn't allow "passing" hence this is required. + +name: Skipped Tests + +on: + pull_request: + paths: + - "**.js" + - "**.css" + - "**.md" + - "**.html" + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + container: [1, 2, 3, 4] + + name: Python Unit Tests + + steps: + - name: Pass skipped tests unconditionally + run: "echo Skipped" From 5840a242a65cf2ddad87944f1fd8de2a4bdf3203 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 28 Jul 2025 22:23:34 +0530 Subject: [PATCH 47/67] Potential fix for code scanning alert no. 13: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/server-tests-mariadb-faux.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/server-tests-mariadb-faux.yml b/.github/workflows/server-tests-mariadb-faux.yml index 8334661cb0c..b448b115081 100644 --- a/.github/workflows/server-tests-mariadb-faux.yml +++ b/.github/workflows/server-tests-mariadb-faux.yml @@ -1,6 +1,7 @@ # Tests are skipped for these files but github doesn't allow "passing" hence this is required. name: Skipped Tests +permissions: {} on: pull_request: From 9c3011cb9f7b2d46d74e92a94c9d1dfa27ab406c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 28 Jul 2025 22:23:42 +0530 Subject: [PATCH 48/67] Potential fix for code scanning alert no. 9: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/patch_faux.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/patch_faux.yml b/.github/workflows/patch_faux.yml index 93d88bdd991..7674631f41b 100644 --- a/.github/workflows/patch_faux.yml +++ b/.github/workflows/patch_faux.yml @@ -1,6 +1,7 @@ # Tests are skipped for these files but github doesn't allow "passing" hence this is required. name: Skipped Patch Test +permissions: none on: pull_request: From 1efdff0ad1b4f7de527d074c63777393278f72fd Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 29 Jul 2025 11:15:14 +0530 Subject: [PATCH 49/67] fix: over billed purchase receipt status (cherry picked from commit 15e354f76e2cf92e30381e81f870741434c20150) --- erpnext/controllers/status_updater.py | 2 +- .../stock/doctype/purchase_receipt/purchase_receipt_list.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index f16553ad908..e41ac0dd0ab 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -96,7 +96,7 @@ status_map = { ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], [ "Completed", - "eval:(self.per_billed == 100 and self.docstatus == 1) or (self.docstatus == 1 and self.grand_total == 0 and self.per_returned != 100 and self.is_return == 0)", + "eval:(self.per_billed >= 100 and self.docstatus == 1) or (self.docstatus == 1 and self.grand_total == 0 and self.per_returned != 100 and self.is_return == 0)", ], ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js index e95a1a2e9f8..d70b357d731 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js @@ -21,8 +21,8 @@ frappe.listview_settings["Purchase Receipt"] = { return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"]; } else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) { return [__("Partly Billed"), "yellow", "per_billed,<,100|docstatus,=,1"]; - } else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) { - return [__("Completed"), "green", "per_billed,=,100|docstatus,=,1"]; + } else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) >= 100) { + return [__("Completed"), "green", "per_billed,>=,100|docstatus,=,1"]; } }, From 18b007dbf2733bc7d3c723f9a93e580e8b307ab7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:48:12 +0530 Subject: [PATCH 50/67] Merge pull request #48805 from frappe/mergify/bp/version-15-hotfix/pr-48676 fix: missing account in GL entries (subcontracting) (backport #48676) --- erpnext/patches.txt | 1 + ..._entries_with_no_account_subcontracting.py | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 erpnext/patches/v15_0/repost_gl_entries_with_no_account_subcontracting.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index abad865a1ed..21de348b98f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -414,4 +414,5 @@ erpnext.patches.v15_0.update_pegged_currencies erpnext.patches.v15_0.set_company_on_pos_inv_merge_log erpnext.patches.v15_0.rename_price_list_to_buying_price_list erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice +erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction", 1) diff --git a/erpnext/patches/v15_0/repost_gl_entries_with_no_account_subcontracting.py b/erpnext/patches/v15_0/repost_gl_entries_with_no_account_subcontracting.py new file mode 100644 index 00000000000..b2208667ea6 --- /dev/null +++ b/erpnext/patches/v15_0/repost_gl_entries_with_no_account_subcontracting.py @@ -0,0 +1,25 @@ +import frappe + + +def execute(): + docs = frappe.get_all( + "GL Entry", + filters={"voucher_type": "Subcontracting Receipt", "account": ["is", "not set"], "is_cancelled": 0}, + pluck="voucher_no", + ) + for doc in docs: + doc = frappe.get_doc("Subcontracting Receipt", doc) + for item in doc.supplied_items: + account, cost_center = frappe.db.get_values( + "Subcontracting Receipt Item", item.reference_name, ["expense_account", "cost_center"] + )[0] + + if not item.expense_account: + item.db_set("expense_account", account) + if not item.cost_center: + item.db_set("cost_center", cost_center) + + doc.docstatus = 2 + doc.make_gl_entries_on_cancel() + doc.docstatus = 1 + doc.make_gl_entries() From ebda3965183b3749e31523ace3840264a6ba84c3 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Wed, 23 Jul 2025 15:46:08 +0530 Subject: [PATCH 51/67] fix: update subscription details patch (cherry picked from commit c7b1379a7f2cbc8b852330dd7e627acd7c86965c) --- erpnext/patches.txt | 2 +- erpnext/patches/v14_0/update_subscription_details.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 21de348b98f..20bf9870caa 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -320,7 +320,7 @@ erpnext.patches.v14_0.set_period_start_end_date_in_pcv erpnext.patches.v14_0.update_closing_balances #20-12-2024 execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts -erpnext.patches.v14_0.update_subscription_details +erpnext.patches.v14_0.update_subscription_details # 23-07-2025 execute:frappe.delete_doc("Report", "Tax Detail", force=True) erpnext.patches.v15_0.enable_all_leads erpnext.patches.v14_0.update_company_in_ldc diff --git a/erpnext/patches/v14_0/update_subscription_details.py b/erpnext/patches/v14_0/update_subscription_details.py index 58ab16d39e3..bd1a654c7c2 100644 --- a/erpnext/patches/v14_0/update_subscription_details.py +++ b/erpnext/patches/v14_0/update_subscription_details.py @@ -12,6 +12,7 @@ def execute(): subscription_invoice.invoice, "subscription", subscription_invoice.parent, + update_modified=False, ) - frappe.delete_doc_if_exists("DocType", "Subscription Invoice") + frappe.delete_doc_if_exists("DocType", "Subscription Invoice", force=1) From 7fd5b2b26a69502965222fef9116f3f5ac6cfa36 Mon Sep 17 00:00:00 2001 From: Logesh Periyasamy Date: Tue, 29 Jul 2025 13:05:29 +0530 Subject: [PATCH 52/67] feat: show opening/closing balance in cash flow report (#47877) * feat: add checkbox to carryforward opening balance * fix: ignore period closing voucher * chore: rename filter check box * feat: add total for opening and closing balance * fix: update section name * fix: remove section rename --------- Co-authored-by: venkat102 (cherry picked from commit 88b9f8d68cadcf379ae154d1a772735cb0b3bda7) --- .../accounts/report/cash_flow/cash_flow.js | 19 ++- .../accounts/report/cash_flow/cash_flow.py | 154 +++++++++++++++++- 2 files changed, 163 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/report/cash_flow/cash_flow.js b/erpnext/accounts/report/cash_flow/cash_flow.js index bc76ee0a114..6c44c07a508 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.js +++ b/erpnext/accounts/report/cash_flow/cash_flow.js @@ -14,9 +14,16 @@ erpnext.utils.add_dimensions("Cash Flow", 10); frappe.query_reports["Cash Flow"]["filters"].splice(8, 1); -frappe.query_reports["Cash Flow"]["filters"].push({ - fieldname: "include_default_book_entries", - label: __("Include Default FB Entries"), - fieldtype: "Check", - default: 1, -}); +frappe.query_reports["Cash Flow"]["filters"].push( + { + fieldname: "include_default_book_entries", + label: __("Include Default FB Entries"), + fieldtype: "Check", + default: 1, + }, + { + fieldname: "show_opening_and_closing_balance", + label: __("Show Opening and Closing Balance"), + fieldtype: "Check", + } +); diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index c647c5b2e4f..0ee0cb14edb 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -2,9 +2,13 @@ # For license information, please see license.txt +from datetime import timedelta + import frappe from frappe import _ -from frappe.utils import cstr +from frappe.query_builder import DocType +from frappe.utils import cstr, flt +from pypika import Order from erpnext.accounts.report.financial_statements import ( get_columns, @@ -12,6 +16,7 @@ from erpnext.accounts.report.financial_statements import ( get_data, get_filtered_list_for_consolidated_report, get_period_list, + set_gl_entries_by_account, ) from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( get_net_profit_loss, @@ -119,10 +124,20 @@ def execute(filters=None): filters, ) - add_total_row_account( + net_change_in_cash = add_total_row_account( data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters ) - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company, True) + + if filters.show_opening_and_closing_balance: + show_opening_and_closing_balance(data, period_list, company_currency, net_change_in_cash, filters) + + columns = get_columns( + filters.periodicity, + period_list, + filters.accumulated_values, + filters.company, + True, + ) chart = get_chart_data(columns, data, company_currency) @@ -255,6 +270,137 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data, out.append(total_row) out.append({}) + return total_row + + +def show_opening_and_closing_balance(out, period_list, currency, net_change_in_cash, filters): + opening_balance = { + "section_name": "Opening", + "section": "Opening", + "currency": currency, + } + closing_balance = { + "section_name": "Closing (Opening + Total)", + "section": "Closing (Opening + Total)", + "currency": currency, + } + + opening_amount = get_opening_balance(filters.company, period_list, filters) or 0.0 + running_total = opening_amount + + for i, period in enumerate(period_list): + key = period["key"] + change = net_change_in_cash.get(key, 0.0) + + opening_balance[key] = opening_amount if i == 0 else running_total + running_total += change + closing_balance[key] = running_total + + opening_balance["total"] = opening_balance[period_list[0]["key"]] + closing_balance["total"] = closing_balance[period_list[-1]["key"]] + + out.extend([opening_balance, net_change_in_cash, closing_balance, {}]) + + +def get_opening_balance(company, period_list, filters): + from copy import deepcopy + + cash_value = {} + account_types = get_cash_flow_accounts() + net_profit_loss = 0.0 + + local_filters = deepcopy(filters) + local_filters.start_date, local_filters.end_date = get_opening_range_using_fiscal_year( + company, period_list + ) + + for section in account_types: + section_name = section.get("section_name") + cash_value.setdefault(section_name, 0.0) + + if section_name == "Operations": + net_profit_loss += get_net_income(company, period_list, local_filters) + + for account in section.get("account_types", []): + account_type = account.get("account_type") + local_filters.account_type = account_type + + amount = get_account_type_based_gl_data(company, local_filters) or 0.0 + + if account_type == "Depreciation": + cash_value[section_name] += amount * -1 + else: + cash_value[section_name] += amount + + return sum(cash_value.values()) + net_profit_loss + + +def get_net_income(company, period_list, filters): + gl_entries_by_account_for_income, gl_entries_by_account_for_expense = {}, {} + income, expense = 0.0, 0.0 + from_date, to_date = get_opening_range_using_fiscal_year(company, period_list) + + for root_type in ["Income", "Expense"]: + for root in frappe.db.sql( + """select lft, rgt from tabAccount + where root_type=%s and ifnull(parent_account, '') = ''""", + root_type, + as_dict=1, + ): + set_gl_entries_by_account( + company, + from_date, + to_date, + filters, + gl_entries_by_account_for_income + if root_type == "Income" + else gl_entries_by_account_for_expense, + root.lft, + root.rgt, + root_type=root_type, + ignore_closing_entries=True, + ) + + for entries in gl_entries_by_account_for_income.values(): + for entry in entries: + if entry.posting_date <= to_date: + amount = (entry.debit - entry.credit) * -1 + income = flt((income + amount), 2) + + for entries in gl_entries_by_account_for_expense.values(): + for entry in entries: + if entry.posting_date <= to_date: + amount = entry.debit - entry.credit + expense = flt((expense + amount), 2) + + return income - expense + + +def get_opening_range_using_fiscal_year(company, period_list): + first_from_date = period_list[0]["from_date"] + previous_day = first_from_date - timedelta(days=1) + + # Get the earliest fiscal year for the company + + FiscalYear = DocType("Fiscal Year") + FiscalYearCompany = DocType("Fiscal Year Company") + + earliest_fy = ( + frappe.qb.from_(FiscalYear) + .join(FiscalYearCompany) + .on(FiscalYearCompany.parent == FiscalYear.name) + .select(FiscalYear.year_start_date) + .where(FiscalYearCompany.company == company) + .orderby(FiscalYear.year_start_date, order=Order.asc) + .limit(1) + ).run(as_dict=True) + + if not earliest_fy: + frappe.throw(_("Not able to find the earliest Fiscal Year for the given company.")) + + company_start_date = earliest_fy[0]["year_start_date"] + return company_start_date, previous_day + def get_report_summary(summary_data, currency): report_summary = [] @@ -276,7 +422,7 @@ def get_chart_data(columns, data, currency): for section in data if section.get("parent_section") is None and section.get("currency") ] - datasets = datasets[:-1] + datasets = datasets[:-2] chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"} From ae945b2e6ff18e3aad38353164fd4f804f5f9e50 Mon Sep 17 00:00:00 2001 From: Vishist Singh Solanki Date: Fri, 18 Jul 2025 16:07:14 +0000 Subject: [PATCH 53/67] fix: prevent negative scrap quantity in Job Card (#48545) (cherry picked from commit 94ec76545c4374bc4a898c29fac9fc0ee5527d46) --- .../doctype/job_card_scrap_item/job_card_scrap_item.json | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json b/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json index 9e9f1c4c89f..5fb65698235 100644 --- a/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json +++ b/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json @@ -51,6 +51,7 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Qty", + "non_negative": 1, "reqd": 1 }, { From 2073e9861314144518a00c6010156b1a8362828d Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Tue, 15 Jul 2025 13:23:23 +0530 Subject: [PATCH 54/67] fix(sales-order): disallow address edits after sales order is submitted (cherry picked from commit daac7c589b7030f1d61299dd52d4b8fcbc0985af) --- erpnext/selling/doctype/sales_order/sales_order.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 4a641fec492..a3219cac8f0 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -358,7 +358,6 @@ "options": "fa fa-bullhorn" }, { - "allow_on_submit": 1, "fieldname": "customer_address", "fieldtype": "Link", "hide_days": 1, @@ -439,7 +438,6 @@ "width": "50%" }, { - "allow_on_submit": 1, "fieldname": "shipping_address_name", "fieldtype": "Link", "hide_days": 1, From d4fae00b803ec4e31d4ba1253f1425070c743533 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Thu, 24 Jul 2025 14:51:28 +0530 Subject: [PATCH 55/67] fix: set letter head from company if exists (cherry picked from commit d163da171fcc947f8170cb60fa1f4f56cc1363fa) --- .../accounts/doctype/journal_entry/journal_entry.js | 1 + .../accounts/doctype/payment_entry/payment_entry.js | 1 + erpnext/accounts/doctype/pos_invoice/pos_invoice.js | 1 + erpnext/accounts/doctype/pos_profile/pos_profile.js | 1 + .../process_statement_of_accounts.js | 1 + erpnext/public/js/controllers/buying.js | 1 + erpnext/public/js/utils.js | 10 ++++++++++ .../stock/doctype/material_request/material_request.js | 1 + .../subcontracting_order/subcontracting_order.js | 4 ++++ 9 files changed, 21 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index f2f9f70e75d..f04bf6b22b3 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -163,6 +163,7 @@ frappe.ui.form.on("Journal Entry", { }); erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + erpnext.utils.set_letter_head(frm); }, voucher_type: function (frm) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 47ffc5719e4..03dfd724229 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -273,6 +273,7 @@ frappe.ui.form.on("Payment Entry", { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + erpnext.utils.set_letter_head(frm); }, contact_person: function (frm) { diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index ae2b0970d2f..2f170631226 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -14,6 +14,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex } company() { + erpnext.utils.set_letter_head(this.frm); erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); this.frm.set_value("set_warehouse", ""); this.frm.set_value("taxes_and_charges", ""); diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index 31f0f0725a6..2a5290e3f45 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -135,6 +135,7 @@ frappe.ui.form.on("POS Profile", { company: function (frm) { frm.trigger("toggle_display_account_head"); erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + erpnext.utils.set_letter_head(frm); }, toggle_display_account_head: function (frm) { diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js index 57d0c59329c..f52d9eea0ac 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js @@ -92,6 +92,7 @@ frappe.ui.form.on("Process Statement Of Accounts", { frm.set_value("account", ""); frm.set_value("cost_center", ""); frm.set_value("project", ""); + erpnext.utils.set_letter_head(frm); }, report: function (frm) { let filters = { diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 8b5c8e0fe59..a1675be8954 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -176,6 +176,7 @@ erpnext.buying = { this.frm.set_value("shipping_address", r.message.shipping_address || ""); }, }); + erpnext.utils.set_letter_head(this.frm) } supplier_address() { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 19a3f38d1e2..1cf3275a530 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -449,6 +449,16 @@ $.extend(erpnext.utils, { }); return fiscal_year; }, + + set_letter_head: function (frm) { + if (frm.fields_dict.letter_head) { + frappe.db.get_value("Company", frm.doc.company, "default_letter_head").then((res) => { + if (res.message?.default_letter_head) { + frm.set_value("letter_head", res.message.default_letter_head); + } + }); + } + }, }); erpnext.utils.select_alternate_items = function (opts) { diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 5208b947288..8a813dfadf3 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -84,6 +84,7 @@ frappe.ui.form.on("Material Request", { company: function (frm) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + erpnext.utils.set_letter_head(frm); }, onload_post_render: function (frm) { diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index bf0c8dc53f8..a185a1c7488 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -192,6 +192,10 @@ frappe.ui.form.on("Subcontracting Order", { }); }, + company: function (frm) { + erpnext.utils.set_letter_head(frm); + }, + get_materials_from_supplier: function (frm) { let sco_rm_details = []; From e1d7ec906f3d0c6542cc26e32de9c3fc1927b84c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 08:53:25 +0000 Subject: [PATCH 56/67] feat: Add non-negative constraint to workstation cost fields (backport #48557) (#48826) * feat: Add non-negative constraint to workstation cost fields (cherry picked from commit a2bb55757040eebe963291e5f9fc106147e86dde) # Conflicts: # erpnext/manufacturing/doctype/workstation/workstation.json * fix: Add non-negative constraint to job capacity field in workstation (cherry picked from commit 92a12d7feac2c3fc537ff97e7cbc99027876c83a) # Conflicts: # erpnext/manufacturing/doctype/workstation/workstation.json * chore: resolve conflicts --------- Co-authored-by: KerollesFathy Co-authored-by: Mihir Kandoi --- .../manufacturing/doctype/workstation/workstation.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index 4758e5d3588..cf83e1f26f4 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -72,6 +72,7 @@ "fieldname": "hour_rate_electricity", "fieldtype": "Currency", "label": "Electricity Cost", + "non_negative": 1, "oldfieldname": "hour_rate_electricity", "oldfieldtype": "Currency" }, @@ -81,6 +82,7 @@ "fieldname": "hour_rate_consumable", "fieldtype": "Currency", "label": "Consumable Cost", + "non_negative": 1, "oldfieldname": "hour_rate_consumable", "oldfieldtype": "Currency" }, @@ -94,6 +96,7 @@ "fieldname": "hour_rate_rent", "fieldtype": "Currency", "label": "Rent Cost", + "non_negative": 1, "oldfieldname": "hour_rate_rent", "oldfieldtype": "Currency" }, @@ -103,6 +106,7 @@ "fieldname": "hour_rate_labour", "fieldtype": "Currency", "label": "Wages", + "non_negative": 1, "oldfieldname": "hour_rate_labour", "oldfieldtype": "Currency" }, @@ -138,6 +142,7 @@ "fieldname": "production_capacity", "fieldtype": "Int", "label": "Job Capacity", + "non_negative": 1, "reqd": 1 }, { @@ -240,7 +245,7 @@ "idx": 1, "image_field": "on_status_image", "links": [], - "modified": "2024-06-20 14:17:13.806609", + "modified": "2025-07-13 16:02:13.615001", "modified_by": "Administrator", "module": "Manufacturing", "name": "Workstation", @@ -260,6 +265,7 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "ASC", From 5a82b723c29acbdfdc57626aa4c3b52eb5bf321f Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 29 Jul 2025 15:17:30 +0530 Subject: [PATCH 57/67] fix: post gl entry on completion date of asset repair --- erpnext/assets/doctype/asset_repair/asset_repair.json | 6 ++++-- erpnext/assets/doctype/asset_repair/asset_repair.py | 10 +++++----- .../assets/doctype/asset_repair/test_asset_repair.py | 3 ++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index a28afa280de..16f3da9b988 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -74,6 +74,7 @@ "fieldname": "completion_date", "fieldtype": "Datetime", "label": "Completion Date", + "mandatory_depends_on": "eval:doc.repair_status==\"Completed\"", "no_copy": 1 }, { @@ -249,7 +250,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-06-29 22:30:00.589597", + "modified": "2025-07-29 15:14:34.044564", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", @@ -287,10 +288,11 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], "title_field": "asset_name", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 2ac2803fd53..358945edf87 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -70,7 +70,7 @@ class AssetRepair(AccountsController): ) def validate_dates(self): - if self.completion_date and (self.failure_date > self.completion_date): + if self.completion_date and (getdate(self.failure_date) > getdate(self.completion_date)): frappe.throw( _("Completion Date can not be before Failure Date. Please adjust the dates accordingly.") ) @@ -303,7 +303,7 @@ class AssetRepair(AccountsController): "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, - "posting_date": getdate(), + "posting_date": self.completion_date, "against_voucher_type": "Purchase Invoice", "against_voucher": self.purchase_invoice, "company": self.company, @@ -322,7 +322,7 @@ class AssetRepair(AccountsController): "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, - "posting_date": getdate(), + "posting_date": self.completion_date, "company": self.company, }, item=self, @@ -356,7 +356,7 @@ class AssetRepair(AccountsController): "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, - "posting_date": getdate(), + "posting_date": self.completion_date, "company": self.company, }, item=self, @@ -373,7 +373,7 @@ class AssetRepair(AccountsController): "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, - "posting_date": getdate(), + "posting_date": self.completion_date, "against_voucher_type": "Stock Entry", "against_voucher": stock_entry.name, "company": self.company, diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 5c0a18baccb..f5630b48d64 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -4,7 +4,7 @@ import unittest import frappe -from frappe.utils import add_months, flt, get_first_day, nowdate, nowtime, today +from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today from erpnext.assets.doctype.asset.asset import ( get_asset_account, @@ -359,6 +359,7 @@ def create_asset_repair(**args): if args.submit: asset_repair.repair_status = "Completed" + asset_repair.completion_date = add_days(args.failure_date, 1) asset_repair.cost_center = frappe.db.get_value("Company", asset.company, "cost_center") if args.stock_consumption: From 3739e2ca5a3d8c205ce094d3cc53a3fb6508c376 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Sat, 26 Jul 2025 12:56:10 +0530 Subject: [PATCH 58/67] fix: attribute error in payment entry (cherry picked from commit dc841fe6618ba3e967b91b14758aa830ed93f185) --- .../doctype/payment_entry/payment_entry.py | 5 +++-- erpnext/accounts/utils.py | 15 +++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index de4305f86e0..6e8e5e02451 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1487,13 +1487,14 @@ class PaymentEntry(AccountsController): "voucher_no": self.name, "voucher_detail_no": invoice.name, } - if invoice.reconcile_effect_on: posting_date = invoice.reconcile_effect_on else: # For backwards compatibility # Supporting reposting on payment entries reconciled before select field introduction - posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date) + posting_date = get_reconciliation_effect_date( + invoice.reference_doctype, invoice.reference_name, self.company, self.posting_date + ) frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date) dr_or_cr, account = self.get_dr_and_account_for_advances(invoice) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 30081e275ff..ca1108f9340 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -717,7 +717,9 @@ def update_reference_in_payment_entry( # Update Reconciliation effect date in reference if payment_entry.book_advance_payments_in_separate_party_account: - reconcile_on = get_reconciliation_effect_date(d, payment_entry.company, payment_entry.posting_date) + reconcile_on = get_reconciliation_effect_date( + d.against_voucher_type, d.against_voucher, payment_entry.company, payment_entry.posting_date + ) reference_details.update({"reconcile_effect_on": reconcile_on}) if d.voucher_detail_no: @@ -771,20 +773,21 @@ def update_reference_in_payment_entry( return row, update_advance_paid -def get_reconciliation_effect_date(reference, company, posting_date): +def get_reconciliation_effect_date(against_voucher_type, against_voucher, company, posting_date): reconciliation_takes_effect_on = frappe.get_cached_value( "Company", company, "reconciliation_takes_effect_on" ) + # default + reconcile_on = posting_date + if reconciliation_takes_effect_on == "Advance Payment Date": reconcile_on = posting_date elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": date_field = "posting_date" - if reference.against_voucher_type in ["Sales Order", "Purchase Order"]: + if against_voucher_type in ["Sales Order", "Purchase Order"]: date_field = "transaction_date" - reconcile_on = frappe.db.get_value( - reference.against_voucher_type, reference.against_voucher, date_field - ) + reconcile_on = frappe.db.get_value(against_voucher_type, against_voucher, date_field) if getdate(reconcile_on) < getdate(posting_date): reconcile_on = posting_date elif reconciliation_takes_effect_on == "Reconciliation Date": From 1697ac0b579184ade67c24208245a77e08d25e68 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 29 Jul 2025 16:11:46 +0530 Subject: [PATCH 59/67] chore: added test case for reconciliation_effect_date (cherry picked from commit f7ee9ee967408f3355446bc474dd4a25f19a106d) --- .../test_payment_reconciliation.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 061bbf556fc..bd5ba42b0b6 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -1714,6 +1714,67 @@ class TestPaymentReconciliation(FrappeTestCase): ) self.assertEqual(len(pl_entries), 3) + def test_advance_payment_reconciliation_date_for_older_date(self): + old_settings = frappe.db.get_value( + "Company", + self.company, + [ + "reconciliation_takes_effect_on", + "default_advance_paid_account", + "book_advance_payments_in_separate_party_account", + ], + as_dict=True, + ) + frappe.db.set_value( + "Company", + self.company, + { + "book_advance_payments_in_separate_party_account": 1, + "default_advance_paid_account": self.advance_payable_account, + "reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance", + }, + ) + + self.supplier = "_Test Supplier" + + pi1 = self.create_purchase_invoice(qty=10, rate=100) + po = self.create_purchase_order(qty=10, rate=100) + + pay = get_payment_entry(po.doctype, po.name) + pay.paid_amount = 1000 + pay.save().submit() + + pr = frappe.new_doc("Payment Reconciliation") + pr.company = self.company + pr.party_type = "Supplier" + pr.party = self.supplier + pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company) + pr.default_advance_account = self.advance_payable_account + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.allocation[0].allocated_amount = 100 + pr.reconcile() + + pay.reload() + self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date)) + + # test setting of date if not available + frappe.db.set_value("Payment Entry Reference", pay.references[1].name, "reconcile_effect_on", None) + pay.reload() + pay.cancel() + + pay.reload() + pi1.reload() + po.reload() + + self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date)) + pi1.cancel() + po.cancel() + + frappe.db.set_value("Company", self.company, old_settings) + def test_advance_payment_reconciliation_against_journal_for_customer(self): frappe.db.set_value( "Company", From 424baed077b365a1b21eb8602e210a5a49ce6b6b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Jul 2025 16:26:57 +0530 Subject: [PATCH 60/67] fix: ignore is overridden by transaction.js upon clicking cancel which overrides with 'Serial and Batch Bundle' (cherry picked from commit cf70147c0d8e7324cc5261a59c398c34039f09dc) --- erpnext/buying/doctype/purchase_order/purchase_order.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 7925d59d25a..bdd5e89dda0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -12,7 +12,6 @@ erpnext.buying.setup_buying_controller(); frappe.ui.form.on("Purchase Order", { setup: function (frm) { - frm.ignore_doctypes_on_cancel_all = ["Unreconcile Payment", "Unreconcile Payment Entries"]; if (frm.doc.is_old_subcontracting_flow) { frm.set_query("reserve_warehouse", "supplied_items", function () { return { @@ -140,6 +139,10 @@ frappe.ui.form.on("Purchase Order", { }, onload: function (frm) { + var ignore_list = ["Unreconcile Payment", "Unreconcile Payment Entries"]; + frm.ignore_doctypes_on_cancel_all = Object.hasOwn(frm, "ignore_doctypes_on_cancel_all") + ? frm.ignore_doctypes_on_cancel_all.concat(ignore_list) + : ignore_list; set_schedule_date(frm); if (!frm.doc.transaction_date) { frm.set_value("transaction_date", frappe.datetime.get_today()); From 36f22f929d784462f3a31754ceb9e8376a986c21 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 29 Jul 2025 17:39:04 +0530 Subject: [PATCH 61/67] fix: append finance book row only when calculate depreciation is checked --- erpnext/assets/doctype/asset/asset.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 019c97114fa..0adf4cc22a3 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -314,6 +314,9 @@ class Asset(AccountsController): ) def set_missing_values(self): + if not self.calculate_depreciation: + return + if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") @@ -1300,7 +1303,7 @@ def create_new_asset_after_split(asset, split_qty): ) new_asset.insert() - + print(".............") add_asset_activity( new_asset.name, _("Asset created after being split from Asset {0}").format(get_link_to_form("Asset", asset.name)), From 1be071683aaa68c44534d8c37c1baa088c56daa2 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 29 Jul 2025 17:39:48 +0530 Subject: [PATCH 62/67] test: test assets after split --- erpnext/assets/doctype/asset/test_asset.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 9c8db82f41b..07dc5c29b2e 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -445,6 +445,27 @@ class TestAsset(AssetSetup): self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") + def test_asset_splitting_without_depreciation(self): + asset = create_asset( + calculate_depreciation=0, + asset_quantity=10, + available_for_use_date="2020-01-01", + purchase_date="2020-01-01", + gross_purchase_amount=120000, + submit=1, + ) + + self.assertEqual(asset.asset_quantity, 10) + self.assertEqual(asset.gross_purchase_amount, 120000) + + new_asset = split_asset(asset.name, 2) + asset.load_from_db() + self.assertEqual(asset.asset_quantity, 8) + self.assertEqual(asset.gross_purchase_amount, 96000) + + self.assertEqual(new_asset.asset_quantity, 2) + self.assertEqual(new_asset.gross_purchase_amount, 24000) + def test_asset_splitting(self): asset = create_asset( calculate_depreciation=1, From bc1d3ea01775dc808e024805f2aa929390197d56 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 29 Jul 2025 17:43:21 +0530 Subject: [PATCH 63/67] chore: remove print statement --- erpnext/assets/doctype/asset/asset.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 0adf4cc22a3..ceb1c46dc6c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1303,7 +1303,6 @@ def create_new_asset_after_split(asset, split_qty): ) new_asset.insert() - print(".............") add_asset_activity( new_asset.name, _("Asset created after being split from Asset {0}").format(get_link_to_form("Asset", asset.name)), From b82aea4a87852a683e231c39709eb036857efb66 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 29 Jul 2025 17:49:46 +0530 Subject: [PATCH 64/67] fix: serial no warehouse for backdated stock reco (cherry picked from commit 1deedc766c78b7ef2179d831df7d667860e9b4fa) --- .../test_stock_reconciliation.py | 76 +++++++++++++++++++ erpnext/stock/serial_batch_bundle.py | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 1fcbd73b49a..51cf4a34943 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1514,6 +1514,82 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertTrue(len(stock_ledgers) == 2) + def test_serial_no_backdated_stock_reco(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + serial_item = self.make_item( + "Test Serial Item Stock Reco Backdated", + { + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "TSISRB.####", + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + se = make_stock_entry( + item_code=serial_item, + target=warehouse, + qty=1, + basic_rate=100, + use_serial_batch_fields=1, + ) + + serial_no = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)[0] + status = frappe.get_value( + "Serial No", + serial_no, + "status", + ) + + self.assertTrue(status == "Active") + + sr = create_stock_reconciliation( + item_code=serial_item, + warehouse=warehouse, + qty=1, + rate=200, + use_serial_batch_fields=1, + serial_no=serial_no, + ) + + sr.reload() + + status = frappe.get_value( + "Serial No", + serial_no, + "status", + ) + + self.assertTrue(status == "Active") + + make_stock_entry( + item_code=serial_item, + source=warehouse, + qty=1, + basic_rate=100, + use_serial_batch_fields=1, + ) + + status = frappe.get_value( + "Serial No", + serial_no, + "status", + ) + + self.assertFalse(status == "Active") + + sr.cancel() + + status = frappe.get_value( + "Serial No", + serial_no, + "status", + ) + + self.assertFalse(status == "Active") + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 21a45f352cd..3e3f2bcdf69 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -359,7 +359,7 @@ class SerialBatchBundle: self.update_serial_no_status_warehouse(self.sle, serial_nos) def update_serial_no_status_warehouse(self, sle, serial_nos): - warehouse = self.warehouse if sle.actual_qty > 0 else None + warehouse = sle.warehouse if sle.actual_qty > 0 else None if isinstance(serial_nos, str): serial_nos = [serial_nos] From 6a5041042eff7882099e5d5f7192ff39158d3eb0 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Wed, 23 Jul 2025 08:34:52 +0530 Subject: [PATCH 65/67] fix(pick list): make warehouse editable (cherry picked from commit f5beda48dcb984cf7df0d09d6360d2bb2a229164) --- erpnext/stock/doctype/pick_list/pick_list.js | 17 +++++++++++++++++ erpnext/stock/doctype/pick_list/pick_list.json | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index dea83560494..c72fa864960 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -21,6 +21,14 @@ frappe.ui.form.on("Pick List", { "Stock Entry": "Stock Entry", }; + frm.set_query("warehouse", "locations", () => { + return { + filters: { + company: frm.doc.company, + }, + }; + }); + frm.set_query("parent_warehouse", () => { return { filters: { @@ -91,6 +99,15 @@ frappe.ui.form.on("Pick List", { }); } }, + + pick_manually: function (frm) { + frm.fields_dict.locations.grid.update_docfield_property( + "warehouse", + "read_only", + !frm.doc.pick_manually + ); + }, + get_item_locations: (frm) => { // Button on the form frm.events.set_item_locations(frm, false); diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index e6449476971..69a1482d6d9 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -201,7 +201,7 @@ }, { "default": "0", - "description": "If enabled then system won't override the picked qty / batches / serial numbers.", + "description": "If enabled then system won't override the picked qty / batches / serial numbers / warehouse.", "fieldname": "pick_manually", "fieldtype": "Check", "label": "Pick Manually" @@ -247,7 +247,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2025-05-31 19:18:30.860044", + "modified": "2025-07-23 08:34:32.099673", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", From cfcd21d5c69ee7bef9d20ff9994fc52e34288c94 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Tue, 29 Jul 2025 16:03:10 +0530 Subject: [PATCH 66/67] fix: include empty values in user permission (cherry picked from commit f13d98fc7c77e03761efee90b3ad15e28efb7719) --- erpnext/accounts/utils.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index ca1108f9340..97b1bc9ab83 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -27,6 +27,7 @@ from frappe.utils import ( nowdate, ) from pypika import Order +from pypika.functions import Coalesce from pypika.terms import ExistsCriterion import erpnext @@ -2357,6 +2358,8 @@ def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15): def build_qb_match_conditions(doctype, user=None) -> list: match_filters = build_match_conditions(doctype, user, False) criterion = [] + apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions") + if match_filters: from frappe import qb @@ -2365,6 +2368,12 @@ def build_qb_match_conditions(doctype, user=None) -> list: for filter in match_filters: for d, names in filter.items(): fieldname = d.lower().replace(" ", "_") - criterion.append(_dt[fieldname].isin(names)) + field = _dt[fieldname] + + cond = field.isin(names) + if not apply_strict_user_permissions: + cond = (Coalesce(field, "") == "") | field.isin(names) + + criterion.append(cond) return criterion From 074a7065bed1d52c57c570fa1f1e853236fbba3f Mon Sep 17 00:00:00 2001 From: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com> Date: Tue, 29 Jul 2025 20:35:12 +0530 Subject: [PATCH 67/67] fix: update advance paid amount on unreconcile (cherry picked from commit 99f7eb38d383c0e5c42d048235d4d324f34bb84d) --- .../test_unreconcile_payment.py | 61 ++++++++++++++++++- .../unreconcile_payment.py | 15 +++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py index c058dbfa0b8..021703f6483 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py @@ -9,6 +9,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order @@ -17,6 +18,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): def setUp(self): self.create_company() self.create_customer() + self.create_supplier() self.create_usd_receivable_account() self.create_item() self.clear_old_entries() @@ -364,13 +366,13 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): # Assert 'Advance Paid' so.reload() pe.reload() - self.assertEqual(so.advance_paid, 100) + self.assertEqual(so.advance_paid, 0) self.assertEqual(len(pe.references), 0) self.assertEqual(pe.unallocated_amount, 100) pe.cancel() so.reload() - self.assertEqual(so.advance_paid, 100) + self.assertEqual(so.advance_paid, 0) def test_06_unreconcile_advance_from_payment_entry(self): self.enable_advance_as_liability() @@ -417,7 +419,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): so2.reload() pe.reload() self.assertEqual(so1.advance_paid, 150) - self.assertEqual(so2.advance_paid, 110) + self.assertEqual(so2.advance_paid, 0) self.assertEqual(len(pe.references), 1) self.assertEqual(pe.unallocated_amount, 110) @@ -468,3 +470,56 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): self.assertEqual(so.advance_paid, 1000) self.disable_advance_as_liability() + + def test_unreconcile_advance_from_journal_entry(self): + po = create_purchase_order( + company=self.company, + supplier=self.supplier, + item=self.item, + qty=1, + rate=100, + transaction_date=today(), + do_not_submit=False, + ) + + je = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": self.company, + "voucher_type": "Journal Entry", + "posting_date": po.transaction_date, + "multi_currency": True, + "accounts": [ + { + "account": "Creditors - _TC", + "party_type": "Supplier", + "party": po.supplier, + "debit_in_account_currency": 100, + "is_advance": "Yes", + "reference_type": po.doctype, + "reference_name": po.name, + }, + {"account": "Cash - _TC", "credit_in_account_currency": 100}, + ], + } + ) + je.save().submit() + po.reload() + self.assertEqual(po.advance_paid, 100) + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payment", + "company": self.company, + "voucher_type": je.doctype, + "voucher_no": je.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 1) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEqual([po.name], allocations) + unreconcile.save().submit() + + po.reload() + self.assertEqual(po.advance_paid, 0) diff --git a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py index e57b90f11f7..f7c826bf3fc 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py @@ -86,10 +86,25 @@ class UnreconcilePayment(Document): alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) if doc.doctype in get_advance_payment_doctypes(): + self.make_advance_payment_ledger(alloc) doc.set_total_advance_paid() frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) + def make_advance_payment_ledger(self, alloc): + if alloc.allocated_amount > 0: + doc = frappe.new_doc("Advance Payment Ledger Entry") + doc.company = self.company + doc.voucher_type = self.voucher_type + doc.voucher_no = self.voucher_no + doc.against_voucher_type = alloc.reference_doctype + doc.against_voucher_no = alloc.reference_name + doc.amount = -1 * alloc.allocated_amount + doc.event = "Unreconcile" + doc.currency = alloc.account_currency + doc.flags.ignore_permissions = 1 + doc.save() + @frappe.whitelist() def doc_has_references(doctype: str | None = None, docname: str | None = None):