From f4cdb4912676f811d3390f270c2d7b279e6c0bba Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Wed, 10 Sep 2025 18:27:39 +0300 Subject: [PATCH 01/47] fix: clear asset custodian when asset take back from employee without assign to another employee --- erpnext/assets/doctype/asset_movement/asset_movement.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index db4e7510670..b9fb331ae12 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -144,6 +144,10 @@ class AssetMovement(Document): if employee and employee != asset.custodian: frappe.db.set_value("Asset", asset_id, "custodian", employee) + + elif not employee and asset.custodian: + frappe.db.set_value("Asset", asset_id, "custodian", None) + if location and location != asset.location: frappe.db.set_value("Asset", asset_id, "location", location) From 4bc76be1304282357704c1a61f4aa59a6c782feb Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Wed, 10 Sep 2025 19:01:14 +0300 Subject: [PATCH 02/47] refactor: optimize asset location and custodian update logic to avoid multiple updates --- .../assets/doctype/asset_movement/asset_movement.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index b9fb331ae12..affc0cf1424 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -142,14 +142,18 @@ class AssetMovement(Document): def update_asset_location_and_custodian(self, asset_id, location, employee): asset = frappe.get_doc("Asset", asset_id) + updates = {} if employee and employee != asset.custodian: - frappe.db.set_value("Asset", asset_id, "custodian", employee) + updates["custodian"] = employee elif not employee and asset.custodian: - frappe.db.set_value("Asset", asset_id, "custodian", None) + updates["custodian"] = "" if location and location != asset.location: - frappe.db.set_value("Asset", asset_id, "location", location) + updates["location"] = location + + if updates: + frappe.db.set_value("Asset", asset_id, updates) def log_asset_activity(self, asset_id, location, employee): if location and employee: From 82386b18aa05ec3166942132fd42e6e5839bbe87 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Fri, 26 Sep 2025 17:51:26 +0530 Subject: [PATCH 03/47] fix: get unconsumed qty as per BOM qty (cherry picked from commit cf4b395ee38c72798d14e06d5eacad2dfdeaf2e3) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 1fe9134e42d..7ad2b9f3cd1 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2279,10 +2279,12 @@ class StockEntry(StockController): wo_item_qty = item.transferred_qty or item.required_qty - wo_qty_consumed = flt(wo_item_qty) - flt(item.consumed_qty) + wo_qty_unconsumed = flt(wo_item_qty) - flt(item.consumed_qty) wo_qty_to_produce = flt(work_order_qty) - flt(wo.produced_qty) + bom_qty_per_unit = item.required_qty / wo.qty # per-unit BOM qty - req_qty_each = (wo_qty_consumed) / (wo_qty_to_produce or 1) + req_qty_each = (wo_qty_unconsumed) / (wo_qty_to_produce or 1) + req_qty_each = min(req_qty_each, bom_qty_per_unit) qty = req_qty_each * flt(self.fg_completed_qty) From f548f0b2315da7a77c279856c4a69ee4e0d920b9 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Tue, 30 Sep 2025 00:24:34 +0530 Subject: [PATCH 04/47] test: required_qty clamping in manufacture entry (cherry picked from commit 34d2c8d9c2dcd04229a0bb58bb05cd662cf7b092) # Conflicts: # erpnext/manufacturing/doctype/work_order/test_work_order.py --- .../doctype/work_order/test_work_order.py | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index c77d34950ec..6716e0855e9 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2828,6 +2828,111 @@ class TestWorkOrder(FrappeTestCase): wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10) ) +<<<<<<< HEAD +======= + def test_allow_additional_material_transfer(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 50) + wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2) + for row in wo_order.required_items: + make_stock_entry_test_record( + item_code=row.item_code, + target=row.source_warehouse, + qty=row.required_qty * 2, + basic_rate=100, + ) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 2)) + stock_entry.insert() + stock_entry.submit() + + wo_order.reload() + self.assertEqual(wo_order.material_transferred_for_manufacturing, 2) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1)) + stock_entry.insert() + stock_entry.submit() + + wo_order.reload() + self.assertEqual(wo_order.material_transferred_for_manufacturing, 3) + frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 0) + + def test_req_qty_clamping_in_manufacture_entry(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + fg_item = "Test Unconsumed RM FG Item" + rm_item_1 = "Test Unconsumed RM Item 1" + rm_item_2 = "Test Unconsumed RM Item 2" + + source_warehouse = "_Test Warehouse - _TC" + wip_warehouse = "Stores - _TC" + fg_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company") + + make_item(fg_item, {"is_stock_item": 1}) + make_item(rm_item_1, {"is_stock_item": 1}) + make_item(rm_item_2, {"is_stock_item": 1}) + + # create a BOM: 1 FG = 1 RM1 + 1 RM2 + bom = make_bom( + item=fg_item, + source_warehouse=source_warehouse, + raw_materials=[rm_item_1, rm_item_2], + operating_cost_per_bom_quantity=1, + do_not_submit=True, + ) + + for row in bom.exploded_items: + make_stock_entry_test_record( + item_code=row.item_code, + target=source_warehouse, + qty=100, + basic_rate=100, + ) + + wo = make_wo_order_test_record( + item=fg_item, + qty=50, + source_warehouse=source_warehouse, + wip_warehouse=wip_warehouse, + ) + wo.submit() + + # first partial transfer & manufacture (6 units) + se_transfer_1 = frappe.get_doc( + make_stock_entry(wo.name, "Material Transfer for Manufacture", 6, wip_warehouse) + ) + se_transfer_1.insert() + se_transfer_1.submit() + + stock_entry_1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 6, fg_warehouse)) + + # remove rm_2 from the items to simulate unconsumed RM scenario + stock_entry_1.items = [row for row in stock_entry_1.items if row.item_code != rm_item_2] + stock_entry_1.save() + stock_entry_1.submit() + + wo.reload() + + se_transfer_2 = frappe.get_doc( + make_stock_entry(wo.name, "Material Transfer for Manufacture", 20, wip_warehouse) + ) + se_transfer_2.insert() + se_transfer_2.submit() + + stock_entry_2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 20, fg_warehouse)) + + # validate rm_item_2 quantity is clamped correctly (per-unit BOM = 1 → max 20) + for row in stock_entry_2.items: + if row.item_code == rm_item_2: + self.assertLessEqual(row.qty, 20) + self.assertGreaterEqual(row.qty, 0) + +>>>>>>> 34d2c8d9c2 (test: required_qty clamping in manufacture entry) def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( From 0fb5f75e9345b4117ddaad1aae817b4738540c8d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 30 Sep 2025 18:43:38 +0530 Subject: [PATCH 05/47] chore: fix conflicts Removed the test for additional material transfer in work orders. --- .../doctype/work_order/test_work_order.py | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 6716e0855e9..0b9dc3348fe 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2828,38 +2828,6 @@ class TestWorkOrder(FrappeTestCase): wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10) ) -<<<<<<< HEAD -======= - def test_allow_additional_material_transfer(self): - from erpnext.stock.doctype.stock_entry.test_stock_entry import ( - make_stock_entry as make_stock_entry_test_record, - ) - - frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 50) - wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2) - for row in wo_order.required_items: - make_stock_entry_test_record( - item_code=row.item_code, - target=row.source_warehouse, - qty=row.required_qty * 2, - basic_rate=100, - ) - - stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 2)) - stock_entry.insert() - stock_entry.submit() - - wo_order.reload() - self.assertEqual(wo_order.material_transferred_for_manufacturing, 2) - - stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1)) - stock_entry.insert() - stock_entry.submit() - - wo_order.reload() - self.assertEqual(wo_order.material_transferred_for_manufacturing, 3) - frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 0) - def test_req_qty_clamping_in_manufacture_entry(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( make_stock_entry as make_stock_entry_test_record, @@ -2932,7 +2900,6 @@ class TestWorkOrder(FrappeTestCase): self.assertLessEqual(row.qty, 20) self.assertGreaterEqual(row.qty, 0) ->>>>>>> 34d2c8d9c2 (test: required_qty clamping in manufacture entry) def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( From 0eb76f4d2ce9a89022af266177c78afaf9a0f1e0 Mon Sep 17 00:00:00 2001 From: rahulgupta8848 <147691594+rahulgupta8848@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:37:17 +0530 Subject: [PATCH 06/47] feat: validating asset scrap date (#43093) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: validating asset scrap date * refactor: refactorization of scrap asset function --------- Co-authored-by: “rahulgupta8848” <“rahul.gupta@8848digital.com”> (cherry picked from commit e07bc5af413ca951df78fa3583e0fbe3935b2e42) # Conflicts: # erpnext/assets/doctype/asset/asset.js --- erpnext/assets/doctype/asset/asset.js | 32 ++++++++++++++--- erpnext/assets/doctype/asset/depreciation.py | 38 ++++++++++++++++++-- erpnext/assets/doctype/asset/test_asset.py | 26 ++++++++++++++ 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 9fecd26a68a..455e0a0f146 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -790,17 +790,41 @@ frappe.ui.form.on("Asset Finance Book", { }); erpnext.asset.scrap_asset = function (frm) { - frappe.confirm(__("Do you really want to scrap this asset?"), function () { - frappe.call({ - args: { - asset_name: frm.doc.name, + var scrap_dialog = new frappe.ui.Dialog({ + title: __("Enter date to scrap asset"), + fields: [ + { + label: __("Select the date"), + fieldname: "scrap_date", + fieldtype: "Date", + reqd: 1, }, +<<<<<<< HEAD method: "erpnext.assets.doctype.asset.depreciation.scrap_asset", callback: function (r) { cur_frm.reload_doc(); }, }); +======= + ], + size: "medium", + primary_action_label: "Submit", + primary_action(values) { + frappe.call({ + args: { + asset_name: frm.doc.name, + scrap_date: values.scrap_date, + }, + method: "erpnext.assets.doctype.asset.depreciation.scrap_asset", + callback: function (r) { + frm.reload_doc(); + scrap_dialog.hide(); + }, + }); + }, +>>>>>>> e07bc5af41 (feat: validating asset scrap date (#43093)) }); + scrap_dialog.show(); }; erpnext.asset.restore_asset = function (frm) { diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index a0681cc2c56..b06244fd344 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -394,7 +394,7 @@ def get_comma_separated_links(names, doctype): @frappe.whitelist() -def scrap_asset(asset_name): +def scrap_asset(asset_name, scrap_date=None): asset = frappe.get_doc("Asset", asset_name) if asset.docstatus != 1: @@ -402,7 +402,11 @@ def scrap_asset(asset_name): elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized"): frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status)) - date = today() + today_date = getdate(today()) + date = getdate(scrap_date) or today_date + purchase_date = getdate(asset.purchase_date) + + validate_scrap_date(date, today_date, purchase_date, asset.calculate_depreciation, asset_name) notes = _("This schedule was created when Asset {0} was scrapped.").format( get_link_to_form(asset.doctype, asset.name) @@ -436,6 +440,36 @@ def scrap_asset(asset_name): frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name)) +def validate_scrap_date(scrap_date, today_date, purchase_date, calculate_depreciation, asset_name): + if scrap_date > today_date: + frappe.throw(_("Future date is not allowed")) + elif scrap_date < purchase_date: + frappe.throw(_("Scrap date cannot be before purchase date")) + + if calculate_depreciation: + asset_depreciation_schedules = frappe.db.get_all( + "Asset Depreciation Schedule", filters={"asset": asset_name, "docstatus": 1}, fields=["name"] + ) + + for depreciation_schedule in asset_depreciation_schedules: + last_booked_depreciation_date = frappe.db.get_value( + "Depreciation Schedule", + { + "parent": depreciation_schedule["name"], + "docstatus": 1, + "journal_entry": ["!=", ""], + }, + "schedule_date", + order_by="schedule_date desc", + ) + if ( + last_booked_depreciation_date + and scrap_date < last_booked_depreciation_date + and scrap_date > purchase_date + ): + frappe.throw(_("Asset cannot be scrapped before the last depreciation entry.")) + + @frappe.whitelist() def restore_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 78a7929a4fe..f4b02d3fea3 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -15,6 +15,7 @@ from frappe.utils import ( is_last_day_of_the_month, nowdate, ) +from frappe.utils.data import add_to_date from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice @@ -219,6 +220,31 @@ class TestAsset(AssetSetup): ) self.assertEqual(accumulated_depr_amount, 18000.0) + asset_depreciation = frappe.db.get_value( + "Asset Depreciation Schedule", {"asset": asset.name, "docstatus": 1}, "name" + ) + last_booked_depreciation_date = frappe.db.get_value( + "Depreciation Schedule", + { + "parent": asset_depreciation, + "docstatus": 1, + "journal_entry": ["!=", ""], + }, + "schedule_date", + order_by="schedule_date desc", + ) + + before_purchase_date = add_to_date(asset.purchase_date, days=-1) + future_date = add_to_date(nowdate(), days=1) + if last_booked_depreciation_date: + before_last_booked_depreciation_date = add_to_date(last_booked_depreciation_date, days=-1) + + self.assertRaises(frappe.ValidationError, scrap_asset, asset.name, scrap_date=before_purchase_date) + self.assertRaises(frappe.ValidationError, scrap_asset, asset.name, scrap_date=future_date) + self.assertRaises( + frappe.ValidationError, scrap_asset, asset.name, scrap_date=before_last_booked_depreciation_date + ) + scrap_asset(asset.name) asset.load_from_db() first_asset_depr_schedule.load_from_db() From e81b85b2413ec008487a20d8d2eed3143731e9eb Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:39:57 +0530 Subject: [PATCH 07/47] chore: resolve conflict --- erpnext/assets/doctype/asset/asset.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 455e0a0f146..f28860d325b 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -799,13 +799,6 @@ erpnext.asset.scrap_asset = function (frm) { fieldtype: "Date", reqd: 1, }, -<<<<<<< HEAD - method: "erpnext.assets.doctype.asset.depreciation.scrap_asset", - callback: function (r) { - cur_frm.reload_doc(); - }, - }); -======= ], size: "medium", primary_action_label: "Submit", @@ -822,7 +815,6 @@ erpnext.asset.scrap_asset = function (frm) { }, }); }, ->>>>>>> e07bc5af41 (feat: validating asset scrap date (#43093)) }); scrap_dialog.show(); }; From 825ccd34224d22487507f2d4f90797802887c15f Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Wed, 24 Sep 2025 11:37:16 +0530 Subject: [PATCH 08/47] fix(accounting): ensure proper removal of advance references during unreconcillation (cherry picked from commit a7ec01bf219dc1a682487a698cd6a1fa83ca8c53) --- erpnext/accounts/utils.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 18cbc65731c..85f2a1d5ee4 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -947,19 +947,28 @@ def update_accounting_ledgers_after_reference_removal( adv_ple.run() -def remove_ref_from_advance_section(ref_doc: object = None): +def remove_ref_from_advance_section(ref_doc: object = None, payment_name: str | None = None): # TODO: this might need some testing if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"): - ref_doc.set("advances", []) - adv_type = qb.DocType(f"{ref_doc.doctype} Advance") - qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run() + row_names = [] + for adv in ref_doc.get("advances") or []: + if adv.get("reference_name", None) == payment_name: + row_names.append(adv.name) + + if not row_names: + return + + child_table = ( + "Sales Invoice Advance" if ref_doc.doctype == "Sales Invoice" else "Purchase Invoice Advance" + ) + frappe.db.delete(child_table, {"name": ("in", row_names)}) def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str | None = None): remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name) remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name) update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name) - remove_ref_from_advance_section(ref_doc) + remove_ref_from_advance_section(ref_doc, payment_name) def remove_ref_doc_link_from_jv( @@ -1026,7 +1035,6 @@ def remove_ref_doc_link_from_pe( query = query.where(per.parent == payment_name) reference_rows = query.run(as_dict=True) - if not reference_rows: return From a2bf53ff0aea3debc69ef696351c4ce2a5dfdb17 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 1 Oct 2025 18:25:11 +0530 Subject: [PATCH 09/47] fix: too many writes on patch run (cherry picked from commit 44ff6ed6a14f6757f9e3e910173a72d152acdecf) --- .../patch_missing_buying_price_list_in_material_request.py | 3 +++ 1 file changed, 3 insertions(+) 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 index 48f85335dd2..369be8469d5 100644 --- 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 @@ -9,5 +9,8 @@ def execute(): docs = frappe.get_all( "Material Request", filters={"buying_price_list": ["is", "not set"], "docstatus": 1}, pluck="name" ) + old_limit = frappe.db.MAX_WRITES_PER_TRANSACTION + frappe.db.MAX_WRITES_PER_TRANSACTION *= 4 for doc in docs: frappe.db.set_value("Material Request", doc, "buying_price_list", default_buying_price_list) + frappe.db.MAX_WRITES_PER_TRANSACTION = old_limit From e21baec246177c677b05fbb56f98719e743d7151 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 1 Oct 2025 18:39:29 +0530 Subject: [PATCH 10/47] fix: Add try-finally for setting buying price list (cherry picked from commit 35a8d02866260961ab224936817de9f74bc2f138) --- ...patch_missing_buying_price_list_in_material_request.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 index 369be8469d5..379a1a50983 100644 --- 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 @@ -11,6 +11,8 @@ def execute(): ) old_limit = frappe.db.MAX_WRITES_PER_TRANSACTION frappe.db.MAX_WRITES_PER_TRANSACTION *= 4 - for doc in docs: - frappe.db.set_value("Material Request", doc, "buying_price_list", default_buying_price_list) - frappe.db.MAX_WRITES_PER_TRANSACTION = old_limit + try: + for doc in docs: + frappe.db.set_value("Material Request", doc, "buying_price_list", default_buying_price_list) + finally: + frappe.db.MAX_WRITES_PER_TRANSACTION = old_limit From 4e3697284eed86b5afd9cc5a02521a251fc3f0c1 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Wed, 1 Oct 2025 23:57:09 +0530 Subject: [PATCH 11/47] Merge pull request #49838 from diptanilsaha/backport_49820 fix: financial ratios translation and pdf export error (backport #49820) --- .../financial_ratios/financial_ratios.js | 4 +-- .../financial_ratios/financial_ratios.py | 26 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.js b/erpnext/accounts/report/financial_ratios/financial_ratios.js index c0ae31d9bee..e5bc59a3833 100644 --- a/erpnext/accounts/report/financial_ratios/financial_ratios.js +++ b/erpnext/accounts/report/financial_ratios/financial_ratios.js @@ -52,7 +52,7 @@ frappe.query_reports["Financial Ratios"] = { }, ], formatter: function (value, row, column, data, default_formatter) { - let heading_ratios = ["Liquidity Ratios", "Solvency Ratios", "Turnover Ratios"]; + let heading_ratios = [__("Liquidity Ratios"), __("Solvency Ratios"), __("Turnover Ratios")]; if (heading_ratios.includes(value)) { value = $(`${value}`); @@ -60,7 +60,7 @@ frappe.query_reports["Financial Ratios"] = { value = $value.wrap("

").parent().html(); } - if (heading_ratios.includes(row[1].content) && column.fieldtype == "Float") { + if (heading_ratios.includes(row[1]?.content) && column.fieldtype == "Float") { column.fieldtype = "Data"; } diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.py b/erpnext/accounts/report/financial_ratios/financial_ratios.py index c97cd898ca2..5084b6c1651 100644 --- a/erpnext/accounts/report/financial_ratios/financial_ratios.py +++ b/erpnext/accounts/report/financial_ratios/financial_ratios.py @@ -147,9 +147,9 @@ def get_gl_data(filters, period_list, years): def add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset): precision = frappe.db.get_single_value("System Settings", "float_precision") - data.append({"ratio": "Liquidity Ratios"}) + data.append({"ratio": _("Liquidity Ratios")}) - ratio_data = [["Current Ratio", current_asset], ["Quick Ratio", quick_asset]] + ratio_data = [[_("Current Ratio"), current_asset], [_("Quick Ratio"), quick_asset]] for d in ratio_data: row = { @@ -165,13 +165,13 @@ def add_solvency_ratios( data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense ): precision = frappe.db.get_single_value("System Settings", "float_precision") - data.append({"ratio": "Solvency Ratios"}) + data.append({"ratio": _("Solvency Ratios")}) - debt_equity_ratio = {"ratio": "Debt Equity Ratio"} - gross_profit_ratio = {"ratio": "Gross Profit Ratio"} - net_profit_ratio = {"ratio": "Net Profit Ratio"} - return_on_asset_ratio = {"ratio": "Return on Asset Ratio"} - return_on_equity_ratio = {"ratio": "Return on Equity Ratio"} + debt_equity_ratio = {"ratio": _("Debt Equity Ratio")} + gross_profit_ratio = {"ratio": _("Gross Profit Ratio")} + net_profit_ratio = {"ratio": _("Net Profit Ratio")} + return_on_asset_ratio = {"ratio": _("Return on Asset Ratio")} + return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")} for year in years: profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year)) @@ -195,7 +195,7 @@ def add_solvency_ratios( def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense): precision = frappe.db.get_single_value("System Settings", "float_precision") - data.append({"ratio": "Turnover Ratios"}) + data.append({"ratio": _("Turnover Ratios")}) avg_data = {} for d in ["Receivable", "Payable", "Stock"]: @@ -208,10 +208,10 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale ) ratio_data = [ - ["Fixed Asset Turnover Ratio", net_sales, total_asset], - ["Debtor Turnover Ratio", net_sales, avg_debtors], - ["Creditor Turnover Ratio", direct_expense, avg_creditors], - ["Inventory Turnover Ratio", cogs, avg_stock], + [_("Fixed Asset Turnover Ratio"), net_sales, total_asset], + [_("Debtor Turnover Ratio"), net_sales, avg_debtors], + [_("Creditor Turnover Ratio"), direct_expense, avg_creditors], + [_("Inventory Turnover Ratio"), cogs, avg_stock], ] for ratio in ratio_data: row = { From f321725b49feb00aea540d6d580a67bce1cf506b Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Wed, 1 Oct 2025 02:14:56 +0530 Subject: [PATCH 12/47] Merge pull request #49496 from elshafei-developer/Add-a-missing-translate-function fix(Accounts Payable Summary): add a missing translate function --- .../accounts/report/accounts_receivable/accounts_receivable.py | 2 +- .../accounts_receivable_summary/accounts_receivable_summary.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 10a14ca4714..729155cec8d 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1270,7 +1270,7 @@ class ReceivablePayableReport: def setup_ageing_columns(self): # for charts self.ageing_column_labels = [] - ranges = [*self.ranges, "Above"] + ranges = [*self.ranges, _("Above")] prev_range_value = 0 for idx, curr_range_value in enumerate(ranges): diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 87fc7ea68be..19fd7dc96ef 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -171,7 +171,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.add_column(_("Difference"), fieldname="diff") self.setup_ageing_columns() - self.add_column(label="Total Amount Due", fieldname="total_due") + self.add_column(label=_("Total Amount Due"), fieldname="total_due") if self.filters.show_future_payments: self.add_column(label=_("Future Payment Amount"), fieldname="future_amount") From 5e5850d89a9c9156a71f08b9294f6ac88e7a559d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 00:16:34 +0200 Subject: [PATCH 13/47] refactor(Supplier): custom buttons call make methods (backport #49840) (#49842) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/buying/doctype/supplier/supplier.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index cf3506a67d3..93fa566dc74 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -109,21 +109,9 @@ frappe.ui.form.on("Supplier", { __("View") ); - frm.add_custom_button( - __("Bank Account"), - function () { - erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name); - }, - __("Create") - ); + frm.add_custom_button(__("Bank Account"), () => frm.make_methods["Bank Account"](), __("Create")); - frm.add_custom_button( - __("Pricing Rule"), - function () { - erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); - }, - __("Create") - ); + frm.add_custom_button(__("Pricing Rule"), () => frm.make_methods["Pricing Rule"](), __("Create")); frm.add_custom_button( __("Get Supplier Group Details"), From a5ed9fdc67986988388346dc97e832ecf3739ae6 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:18:09 +0530 Subject: [PATCH 14/47] fix: add default scrap warehouse in wo (cherry picked from commit 7e51346946fe02cd3a1e4cb530859b353ce357bc) --- .../manufacturing/doctype/production_plan/production_plan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index fb8c8039d98..d44f48a08ed 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -748,6 +748,7 @@ class ProductionPlan(Document): work_order_data = { "wip_warehouse": default_warehouses.get("wip_warehouse"), "fg_warehouse": default_warehouses.get("fg_warehouse"), + "scrap_warehouse": default_warehouses.get("scrap_warehouse"), "company": self.get("company"), } @@ -1821,7 +1822,7 @@ def get_sub_assembly_items( def set_default_warehouses(row, default_warehouses): - for field in ["wip_warehouse", "fg_warehouse"]: + for field in ["wip_warehouse", "fg_warehouse", "scrap_warehouse"]: if not row.get(field): row[field] = default_warehouses.get(field) From 7ce97ce0c2fdc2f6308e5b9266b1db198984061b Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:10:38 +0530 Subject: [PATCH 15/47] fix: validate transfer_qty based on overproduction wo percentage (cherry picked from commit 4024d8846b59387d69ead723c93d29da72c4dc30) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 7ad2b9f3cd1..f8a9160aca8 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1356,12 +1356,6 @@ class StockEntry(StockController): d.item_code, self.work_order ) ) - elif flt(d.transfer_qty) > flt(self.fg_completed_qty): - frappe.throw( - _("Quantity in row {0} ({1}) must be same as manufactured quantity {2}").format( - d.idx, d.transfer_qty, self.fg_completed_qty - ) - ) finished_items.append(d.item_code) From b6d57ff8a5d66c09dbd4098f2747c9f8c692b3ad Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:38:45 +0530 Subject: [PATCH 16/47] fix: set fg_completed_qty based upon fg item qty (cherry picked from commit 526b850e61f507f0fd571d09dbf2b048ce8dce9c) --- .../stock/doctype/stock_entry/stock_entry.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 0989d610b4a..4b28f6094c1 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -823,9 +823,28 @@ frappe.ui.form.on("Stock Entry", { refresh_field("process_loss_qty"); } }, + + set_fg_completed_qty(frm) { + let fg_completed_qty = 0; + + frm.doc.items.forEach((item) => { + if (item.is_finished_item) { + fg_completed_qty += flt(item.qty); + } + }); + + frm.doc.fg_completed_qty = fg_completed_qty; + frm.refresh_field("fg_completed_qty"); + }, }); frappe.ui.form.on("Stock Entry Detail", { + items_add(frm, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + if (item.is_finished_item) { + frm.events.set_fg_completed_qty(frm); + } + }, set_basic_rate_manually(frm, cdt, cdn) { let row = locals[cdt][cdn]; frm.fields_dict.items.grid.update_docfield_property( @@ -837,6 +856,10 @@ frappe.ui.form.on("Stock Entry Detail", { qty(frm, cdt, cdn) { frm.events.set_basic_rate(frm, cdt, cdn); + let item = frappe.get_doc(cdt, cdn); + if (item.is_finished_item) { + frm.events.set_fg_completed_qty(frm); + } }, conversion_factor(frm, cdt, cdn) { From 0fec34e886179838a2bf89d26bca332385ae0d2d Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:27:45 +0530 Subject: [PATCH 17/47] test: test overproduction allowed qty in wo (cherry picked from commit b527d38bfafd5843deaa4732e8df2e1ed244cddc) --- .../doctype/work_order/test_work_order.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0b9dc3348fe..0ece122fe0e 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2900,6 +2900,39 @@ class TestWorkOrder(FrappeTestCase): self.assertLessEqual(row.qty, 20) self.assertGreaterEqual(row.qty, 0) + def test_overproduction_allowed_qty(self): + """Test overproduction allowed qty in work order""" + allow_overproduction("overproduction_percentage_for_work_order", 50) + + wo_order = make_wo_order_test_record(planned_start_date=now(), qty=10) + + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="Stores - _TC", qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=100, + basic_rate=1000.0, + ) + + mt_stock_entry = frappe.get_doc( + make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 10) + ) + mt_stock_entry.submit() + + fg_stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) + fg_stock_entry.items[2].qty = 15 + fg_stock_entry.fg_completed_qty = 15 + fg_stock_entry.submit() + + wo_order.reload() + + self.assertEqual(wo_order.produced_qty, 15) + self.assertEqual(wo_order.status, "Completed") + + allow_overproduction("overproduction_percentage_for_work_order", 0) + def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( From 0dad1957c8cdf673eb96ea86c943874a8fe25dff Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 3 Oct 2025 15:11:06 +0530 Subject: [PATCH 18/47] fix: failing patch (cherry picked from commit 41d1703e7c374694897e2470ef3fec9d5de45010) --- .../patch_missing_buying_price_list_in_material_request.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 379a1a50983..473c0d5fbc3 100644 --- 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 @@ -9,10 +9,9 @@ def execute(): docs = frappe.get_all( "Material Request", filters={"buying_price_list": ["is", "not set"], "docstatus": 1}, pluck="name" ) - old_limit = frappe.db.MAX_WRITES_PER_TRANSACTION - frappe.db.MAX_WRITES_PER_TRANSACTION *= 4 + frappe.db.auto_commit_on_many_writes = 1 try: for doc in docs: frappe.db.set_value("Material Request", doc, "buying_price_list", default_buying_price_list) finally: - frappe.db.MAX_WRITES_PER_TRANSACTION = old_limit + frappe.db.auto_commit_on_many_writes = 0 From 4ccdedeb12c2c39868a0644c60b642d396fb9e45 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Fri, 3 Oct 2025 18:43:10 +0530 Subject: [PATCH 19/47] fix: remove allow_on_submit for pick list items (cherry picked from commit da716b824f2a9039fe9218742f9fe7e0f92c29a1) --- erpnext/stock/doctype/pick_list/pick_list.js | 1 - erpnext/stock/doctype/pick_list/pick_list.json | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 9ea7f006df8..f22965cdb0e 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -140,7 +140,6 @@ frappe.ui.form.on("Pick List", { frm.trigger("add_get_items_button"); if (frm.doc.docstatus === 1) { const status_completed = frm.doc.status === "Completed"; - frm.set_df_property("locations", "allow_on_submit", status_completed ? 0 : 1); if (!status_completed) { frm.add_custom_button(__("Update Current Stock"), () => diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index 69a1482d6d9..4b46f4ecd82 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -77,7 +77,6 @@ "options": "Work Order" }, { - "allow_on_submit": 1, "fieldname": "locations", "fieldtype": "Table", "label": "Item Locations", @@ -247,7 +246,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2025-07-23 08:34:32.099673", + "modified": "2025-10-03 18:36:52.282355", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", From a83331bd2f8b4e6d6c32e0959e582ab22f952e13 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 4 Oct 2025 10:01:32 +0530 Subject: [PATCH 20/47] fix: optimize SQL query by adding index on batch (cherry picked from commit 8756f91857a8252ed5b398c433c3df1c33744058) --- .../stock/doctype/stock_ledger_entry/stock_ledger_entry.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index ce3e3435130..3af946aa8f3 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -73,7 +73,8 @@ "label": "Batch No", "oldfieldname": "batch_no", "oldfieldtype": "Data", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "warehouse", @@ -361,7 +362,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-04-22 12:37:41.304109", + "modified": "2025-10-04 09:59:15.546556", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", From 2c0501b05f3a10316284879cc14e47a7327e000f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 19:22:28 +0200 Subject: [PATCH 21/47] fix(Common Code): fetch canonical URI from Code List (backport #49882) (#49884) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> fix(Common Code): fetch canonical URI from Code List (#49882) --- erpnext/edi/doctype/common_code/common_code.json | 13 +++++++++++-- erpnext/edi/doctype/common_code/common_code.py | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/edi/doctype/common_code/common_code.json b/erpnext/edi/doctype/common_code/common_code.json index b2cb43fa575..f753bc94b6b 100644 --- a/erpnext/edi/doctype/common_code/common_code.json +++ b/erpnext/edi/doctype/common_code/common_code.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "code_list", + "canonical_uri", "title", "common_code", "description", @@ -71,10 +72,17 @@ "in_list_view": 1, "label": "Description", "max_height": "60px" + }, + { + "fetch_from": "code_list.canonical_uri", + "fieldname": "canonical_uri", + "fieldtype": "Data", + "label": "Canonical URI" } ], + "grid_page_length": 50, "links": [], - "modified": "2024-11-06 07:46:17.175687", + "modified": "2025-10-04 17:22:28.176155", "modified_by": "Administrator", "module": "EDI", "name": "Common Code", @@ -94,10 +102,11 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "common_code,description", "show_title_field_in_link": 1, "sort_field": "creation", "sort_order": "DESC", "states": [], "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/edi/doctype/common_code/common_code.py b/erpnext/edi/doctype/common_code/common_code.py index d558b2d282f..d1fd88350be 100644 --- a/erpnext/edi/doctype/common_code/common_code.py +++ b/erpnext/edi/doctype/common_code/common_code.py @@ -22,6 +22,7 @@ class CommonCode(Document): additional_data: DF.Code | None applies_to: DF.Table[DynamicLink] + canonical_uri: DF.Data | None code_list: DF.Link common_code: DF.Data description: DF.SmallText | None From bd3503a3d8ec2b1fa28a02ade52a8c438c935ab5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 6 Oct 2025 11:27:55 +0530 Subject: [PATCH 22/47] fix: Set paid amount automatically only if return entry validated and has negative grand total (#49829) (cherry picked from commit dcbcc596f2004643acde442128137e0ac778a4fe) # Conflicts: # erpnext/public/js/controllers/taxes_and_totals.js --- erpnext/controllers/accounts_controller.py | 10 +++++----- erpnext/public/js/controllers/taxes_and_totals.js | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c2fd009fc4a..fb8b26085eb 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -231,6 +231,11 @@ class AccountsController(TransactionBase): self.validate_date_with_fiscal_year() self.validate_party_accounts() + if self.doctype in ["Sales Invoice", "Purchase Invoice"]: + if self.is_return: + self.validate_qty() + else: + self.validate_deferred_start_and_end_date() self.validate_inter_company_reference() # validate inter company transaction rate @@ -282,11 +287,6 @@ class AccountsController(TransactionBase): self.set_advance_gain_or_loss() - if self.is_return: - self.validate_qty() - else: - self.validate_deferred_start_and_end_date() - self.validate_deferred_income_expense_account() self.set_inter_company_account() diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index fe0110ce2cb..2b0427ee345 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -76,9 +76,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // Update paid amount on return/debit note creation if ( +<<<<<<< HEAD this.frm.doc.doctype === "Purchase Invoice" && this.frm.doc.is_return && (this.frm.doc.grand_total > this.frm.doc.paid_amount) +======= + this.frm.doc.doctype === "Purchase Invoice" && + this.frm.doc.is_return && + this.frm.doc.grand_total < 0 && + this.frm.doc.grand_total > this.frm.doc.paid_amount +>>>>>>> dcbcc596f2 (fix: Set paid amount automatically only if return entry validated and has negative grand total (#49829)) ) { this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total")); } From a39bc626c705a7f3be88dea2a67801b0891b57d6 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 6 Oct 2025 12:17:57 +0530 Subject: [PATCH 23/47] fix: resolved conflict --- erpnext/public/js/controllers/taxes_and_totals.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 2b0427ee345..d8c81b726ee 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -76,16 +76,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // Update paid amount on return/debit note creation if ( -<<<<<<< HEAD - this.frm.doc.doctype === "Purchase Invoice" - && this.frm.doc.is_return - && (this.frm.doc.grand_total > this.frm.doc.paid_amount) -======= this.frm.doc.doctype === "Purchase Invoice" && this.frm.doc.is_return && this.frm.doc.grand_total < 0 && this.frm.doc.grand_total > this.frm.doc.paid_amount ->>>>>>> dcbcc596f2 (fix: Set paid amount automatically only if return entry validated and has negative grand total (#49829)) ) { this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total")); } From 040873a442c971a977412272b099b54c28a782ce Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Fri, 3 Oct 2025 14:03:28 +0530 Subject: [PATCH 24/47] fix: delete column dynamically based on the naming by (cherry picked from commit 4f503ac7f677696dee18db600b5b05893c214e72) --- erpnext/accounts/report/gross_profit/gross_profit.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 5081c450151..367fe4f4fd2 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -178,7 +178,12 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_ # to display item as Item Code: Item Name columns[0] = "Sales Invoice:Link/Item:300" # removing Item Code and Item Name columns - del columns[4:6] + supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name") + customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name") + if supplier_master_name == "Supplier Name" and customer_master_name == "Customer Name": + del columns[4:6] + else: + del columns[5:7] total_base_amount = 0 total_buying_amount = 0 @@ -275,7 +280,7 @@ def get_columns(group_wise_columns, filters): "label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", - "width": 100, + "width": 120, }, "posting_time": { "label": _("Posting Time"), From 20c21a4dc089dd81acc340e1cfee6e1bf7b6bac8 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Fri, 3 Oct 2025 17:04:49 +0530 Subject: [PATCH 25/47] fix: retain shipping address in doc (cherry picked from commit 039f5e614319c3feadb4c7b2078bc653ee2a8e89) # Conflicts: # erpnext/public/js/controllers/buying.js --- erpnext/public/js/controllers/buying.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index e7ee7d505a1..1afca307d94 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -173,8 +173,11 @@ erpnext.buying = { callback: (r) => { this.frm.set_value("billing_address", r.message.primary_address || ""); - if(!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return; - this.frm.set_value("shipping_address", r.message.shipping_address || ""); + if (!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return; + this.frm.set_value( + "shipping_address", + r.message.shipping_address || this.frm.doc.shipping_address || "" + ); }, }); erpnext.utils.set_letter_head(this.frm) From d950de2d099509093cf21093290c086e0db03980 Mon Sep 17 00:00:00 2001 From: Fawaz Alhafiz <48890670+fawaaaz111@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:24:51 +0300 Subject: [PATCH 26/47] fix: SQL operator precedence in Project query customer filter Added explicit parentheses around customer OR conditions in get_project_name() to ensure proper grouping with AND filters. Without these parentheses, SQL operator precedence caused the status filter to be bypassed when a customer filter was applied, resulting in completed and cancelled projects appearing in link field dropdowns. Before: WHERE customer='X' OR customer IS NULL OR customer='' AND status NOT IN (...) was interpreted as: WHERE customer='X' OR customer IS NULL OR (customer='' AND status NOT IN (...)) After: WHERE (customer='X' OR customer IS NULL OR customer='') AND status NOT IN (...) Fixes: Completed/cancelled projects showing in Project link fields Affected: Any doctype using Project link fields with customer filters (cherry picked from commit 0ec30a1ceaac420146e23c72788d05f44778f141) --- erpnext/controllers/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 55e04a2e262..12d5229c9f3 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -313,7 +313,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): if filters: if filters.get("customer"): qb_filter_and_conditions.append( - (proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == "" + (proj.customer == filters.get("customer")) | (proj.customer.isnull()) | (proj.customer == "") ) if filters.get("company"): From 95387b4bf0723138abf7b7572b384849f6d11fe8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 11:49:40 +0200 Subject: [PATCH 27/47] feat: allow fallback to default selling price list (backport #49634) (#49704) Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> Co-authored-by: Henning Wendtland <156231187+HenningWendtland@users.noreply.github.com> --- .../selling_settings/selling_settings.json | 9 +++++++- .../selling_settings/selling_settings.py | 21 +++++++++++++++++++ .../doctype/stock_settings/stock_settings.py | 18 ++++++++++++++++ erpnext/stock/get_item_details.py | 9 ++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 0553ef44980..83ba1907671 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -17,6 +17,7 @@ "role_to_override_stop_action", "column_break_15", "maintain_same_sales_rate", + "fallback_to_default_price_list", "editable_price_list_rate", "validate_selling_price", "editable_bundle_item_rates", @@ -216,6 +217,12 @@ "fieldname": "allow_zero_qty_in_quotation", "fieldtype": "Check", "label": "Allow Quotation with Zero Quantity" + }, + { + "default": "0", + "fieldname": "fallback_to_default_price_list", + "fieldtype": "Check", + "label": "Use Prices from Default Price List as Fallback" } ], "grid_page_length": 50, @@ -224,7 +231,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-05-06 15:23:14.332971", + "modified": "2025-09-23 21:10:14.826653", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index 2954c353a77..06047830724 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -5,6 +5,7 @@ import frappe +from frappe import _ from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.model.document import Document from frappe.utils import cint @@ -33,6 +34,7 @@ class SellingSettings(Document): editable_bundle_item_rates: DF.Check editable_price_list_rate: DF.Check enable_discount_accounting: DF.Check + fallback_to_default_price_list: DF.Check hide_tax_id: DF.Check maintain_same_rate_action: DF.Literal["Stop", "Warn"] maintain_same_sales_rate: DF.Check @@ -69,6 +71,25 @@ class SellingSettings(Document): hide_name_field=False, ) + self.validate_fallback_to_default_price_list() + + def validate_fallback_to_default_price_list(self): + if ( + self.fallback_to_default_price_list + and self.has_value_changed("fallback_to_default_price_list") + and frappe.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing") + ): + stock_meta = frappe.get_meta("Stock Settings") + frappe.msgprint( + _( + "You have enabled {0} and {1} in {2}. This can lead to prices from the default price list being inserted into the transaction price list." + ).format( + "{}".format(_(self.meta.get_label("fallback_to_default_price_list"))), + "{}".format(_(stock_meta.get_label("auto_insert_price_list_rate_if_missing"))), + frappe.bold(_("Stock Settings")), + ) + ) + def toggle_hide_tax_id(self): self.hide_tax_id = cint(self.hide_tax_id) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index ed76d96fd2e..922bdee6e47 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -105,6 +105,7 @@ class StockSettings(Document): self.validate_clean_description_html() self.validate_pending_reposts() self.validate_stock_reservation() + self.validate_auto_insert_price_list_rate_if_missing() self.change_precision_for_for_sales() self.change_precision_for_purchase() @@ -219,6 +220,23 @@ class StockSettings(Document): ) ) + def validate_auto_insert_price_list_rate_if_missing(self): + if ( + self.auto_insert_price_list_rate_if_missing + and self.has_value_changed("auto_insert_price_list_rate_if_missing") + and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list") + ): + selling_meta = frappe.get_meta("Selling Settings") + frappe.msgprint( + _( + "You have enabled {0} and {1} in {2}. This can lead to prices from the default price list being inserted in the transaction price list." + ).format( + "{}".format(_(self.meta.get_label("auto_insert_price_list_rate_if_missing"))), + "{}".format(_(selling_meta.get_label("fallback_to_default_price_list"))), + frappe.bold(_("Selling Settings")), + ) + ) + def on_update(self): self.toggle_warehouse_field_for_inter_warehouse_transfer() diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 81ecbf363fa..b7204459e27 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -98,6 +98,15 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru out.update(get_price_list_rate(args, item)) + if ( + not out.price_list_rate + and args.transaction_type == "selling" + and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list") + ): + fallback_args = args.copy() + fallback_args.price_list = frappe.get_single_value("Selling Settings", "selling_price_list") + out.update(get_price_list_rate(fallback_args, item)) + args.customer = current_customer if args.customer and cint(args.is_pos): From f27b754570b006c415bc3675ad64772ee112426d Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Fri, 19 Sep 2025 16:23:26 +0530 Subject: [PATCH 28/47] fix(subscription): include days before (cherry picked from commit 9164162a9e80e685bb596d4f9eb87573125d8ede) --- .../doctype/subscription/subscription.py | 49 ++++++++----------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 8c4d21ac3fb..77aacb4ac60 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -490,11 +490,18 @@ class Subscription(Document): if prorate is None: prorate = False + prorate_factor = 1 if prorate: prorate_factor = get_prorata_factor( self.current_invoice_end, self.current_invoice_start, - cint(self.generate_invoice_at == "Beginning of the current subscription period"), + cint( + self.generate_invoice_at + in [ + "Beginning of the current subscription period", + "Days before the current subscription period", + ] + ), ) items = [] @@ -511,33 +518,19 @@ class Subscription(Document): deferred = frappe.db.get_value("Item", item_code, deferred_field) - if not prorate: - item = { - "item_code": item_code, - "qty": plan.qty, - "rate": get_plan_rate( - plan.plan, - plan.qty, - party, - self.current_invoice_start, - self.current_invoice_end, - ), - "cost_center": plan_doc.cost_center, - } - else: - item = { - "item_code": item_code, - "qty": plan.qty, - "rate": get_plan_rate( - plan.plan, - plan.qty, - party, - self.current_invoice_start, - self.current_invoice_end, - prorate_factor, - ), - "cost_center": plan_doc.cost_center, - } + item = { + "item_code": item_code, + "qty": plan.qty, + "rate": get_plan_rate( + plan.plan, + plan.qty, + party, + self.current_invoice_start, + self.current_invoice_end, + prorate_factor, + ), + "cost_center": plan_doc.cost_center, + } if deferred: item.update( From 3fcbb10155eb7c7ebfd766389d67abc74ff3b4f8 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Fri, 19 Sep 2025 16:48:35 +0530 Subject: [PATCH 29/47] refactor(subscription): default prorate 0 (cherry picked from commit eda1dae882f0bc39790a84850175f90e83439916) --- erpnext/accounts/doctype/subscription/subscription.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 77aacb4ac60..623073c7e45 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -483,12 +483,10 @@ class Subscription(Document): return invoice - def get_items_from_plans(self, plans: list[dict[str, str]], prorate: bool | None = None) -> list[dict]: + def get_items_from_plans(self, plans: list[dict[str, str]], prorate: int = 0) -> list[dict]: """ Returns the `Item`s linked to `Subscription Plan` """ - if prorate is None: - prorate = False prorate_factor = 1 if prorate: From 4f067085e73caca988d6625e7f74b245aff7cfac Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Mon, 6 Oct 2025 12:12:27 +0530 Subject: [PATCH 30/47] test: add invoice generation before period with prorate (cherry picked from commit b452e06b82756d91e062f1906e0eb8408a4abd03) --- .../doctype/subscription/test_subscription.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index b1e5653e8da..1d906fb9276 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -8,6 +8,7 @@ from frappe.utils.data import ( add_days, add_months, add_to_date, + add_years, cint, date_diff, flt, @@ -555,6 +556,33 @@ class TestSubscription(FrappeTestCase): subscription.reload() self.assertEqual(len(subscription.invoices), 0) + def test_invoice_generation_days_before_subscription_period_with_prorate(self): + settings = frappe.get_single("Subscription Settings") + settings.prorate = 1 + settings.save() + + create_plan( + plan_name="_Test Plan Name 5", + cost=1000, + billing_interval="Year", + billing_interval_count=1, + currency="INR", + ) + + start_date = add_days(nowdate(), 2) + + subscription = create_subscription( + start_date=start_date, + party_type="Supplier", + party="_Test Supplier", + generate_invoice_at="Days before the current subscription period", + generate_new_invoices_past_due_date=1, + number_of_days=2, + plans=[{"plan": "_Test Plan Name 5", "qty": 1}], + ) + subscription.process(nowdate()) + self.assertEqual(len(subscription.invoices), 1) + def make_plans(): create_plan(plan_name="_Test Plan Name", cost=900, currency="INR") From 6c47353205230f1057761ed96af6f8a4a5fabe19 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:02:05 +0200 Subject: [PATCH 31/47] fix: linter; dont change doc after DB update (#49907) Co-authored-by: ruthra kumar --- .../selling/doctype/selling_settings/selling_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index 06047830724..f15fdc7041d 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -91,15 +91,15 @@ class SellingSettings(Document): ) def toggle_hide_tax_id(self): - self.hide_tax_id = cint(self.hide_tax_id) + _hide_tax_id = cint(self.hide_tax_id) # Make property setters to hide tax_id fields for doctype in ("Sales Order", "Sales Invoice", "Delivery Note"): make_property_setter( - doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False + doctype, "tax_id", "hidden", _hide_tax_id, "Check", validate_fields_for_doctype=False ) make_property_setter( - doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False + doctype, "tax_id", "print_hide", _hide_tax_id, "Check", validate_fields_for_doctype=False ) def toggle_editable_rate_for_bundle_items(self): From e132c457f25baa4dc8d738be856a0f5db1db4e4a Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Fri, 19 Sep 2025 00:05:00 +0530 Subject: [PATCH 32/47] fix(profit and loss statement): incorrect total calculation (cherry picked from commit b7c6d8e2a6913ac9c6b38da731c01039eb3f2c85) --- .../accounts/report/financial_statements.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 89f36364826..f48adac6c51 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -323,18 +323,24 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False): + def get_all_parents(account, parent_children_map): + for parent, children in parent_children_map.items(): + for child in children: + if child["name"] == account and parent: + accounts_to_show.add(parent) + get_all_parents(parent, parent_children_map) + data_with_value = [] + accounts_to_show = set() + for d in data: if show_zero_values or d.get("has_value"): + accounts_to_show.add(d.get("account")) + get_all_parents(d.get("account"), parent_children_map) + + for d in data: + if d.get("account") in accounts_to_show: data_with_value.append(d) - else: - # show group with zero balance, if there are balances against child - children = [child.name for child in parent_children_map.get(d.get("account")) or []] - if children: - for row in data: - if row.get("account") in children and row.get("has_value"): - data_with_value.append(d) - break return data_with_value From 75323fda0119e568da4b16c91b312b084a0d9ef5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 6 Oct 2025 18:53:33 +0530 Subject: [PATCH 33/47] fix: do not consider draft bundles (cherry picked from commit a60f7eaf3a33ce988f1d7967d874ac98213ed7b7) --- erpnext/stock/serial_batch_bundle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index d4defccc452..08cacfe3bb2 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1374,7 +1374,7 @@ def throw_negative_batch_validation(batch_no, qty): def get_batchwise_qty(voucher_type, voucher_no): bundles = frappe.get_all( "Serial and Batch Bundle", - filters={"voucher_no": voucher_no, "voucher_type": voucher_type}, + filters={"voucher_no": voucher_no, "voucher_type": voucher_type, "docstatus": (">", 0)}, pluck="name", ) if not bundles: From bdf150bdf8bd2df036b23b80190e1312b9bd0add Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:49:28 +0530 Subject: [PATCH 34/47] fix: check is_rejected attribute (cherry picked from commit 2ac2e02b2ff59a345c011dd0c654841274c250aa) --- erpnext/stock/serial_batch_bundle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index d4defccc452..8c9f47e0806 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1227,7 +1227,7 @@ class SerialBatchCreation: def create_batch(self): from erpnext.stock.doctype.batch.batch import make_batch - if self.is_rejected: + if hasattr(self, "is_rejected") and self.is_rejected: bundle = frappe.db.get_value( "Serial and Batch Bundle", { From 8a310efc974179e4f89fa20f32257e66b6e3fe85 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 6 Oct 2025 00:52:56 +0530 Subject: [PATCH 35/47] perf: serial nos / batches reposting (cherry picked from commit acb3ef78a739f3c1c108c400fd84ec185807f9b2) --- erpnext/stock/deprecated_serial_batch.py | 16 +- .../doctype/stock_entry/test_stock_entry.py | 11 +- erpnext/stock/stock_ledger.py | 137 +++++++++++++++++- 3 files changed, 150 insertions(+), 14 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index d3eb178d778..8d1b76148d6 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -7,6 +7,7 @@ from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import flt, nowtime from frappe.utils.deprecations import deprecated from pypika import Order +from pypika.functions import Coalesce class DeprecatedSerialNoValuation: @@ -197,9 +198,15 @@ class DeprecatedBatchNoValuation: @deprecated def set_balance_value_for_non_batchwise_valuation_batches(self): - self.last_sle = self.get_last_sle_for_non_batch() + if hasattr(self, "prev_sle"): + self.last_sle = self.prev_sle + else: + self.last_sle = self.get_last_sle_for_non_batch() + if self.last_sle and self.last_sle.stock_queue: - self.stock_queue = json.loads(self.last_sle.stock_queue or "[]") or [] + self.stock_queue = self.last_sle.stock_queue + if isinstance(self.stock_queue, str): + self.stock_queue = json.loads(self.stock_queue) or [] self.set_balance_value_from_sl_entries() self.set_balance_value_from_bundle() @@ -293,10 +300,7 @@ class DeprecatedBatchNoValuation: query = query.where(sle.name != self.sle.name) if self.sle.serial_and_batch_bundle: - query = query.where( - (sle.serial_and_batch_bundle != self.sle.serial_and_batch_bundle) - | (sle.serial_and_batch_bundle.isnull()) - ) + query = query.where(Coalesce(sle.serial_and_batch_bundle, "") != self.sle.serial_and_batch_bundle) data = query.run(as_dict=True) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index cde5be4d6ee..08ce705f26f 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1323,9 +1323,18 @@ class TestStockEntry(FrappeTestCase): posting_date="2021-07-02", # Illegal SE purpose="Material Transfer", ), + dict( + item_code=item_code, + qty=2, + from_warehouse=warehouse_names[0], + to_warehouse=warehouse_names[1], + batch_no=batch_no, + posting_date="2021-07-02", # Illegal SE + purpose="Material Transfer", + ), ] - self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) + self.assertRaises(frappe.ValidationError, create_stock_entries, sequence_of_entries) @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_future_negative_sle_batch(self): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 40e576987f8..e92d45ec5dd 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1010,13 +1010,12 @@ class update_entries_after: if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle): return - if self.args.get("sle_id") and sle.actual_qty < 0: - doc = frappe.db.get_value( - "Serial and Batch Bundle", - sle.serial_and_batch_bundle, - ["total_amount", "total_qty"], - as_dict=1, - ) + if sle.actual_qty < 0 and ( + sle.voucher_type in ["Stock Reconciliation", "Asset Capitalization"] + or not frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_return") + ): + doc = frappe._dict({}) + self.update_serial_batch_no_valuation(sle, doc, prev_sle=self.wh_data) else: doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle) doc.set_incoming_rate( @@ -1040,6 +1039,88 @@ class update_entries_after: self.wh_data.qty_after_transaction, self.flt_precision ) + def update_serial_batch_no_valuation(self, sle, doc, prev_sle=None): + from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation + + sabb_data = get_serial_from_sabb(sle.serial_and_batch_bundle) + if not sabb_data: + doc.update({"total_amount": 0.0, "total_qty": 0.0, "avg_rate": 0.0}) + return + + serial_nos = [d.serial_no for d in sabb_data if d.serial_no] + if serial_nos: + sle["serial_nos"] = get_serial_nos_data(",".join(serial_nos)) + sn_obj = SerialNoValuation( + sle=sle, + item_code=self.item_code, + warehouse=sle.warehouse, + ) + else: + sle["batch_nos"] = {row.batch_no: row for row in sabb_data if row.batch_no} + sn_obj = BatchNoValuation( + sle=sle, + item_code=self.item_code, + warehouse=sle.warehouse, + prev_sle=prev_sle, + ) + + tot_amt = 0.0 + total_qty = 0.0 + avg_rate = 0.0 + + for d in sabb_data: + incoming_rate = get_incoming_rate_for_serial_and_batch(self.item_code, d, sn_obj) + + if flt(incoming_rate, self.currency_precision) == flt( + d.valuation_rate, self.currency_precision + ) and not getattr(d, "stock_queue", None): + continue + + amount = incoming_rate * flt(d.qty) + tot_amt += flt(amount) + total_qty += flt(d.qty) + + values_to_update = { + "incoming_rate": incoming_rate, + "stock_value_difference": amount, + } + + if d.stock_queue: + values_to_update["stock_queue"] = d.stock_queue + + frappe.db.set_value( + "Serial and Batch Entry", + d.name, + values_to_update, + update_modified=False, + ) + + if total_qty: + avg_rate = tot_amt / total_qty + + doc.update( + { + "total_amount": tot_amt, + "total_qty": total_qty, + "avg_rate": avg_rate, + } + ) + + frappe.db.set_value( + "Serial and Batch Bundle", + sle.serial_and_batch_bundle, + { + "total_qty": total_qty, + "avg_rate": avg_rate, + "total_amount": tot_amt, + }, + update_modified=False, + ) + + for key in ("serial_nos", "batch_nos"): + if key in sle: + del sle[key] + def get_outgoing_rate_for_batched_item(self, sle): if self.wh_data.qty_after_transaction == 0: return 0 @@ -2297,3 +2378,45 @@ def is_transfer_stock_entry(voucher_no): purpose = frappe.get_cached_value("Stock Entry", voucher_no, "purpose") return purpose in ["Material Transfer", "Material Transfer for Manufacture", "Send to Subcontractor"] + + +@frappe.request_cache +def get_serial_from_sabb(serial_and_batch_bundle): + return frappe.get_all( + "Serial and Batch Entry", + filters={"parent": serial_and_batch_bundle}, + fields=["serial_no", "batch_no", "name", "qty", "incoming_rate"], + order_by="idx", + ) + + +def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj): + if row.serial_no: + return abs(sn_obj.serial_no_incoming_rate.get(row.serial_no, 0.0)) + else: + stock_queue = [] + if hasattr(sn_obj, "stock_queue") and sn_obj.stock_queue: + stock_queue = parse_json(sn_obj.stock_queue) + + val_method = get_valuation_method(item_code) + + actual_qty = row.qty + if stock_queue and val_method == "FIFO" and row.batch_no in sn_obj.non_batchwise_valuation_batches: + if actual_qty < 0: + stock_queue = FIFOValuation(stock_queue) + _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value() + + stock_queue.remove_stock(qty=abs(actual_qty)) + _qty, stock_value = stock_queue.get_total_stock_and_value() + + stock_value_difference = stock_value - prev_stock_value + incoming_rate = abs(flt(stock_value_difference) / abs(flt(actual_qty))) + stock_queue = stock_queue.state + else: + incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(row.batch_no))) + stock_queue.append([row.qty, incoming_rate]) + row.stock_queue = json.dumps(stock_queue) + else: + incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(row.batch_no))) + + return incoming_rate From d49b64dc7c44624028892cc4d921ae93981a072a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 6 Oct 2025 21:22:49 +0530 Subject: [PATCH 36/47] feat: recalculate batch qty (cherry picked from commit 70117d3b063ae9a3139355e08340cc2b800bf8ed) --- erpnext/stock/doctype/batch/batch.js | 11 +++++++++++ erpnext/stock/doctype/batch/batch.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/erpnext/stock/doctype/batch/batch.js b/erpnext/stock/doctype/batch/batch.js index a485f849639..7c9c0eeb09a 100644 --- a/erpnext/stock/doctype/batch/batch.js +++ b/erpnext/stock/doctype/batch/batch.js @@ -22,6 +22,17 @@ frappe.ui.form.on("Batch", { frappe.set_route("query-report", "Stock Ledger"); }); frm.trigger("make_dashboard"); + + frm.add_custom_button(__("Recalculate Batch Qty"), () => { + frm.call({ + method: "recalculate_batch_qty", + doc: frm.doc, + freeze: true, + callback: () => { + frm.reload_doc(); + }, + }); + }); } }, item: (frm) => { diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 7b87d75cee9..46618a2ca4a 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -156,6 +156,17 @@ class Batch(Document): if frappe.db.get_value("Item", self.item, "has_batch_no") == 0: frappe.throw(_("The selected item cannot have Batch")) + @frappe.whitelist() + def recalculate_batch_qty(self): + batches = get_batch_qty(batch_no=self.name, item_code=self.item) + batch_qty = 0.0 + if batches: + for row in batches: + batch_qty += row.get("qty") + + self.db_set("batch_qty", batch_qty) + frappe.msgprint(_("Batch Qty updated to {0}").format(batch_qty), alert=True) + def set_batchwise_valuation(self): from erpnext.stock.utils import get_valuation_method From ed1c2703985fa4642cb297ab839bd1f9fe8735e7 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Mon, 6 Oct 2025 18:14:37 +0530 Subject: [PATCH 37/47] fix: exclude opening entries (cherry picked from commit 3773f56b0b6d1671e29118587fa82d4169e21c70) --- .../assets/report/fixed_asset_register/fixed_asset_register.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index dfb33c55ef7..b66adca1343 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -268,6 +268,7 @@ def get_asset_depreciation_amount_map(filters, finance_book): .where(gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)) .where(gle.debit != 0) .where(gle.is_cancelled == 0) + .where(gle.is_opening == "No") .where(company.name == filters.company) .where(asset.docstatus == 1) ) From a4b5a74644ccd07e8536e139f58689084c20accc Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Fri, 26 Sep 2025 13:19:31 +0530 Subject: [PATCH 38/47] fix: use item valuation rate if no bin (cherry picked from commit 23b1b7ee04afe45c5f5ab1cd9a40f6b94b868cb7) --- erpnext/stock/get_item_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index b7204459e27..38073cfebb2 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1516,7 +1516,7 @@ def get_valuation_rate(item_code, company, warehouse=None): return frappe.db.get_value( "Bin", {"item_code": item_code, "warehouse": warehouse}, ["valuation_rate"], as_dict=True - ) or {"valuation_rate": 0} + ) or {"valuation_rate": item.get("valuation_rate") or 0} elif not item.get("is_stock_item"): pi_item = frappe.qb.DocType("Purchase Invoice Item") From 3f3fd20b31dfba428ad5e9cba40be4bf44cb5982 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 7 Oct 2025 09:49:10 +0530 Subject: [PATCH 39/47] fix: warning message if the batch has incorrect qty (cherry picked from commit 870181de87933d3adfc4b5647b91eeaefa491fa3) --- erpnext/stock/serial_batch_bundle.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 5d3a56fa652..4c30f249ccf 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1363,11 +1363,12 @@ def get_batch_current_qty(batch): def throw_negative_batch_validation(batch_no, qty): - frappe.throw( - _("The Batch {0} has negative quantity {1}. Please correct the quantity.").format( - bold(batch_no), bold(qty) - ), - title=_("Negative Batch Quantity"), + frappe.msgprint( + _( + "The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry." + ).format(bold(get_link_to_form("Batch", batch_no)), bold(qty)), + title=_("Warning!"), + indicator="orange", ) From c42dcbe739f9866c5e9d65504fe5def0b3a9290e Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 23 Sep 2025 19:10:23 +0530 Subject: [PATCH 40/47] fix: do not fetch disabled item tax template (cherry picked from commit b10cf4a928447dc768a1d90fb27fbc86d536ed14) # Conflicts: # erpnext/public/js/controllers/transaction.js # erpnext/stock/get_item_details.py --- erpnext/public/js/controllers/transaction.js | 9 +++++++++ erpnext/stock/get_item_details.py | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0af61025653..0cdbd36bbcf 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2575,11 +2575,20 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe return doc.company ? {filters: {company: doc.company}} : {}; } else { let filters = { +<<<<<<< HEAD 'item_code': item.item_code, 'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], 'item_group': item.item_group, "base_net_rate": item.base_net_rate, } +======= + item_code: item.item_code, + valid_from: ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], + item_group: item.item_group, + base_net_rate: item.base_net_rate, + disabled: 0, + }; +>>>>>>> b10cf4a928 (fix: do not fetch disabled item tax template) if (doc.tax_category) filters['tax_category'] = doc.tax_category; diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 38073cfebb2..9fb0bda1329 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -704,8 +704,15 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): taxes_with_no_validity = [] for tax in taxes: +<<<<<<< HEAD tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, "company") if tax_company == args["company"]: +======= + disabled, tax_company = frappe.get_cached_value( + "Item Tax Template", tax.item_tax_template, ["disabled", "company"] + ) + if not disabled and tax_company == ctx["company"]: +>>>>>>> b10cf4a928 (fix: do not fetch disabled item tax template) if tax.valid_from or tax.maximum_net_rate: # In purchase Invoice first preference will be given to supplier invoice date # if supplier date is not present then posting date From 07c3755f319e23715a40d0c9707324417e6c4a91 Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Sat, 27 Sep 2025 12:54:10 +0000 Subject: [PATCH 41/47] fix(manufacturing): prevent KeyError in BOM Creator when sub-assembly reused Ensure missing (fg_item, fg_reference_id) keys are initialized in production_item_wise_rm before appending items. This avoids crashes when the same sub-assembly is referenced under multiple parents. (cherry picked from commit 4f8b2e520aa8a6f476c488820ec56252967d9bcd) --- erpnext/manufacturing/doctype/bom_creator/bom_creator.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 4761e82a048..55d37b3e588 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -253,6 +253,13 @@ class BOMCreator(Document): if not row.fg_reference_id and production_item_wise_rm.get((row.fg_item, row.fg_reference_id)): frappe.throw(_("Please set Parent Row No for item {0}").format(row.fg_item)) + key = (row.fg_item, row.fg_reference_id) + if key not in production_item_wise_rm: + production_item_wise_rm.setdefault( + key, + frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}), + ) + production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row) reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items()))) From d47f3cc1014db0f395aab21c6632458e85cb27ff Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 7 Oct 2025 11:01:47 +0530 Subject: [PATCH 42/47] chore: resolve conflicts --- erpnext/public/js/controllers/transaction.js | 10 +--------- erpnext/stock/get_item_details.py | 7 +------ 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0cdbd36bbcf..b1b11bb291c 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2575,20 +2575,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe return doc.company ? {filters: {company: doc.company}} : {}; } else { let filters = { -<<<<<<< HEAD 'item_code': item.item_code, 'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], 'item_group': item.item_group, "base_net_rate": item.base_net_rate, + "disabled": 0, } -======= - item_code: item.item_code, - valid_from: ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], - item_group: item.item_group, - base_net_rate: item.base_net_rate, - disabled: 0, - }; ->>>>>>> b10cf4a928 (fix: do not fetch disabled item tax template) if (doc.tax_category) filters['tax_category'] = doc.tax_category; diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 9fb0bda1329..8e71367c663 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -704,15 +704,10 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): taxes_with_no_validity = [] for tax in taxes: -<<<<<<< HEAD - tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, "company") - if tax_company == args["company"]: -======= disabled, tax_company = frappe.get_cached_value( "Item Tax Template", tax.item_tax_template, ["disabled", "company"] ) - if not disabled and tax_company == ctx["company"]: ->>>>>>> b10cf4a928 (fix: do not fetch disabled item tax template) + if not disabled and tax_company == args["company"]: if tax.valid_from or tax.maximum_net_rate: # In purchase Invoice first preference will be given to supplier invoice date # if supplier date is not present then posting date From 7a457dafe09836cf3dc082fbf696c259c00ef722 Mon Sep 17 00:00:00 2001 From: rethik Date: Wed, 1 Oct 2025 17:08:15 +0530 Subject: [PATCH 43/47] chore: add show_disabled_items filter to show both enabled and disabled items (cherry picked from commit bf5f24c0e035a0dfa8a7b93aa72838e0cb692176) --- .../stock_qty_vs_serial_no_count.js | 5 +++++ .../stock_qty_vs_serial_no_count.py | 11 ++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js index 26cccb88297..df146358390 100644 --- a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js @@ -24,6 +24,11 @@ frappe.query_reports["Stock Qty vs Serial No Count"] = { }, reqd: 1, }, + { + fieldname: "show_disables_items", + label: __("Show Disabled Items"), + fieldtype: "Check", + }, ], formatter: function (value, row, column, data, default_formatter) { diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py index 70f04da4753..b29674bbd94 100644 --- a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py @@ -9,7 +9,7 @@ from frappe import _ def execute(filters=None): validate_warehouse(filters) columns = get_columns() - data = get_data(filters.warehouse) + data = get_data(filters.warehouse, filters.show_disables_items) return columns, data @@ -38,12 +38,13 @@ def get_columns(): return columns -def get_data(warehouse): +def get_data(warehouse, show_disables_items): + filters = {"has_serial_no": True} + if not show_disables_items: + filters["disabled"] = False serial_item_list = frappe.get_all( "Item", - filters={ - "has_serial_no": True, - }, + filters=filters, fields=["item_code", "item_name"], ) From baa6d2bcdca633d60bfb596fc76df5cc5ab8b8fd Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:10:15 +0530 Subject: [PATCH 44/47] feat: dynamic due date in payment terms when fetched from order (backport #48864) (#49938) * feat: dynamic due date in payment terms when fetched from order (#48864) * fix: dynamic due date when payment terms are fetched from order * fix(test): use change_settings decorator for settings enable and disable * fix(test): compare schedule for due_date dynamically * fix: save conditions for due date at invoice level * fix: make fields read only and on change of date unset the date condition fields * fix: remove fetch_form * fix: correct field assingment * fix: revert unwanted changes * refactor: streamline payment term field assignments and enhance discount date handling * refactor: remove payment_term from fields_to_copy and optimize currency handling in transaction callback * refactor: ensure default values for payment schedule and discount validity fields (cherry picked from commit 3c70cbbaf8929b358008c2bab6e0c799466ea8b7) # Conflicts: # erpnext/public/js/controllers/transaction.js # erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py * chore: resolve conflicts --------- Co-authored-by: Lakshit Jain --- .../payment_schedule/payment_schedule.json | 53 +++++++++++++++++-- .../payment_schedule/payment_schedule.py | 15 ++++++ .../payment_terms_template_detail.json | 2 +- .../purchase_invoice/test_purchase_invoice.py | 6 +-- .../purchase_order/test_purchase_order.py | 14 ++--- erpnext/controllers/accounts_controller.py | 47 ++++++++++++---- erpnext/public/js/controllers/transaction.js | 45 ++++++++++++---- .../doctype/sales_order/test_sales_order.py | 19 +++---- .../delivery_note/test_delivery_note.py | 6 +-- .../purchase_receipt/test_purchase_receipt.py | 6 +-- 10 files changed, 149 insertions(+), 64 deletions(-) diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json index b72281b6314..2b3e396a052 100644 --- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json +++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json @@ -10,14 +10,19 @@ "description", "section_break_4", "due_date", + "invoice_portion", "mode_of_payment", "column_break_5", - "invoice_portion", + "due_date_based_on", + "credit_days", + "credit_months", "section_break_6", - "discount_type", "discount_date", - "column_break_9", "discount", + "discount_type", + "column_break_9", + "discount_validity_based_on", + "discount_validity", "section_break_9", "payment_amount", "outstanding", @@ -172,12 +177,50 @@ "label": "Paid Amount (Company Currency)", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "fieldname": "due_date_based_on", + "fieldtype": "Select", + "label": "Due Date Based On", + "options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month", + "read_only": 1 + }, + { + "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)", + "fieldname": "credit_days", + "fieldtype": "Int", + "label": "Credit Days", + "non_negative": 1, + "read_only": 1 + }, + { + "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'", + "fieldname": "credit_months", + "fieldtype": "Int", + "label": "Credit Months", + "non_negative": 1, + "read_only": 1 + }, + { + "depends_on": "discount", + "fieldname": "discount_validity_based_on", + "fieldtype": "Select", + "label": "Discount Validity Based On", + "options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month", + "read_only": 1 + }, + { + "depends_on": "discount_validity_based_on", + "fieldname": "discount_validity", + "fieldtype": "Int", + "label": "Discount Validity", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-03-11 11:06:51.792982", + "modified": "2025-07-31 08:38:25.820701", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Schedule", @@ -189,4 +232,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.py b/erpnext/accounts/doctype/payment_schedule/payment_schedule.py index a3d1dbe5564..f506be0eb27 100644 --- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.py +++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.py @@ -17,12 +17,27 @@ class PaymentSchedule(Document): base_outstanding: DF.Currency base_paid_amount: DF.Currency base_payment_amount: DF.Currency + credit_days: DF.Int + credit_months: DF.Int description: DF.SmallText | None discount: DF.Float discount_date: DF.Date | None discount_type: DF.Literal["Percentage", "Amount"] + discount_validity: DF.Int + discount_validity_based_on: DF.Literal[ + "", + "Day(s) after invoice date", + "Day(s) after the end of the invoice month", + "Month(s) after the end of the invoice month", + ] discounted_amount: DF.Currency due_date: DF.Date + due_date_based_on: DF.Literal[ + "", + "Day(s) after invoice date", + "Day(s) after the end of the invoice month", + "Month(s) after the end of the invoice month", + ] invoice_portion: DF.Percent mode_of_payment: DF.Link | None outstanding: DF.Currency diff --git a/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json index 20b3dca6aae..fca9eaa53c2 100644 --- a/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json +++ b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json @@ -161,4 +161,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index f0446b35e5d..90654b42d02 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2150,19 +2150,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): rate = flt(sle.stock_value_difference) / flt(sle.actual_qty) self.assertAlmostEqual(rate, 500) + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) def test_payment_allocation_for_payment_terms(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import ( create_pr_against_po, create_purchase_order, ) - from erpnext.selling.doctype.sales_order.test_sales_order import ( - automatically_fetch_payment_terms, - ) from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( make_purchase_invoice as make_pi_from_pr, ) - automatically_fetch_payment_terms() frappe.db.set_value( "Payment Terms Template", "_Test Payment Term Template", @@ -2188,7 +2185,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): pi = make_pi_from_pr(pr.name) self.assertEqual(pi.payment_schedule[0].payment_amount, 1000) - automatically_fetch_payment_terms(enable=0) frappe.db.set_value( "Payment Terms Template", "_Test Payment Term Template", diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 206cbc0000e..895df37253f 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -541,12 +541,8 @@ class TestPurchaseOrder(FrappeTestCase): self.assertRaises(frappe.ValidationError, pr.submit) self.assertRaises(frappe.ValidationError, pi.submit) + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) def test_make_purchase_invoice_with_terms(self): - from erpnext.selling.doctype.sales_order.test_sales_order import ( - automatically_fetch_payment_terms, - ) - - automatically_fetch_payment_terms() po = create_purchase_order(do_not_save=True) self.assertRaises(frappe.ValidationError, make_pi_from_po, po.name) @@ -570,7 +566,6 @@ class TestPurchaseOrder(FrappeTestCase): self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date)) self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0) self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30)) - automatically_fetch_payment_terms(enable=0) def test_warehouse_company_validation(self): from erpnext.stock.utils import InvalidWarehouseCompany @@ -718,6 +713,7 @@ class TestPurchaseOrder(FrappeTestCase): ) self.assertEqual(due_date, "2023-03-31") + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 0}) def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self): po = create_purchase_order(do_not_save=1) po.payment_terms_template = "_Test Payment Term Template" @@ -905,18 +901,16 @@ class TestPurchaseOrder(FrappeTestCase): bo.load_from_db() self.assertEqual(bo.items[0].ordered_qty, 5) + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, ) from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.selling.doctype.sales_order.test_sales_order import ( - automatically_fetch_payment_terms, compare_payment_schedules, ) - automatically_fetch_payment_terms() - po = create_purchase_order(qty=10, rate=100, do_not_save=1) create_payment_terms_template() po.payment_terms_template = "Test Receivable Template" @@ -930,8 +924,6 @@ class TestPurchaseOrder(FrappeTestCase): # self.assertEqual(po.payment_terms_template, pi.payment_terms_template) compare_payment_schedules(self, po, pi) - automatically_fetch_payment_terms(enable=0) - def test_internal_transfer_flow(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fb8b26085eb..96864bb5a60 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2558,6 +2558,7 @@ class AccountsController(TransactionBase): self.payment_schedule = [] self.payment_terms_template = po_or_so.payment_terms_template + posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date") for schedule in po_or_so.payment_schedule: payment_schedule = { @@ -2570,6 +2571,17 @@ class AccountsController(TransactionBase): } if automatically_fetch_payment_terms: + if schedule.due_date_based_on: + payment_schedule["due_date"] = get_due_date(schedule, posting_date) + payment_schedule["due_date_based_on"] = schedule.due_date_based_on + payment_schedule["credit_days"] = cint(schedule.credit_days) + payment_schedule["credit_months"] = cint(schedule.credit_months) + + if schedule.discount_validity_based_on: + payment_schedule["discount_date"] = get_discount_date(schedule, posting_date) + payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on + payment_schedule["discount_validity"] = cint(schedule.discount_validity) + payment_schedule["payment_amount"] = flt( grand_total * flt(payment_schedule["invoice_portion"]) / 100, schedule.precision("payment_amount"), @@ -3369,14 +3381,27 @@ def get_payment_term_details( term = frappe.get_doc("Payment Term", term) else: term_details.payment_term = term.payment_term - term_details.description = term.description - term_details.invoice_portion = term.invoice_portion + + fields_to_copy = [ + "description", + "invoice_portion", + "discount_type", + "discount", + "mode_of_payment", + "due_date_based_on", + "credit_days", + "credit_months", + "discount_validity_based_on", + "discount_validity", + ] + + for field in fields_to_copy: + term_details[field] = term.get(field) + term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100 term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100 - term_details.discount_type = term.discount_type - term_details.discount = term.discount term_details.outstanding = term_details.payment_amount - term_details.mode_of_payment = term.mode_of_payment + term_details.base_outstanding = term_details.base_payment_amount if bill_date: term_details.due_date = get_due_date(term, bill_date) @@ -3395,11 +3420,11 @@ def get_due_date(term, posting_date=None, bill_date=None): due_date = None date = bill_date or posting_date if term.due_date_based_on == "Day(s) after invoice date": - due_date = add_days(date, term.credit_days) + due_date = add_days(date, cint(term.credit_days)) elif term.due_date_based_on == "Day(s) after the end of the invoice month": - due_date = add_days(get_last_day(date), term.credit_days) + due_date = add_days(get_last_day(date), cint(term.credit_days)) elif term.due_date_based_on == "Month(s) after the end of the invoice month": - due_date = get_last_day(add_months(date, term.credit_months)) + due_date = get_last_day(add_months(date, cint(term.credit_months))) return due_date @@ -3407,11 +3432,11 @@ def get_discount_date(term, posting_date=None, bill_date=None): discount_validity = None date = bill_date or posting_date if term.discount_validity_based_on == "Day(s) after invoice date": - discount_validity = add_days(date, term.discount_validity) + discount_validity = add_days(date, cint(term.discount_validity)) elif term.discount_validity_based_on == "Day(s) after the end of the invoice month": - discount_validity = add_days(get_last_day(date), term.discount_validity) + discount_validity = add_days(get_last_day(date), cint(term.discount_validity)) elif term.discount_validity_based_on == "Month(s) after the end of the invoice month": - discount_validity = get_last_day(add_months(date, term.discount_validity)) + discount_validity = get_last_day(add_months(date, cint(term.discount_validity))) return discount_validity diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b1b11bb291c..ae3b7404f7f 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1082,12 +1082,25 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } - due_date(doc, cdt) { + discount_date(doc, cdt, cdn) { + // Remove fields as discount_date is auto-managed by payment terms + const row = locals[cdt][cdn]; + ["discount_validity", "discount_validity_based_on"].forEach((field) => { + row[field] = ""; + }); + this.frm.refresh_field("payment_schedule"); + } + + due_date(doc, cdt, cdn) { // due_date is to be changed, payment terms template and/or payment schedule must // be removed as due_date is automatically changed based on payment terms if (doc.doctype !== cdt) { - // triggered by change to the due_date field in payment schedule child table - // do nothing to avoid infinite clearing loop + // Remove fields as due_date is auto-managed by payment terms + const row = locals[cdt][cdn]; + ["due_date_based_on", "credit_days", "credit_months"].forEach((field) => { + row[field] = ""; + }); + this.frm.refresh_field("payment_schedule"); return; } @@ -2621,6 +2634,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe payment_term(doc, cdt, cdn) { const me = this; var row = locals[cdt][cdn]; + // empty date condition fields + [ + "due_date_based_on", + "credit_days", + "credit_months", + "discount_validity", + "discount_validity_based_on", + ].forEach(function (field) { + row[field] = ""; + }); + if(row.payment_term) { frappe.call({ method: "erpnext.controllers.accounts_controller.get_payment_term_details", @@ -2633,14 +2657,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }, callback: function(r) { if(r.message && !r.exc) { - for (var d in r.message) { - frappe.model.set_value(cdt, cdn, d, r.message[d]); - const company_currency = me.get_company_currency(); - me.update_payment_schedule_grid_labels(company_currency); + const company_currency = me.get_company_currency(); + for (let d in r.message) { + row[d] = r.message[d]; } + me.update_payment_schedule_grid_labels(company_currency) + me.frm.refresh_field("payment_schedule"); } - } - }) + }, + }); + } else { + me.frm.refresh_field("payment_schedule"); } } diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 04a3a989cae..db4d60b56f6 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, getdate, nowdate, today from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate +from erpnext.controllers.accounts_controller import InvalidQtyError, get_due_date, update_child_qty_rate from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( make_maintenance_schedule, ) @@ -1680,14 +1680,13 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): so.load_from_db() self.assertRaises(frappe.LinkExistsError, so.cancel) + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, ) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice - automatically_fetch_payment_terms() - so = make_sales_order(uom="Nos", do_not_save=1) create_payment_terms_template() so.payment_terms_template = "Test Receivable Template" @@ -1701,8 +1700,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertEqual(so.payment_terms_template, si.payment_terms_template) compare_payment_schedules(self, so, si) - automatically_fetch_payment_terms(enable=0) - def test_zero_amount_sales_order_billing_status(self): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -2421,16 +2418,14 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertEqual(si2.items[0].qty, 20) -def automatically_fetch_payment_terms(enable=1): - accounts_settings = frappe.get_doc("Accounts Settings") - accounts_settings.automatically_fetch_payment_terms = enable - accounts_settings.save() - - def compare_payment_schedules(doc, doc1, doc2): for index, schedule in enumerate(doc1.get("payment_schedule")): + posting_date = doc1.get("bill_date") or doc1.get("posting_date") or doc1.get("transaction_date") + due_date = schedule.due_date + if schedule.due_date_based_on: + due_date = get_due_date(schedule, posting_date=posting_date) doc.assertEqual(schedule.payment_term, doc2.payment_schedule[index].payment_term) - doc.assertEqual(getdate(schedule.due_date), doc2.payment_schedule[index].due_date) + doc.assertEqual(due_date, doc2.payment_schedule[index].due_date) doc.assertEqual(schedule.invoice_portion, doc2.payment_schedule[index].invoice_portion) doc.assertEqual(schedule.payment_amount, doc2.payment_schedule[index].payment_amount) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 00ce64b614c..8e1a38b5ea7 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -15,7 +15,6 @@ from erpnext.accounts.utils import get_balance_on from erpnext.controllers.accounts_controller import InvalidQtyError from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.test_sales_order import ( - automatically_fetch_payment_terms, compare_payment_schedules, create_dn_against_so, make_sales_order, @@ -1300,14 +1299,13 @@ class TestDeliveryNote(FrappeTestCase): frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 0) + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, ) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice - automatically_fetch_payment_terms() - so = make_sales_order(uom="Nos", do_not_save=1) create_payment_terms_template() so.payment_terms_template = "Test Receivable Template" @@ -1327,8 +1325,6 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(so.payment_terms_template, si.payment_terms_template) compare_payment_schedules(self, so, si) - automatically_fetch_payment_terms(enable=0) - def test_returned_qty_in_return_dn(self): # SO ---> SI ---> DN # | diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b7014b19c4a..c0a6c4d0f4a 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1180,6 +1180,7 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, @@ -1190,12 +1191,9 @@ class TestPurchaseReceipt(FrappeTestCase): make_pr_against_po, ) from erpnext.selling.doctype.sales_order.test_sales_order import ( - automatically_fetch_payment_terms, compare_payment_schedules, ) - automatically_fetch_payment_terms() - po = create_purchase_order(qty=10, rate=100, do_not_save=1) create_payment_terms_template() po.payment_terms_template = "Test Receivable Template" @@ -1213,8 +1211,6 @@ class TestPurchaseReceipt(FrappeTestCase): # self.assertEqual(po.payment_terms_template, pi.payment_terms_template) compare_payment_schedules(self, po, pi) - automatically_fetch_payment_terms(enable=0) - @change_settings("Stock Settings", {"allow_negative_stock": 1}) def test_neg_to_positive(self): from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry From e02a55b188a328fe791675b8b666ab78767dbea8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 7 Oct 2025 12:00:41 +0530 Subject: [PATCH 45/47] refactor: old serial nos filter (cherry picked from commit 6a8bd0ae9e95dad94683ba112d10255614c8c053) # Conflicts: # erpnext/stock/deprecated_serial_batch.py --- erpnext/stock/deprecated_serial_batch.py | 29 ++++++++++-------------- erpnext/stock/serial_batch_bundle.py | 2 ++ 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 8d1b76148d6..44d5b8fd625 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -13,10 +13,10 @@ from pypika.functions import Coalesce class DeprecatedSerialNoValuation: @deprecated def calculate_stock_value_from_deprecarated_ledgers(self): - if not has_sle_for_serial_nos(self.sle.item_code): - return + serial_nos = [] + if hasattr(self, "old_serial_nos"): + serial_nos = self.old_serial_nos - serial_nos = self.get_filterd_serial_nos() if not serial_nos: return @@ -26,6 +26,7 @@ class DeprecatedSerialNoValuation: self.stock_value_change += flt(stock_value_change) +<<<<<<< HEAD def get_filterd_serial_nos(self): serial_nos = [] non_filtered_serial_nos = self.get_serial_nos() @@ -38,6 +39,14 @@ class DeprecatedSerialNoValuation: return serial_nos @deprecated +======= + @deprecated( + "erpnext.stock.serial_batch_bundle.SerialNoValuation.get_incoming_value_for_serial_nos", + "unknown", + "v16", + "No known instructions.", + ) +>>>>>>> 6a8bd0ae9e (refactor: old serial nos filter) def get_incoming_value_for_serial_nos(self, serial_nos): from erpnext.stock.utils import get_combine_datetime @@ -82,20 +91,6 @@ class DeprecatedSerialNoValuation: return incoming_values -@frappe.request_cache -def has_sle_for_serial_nos(item_code): - serial_nos = frappe.db.get_all( - "Stock Ledger Entry", - fields=["name"], - filters={"serial_no": ("is", "set"), "is_cancelled": 0, "item_code": item_code}, - limit=1, - ) - if serial_nos: - return True - - return False - - class DeprecatedBatchNoValuation: @deprecated def calculate_avg_rate_from_deprecarated_ledgers(self): diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 4c30f249ccf..2408c178f2b 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -580,11 +580,13 @@ class SerialNoValuation(DeprecatedSerialNoValuation): else: self.serial_no_incoming_rate = defaultdict(float) self.stock_value_change = 0.0 + self.old_serial_nos = [] serial_nos = self.get_serial_nos() for serial_no in serial_nos: incoming_rate = self.get_incoming_rate_from_bundle(serial_no) if incoming_rate is None: + self.old_serial_nos.append(serial_no) continue self.stock_value_change += incoming_rate From 6ea07ba56dc1f838fc29fa4d7bd639fe325337c4 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 7 Oct 2025 16:02:30 +0530 Subject: [PATCH 46/47] chore: fix conflicts --- erpnext/stock/deprecated_serial_batch.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 44d5b8fd625..69443e3a608 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -26,27 +26,7 @@ class DeprecatedSerialNoValuation: self.stock_value_change += flt(stock_value_change) -<<<<<<< HEAD - def get_filterd_serial_nos(self): - serial_nos = [] - non_filtered_serial_nos = self.get_serial_nos() - - # If the serial no inwarded using the Serial and Batch Bundle, then the serial no should not be considered - for serial_no in non_filtered_serial_nos: - if serial_no and serial_no not in self.serial_no_incoming_rate: - serial_nos.append(serial_no) - - return serial_nos - @deprecated -======= - @deprecated( - "erpnext.stock.serial_batch_bundle.SerialNoValuation.get_incoming_value_for_serial_nos", - "unknown", - "v16", - "No known instructions.", - ) ->>>>>>> 6a8bd0ae9e (refactor: old serial nos filter) def get_incoming_value_for_serial_nos(self, serial_nos): from erpnext.stock.utils import get_combine_datetime From 11eab0c8526b923efe50cb2489e03c54d8ef419d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:01:37 +0530 Subject: [PATCH 47/47] Merge pull request #49946 from frappe/mergify/bp/version-15-hotfix/pr-49721 feat: add company links to Email Account and Communication (backport #49721) --- erpnext/patches.txt | 1 + .../v16_0/create_company_custom_fields.py | 6 ++++ erpnext/setup/install.py | 34 +++++++++++++++++++ erpnext/support/doctype/issue/issue.json | 4 ++- 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v16_0/create_company_custom_fields.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index fd3dc3ad3cd..0abf1b42ee4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -263,6 +263,7 @@ execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Deta erpnext.patches.v14_0.update_proprietorship_to_individual erpnext.patches.v15_0.rename_subcontracting_fields erpnext.patches.v15_0.unset_incorrect_additional_discount_percentage +erpnext.patches.v16_0.create_company_custom_fields [post_model_sync] erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets diff --git a/erpnext/patches/v16_0/create_company_custom_fields.py b/erpnext/patches/v16_0/create_company_custom_fields.py new file mode 100644 index 00000000000..76ab40f10b9 --- /dev/null +++ b/erpnext/patches/v16_0/create_company_custom_fields.py @@ -0,0 +1,6 @@ +from erpnext.setup.install import create_custom_company_links + + +def execute(): + """Add link fields to Company in Email Account and Communication.""" + create_custom_company_links() diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index d5f2c5422bd..9491da7ec3b 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -25,6 +25,7 @@ def after_install(): set_single_defaults() create_print_setting_custom_fields() + create_custom_company_links() add_all_roles_to("Administrator") create_default_success_action() create_default_energy_point_rules() @@ -132,6 +133,39 @@ def create_print_setting_custom_fields(): ) +def create_custom_company_links(): + """Add link fields to Company in Email Account and Communication. + + These DocTypes are provided by the Frappe Framework but need to be associated + with a company in ERPNext to allow for multitenancy. I.e. one company should + not be able to access emails and communications from another company. + """ + create_custom_fields( + { + "Email Account": [ + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "insert_after": "email_id", + }, + ], + "Communication": [ + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "insert_after": "email_account", + "fetch_from": "email_account.company", + "read_only": 1, + }, + ], + }, + ) + + def create_default_success_action(): for success_action in get_default_success_action(): if not frappe.db.exists("Success Action", success_action.get("ref_doctype")): diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 622e19e9e3a..fb36538ea6c 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -233,6 +233,8 @@ "options": "Project" }, { + "fetch_from": "email_account.company", + "fetch_if_empty": 1, "fieldname": "company", "fieldtype": "Link", "label": "Company", @@ -391,7 +393,7 @@ "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2025-02-18 21:18:52.797745", + "modified": "2025-09-25 11:10:53.556731", "modified_by": "Administrator", "module": "Support", "name": "Issue",