From 2f10b9c5104367bbeaf90e6ed071d335967a8f34 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 23 Dec 2025 15:32:02 +0530 Subject: [PATCH 01/17] fix: validate depreciation row values --- erpnext/assets/doctype/asset/asset.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 33c6604f01f..5e869ba5df6 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -477,6 +477,7 @@ class Asset(AccountsController): def set_depreciation_rate(self): for d in self.get("finance_books"): + self.validate_asset_finance_books(d) d.rate_of_depreciation = flt( self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") ) @@ -485,6 +486,10 @@ class Asset(AccountsController): row.expected_value_after_useful_life = flt( row.expected_value_after_useful_life, self.precision("gross_purchase_amount") ) + + if flt(row.expected_value_after_useful_life) < 0: + frappe.throw(_("Row {0}: Expected Value After Useful Life cannot be negative").format(row.idx)) + if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): frappe.throw( _("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format( @@ -501,6 +506,8 @@ class Asset(AccountsController): ) row.depreciation_start_date = get_last_day(self.available_for_use_date) + self.validate_total_number_of_depreciations_and_frequency(row) + if not self.is_existing_asset: self.opening_accumulated_depreciation = 0 self.opening_number_of_booked_depreciations = 0 @@ -546,6 +553,15 @@ class Asset(AccountsController): ).format(row.idx) ) + def validate_total_number_of_depreciations_and_frequency(self, row): + if row.total_number_of_depreciations <= 0: + frappe.throw( + _("Row #{0}: Total Number of Depreciations must be greater than zero").format(row.idx) + ) + + if row.frequency_of_depreciation <= 0: + frappe.throw(_("Row #{0}: Frequency of Depreciation must be greater than zero").format(row.idx)) + def set_total_booked_depreciations(self): # set value of total number of booked depreciations field for fb_row in self.get("finance_books"): From 6ff002dbe30482837d4d2d54dd8de366fc3ec048 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 23 Dec 2025 15:44:43 +0530 Subject: [PATCH 02/17] refactor: split long function into smaller --- erpnext/assets/doctype/asset/asset.py | 82 +++++++++++++++------------ 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 5e869ba5df6..fc9a995dfff 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -505,52 +505,32 @@ class Asset(AccountsController): title=_("Invalid Schedule"), ) row.depreciation_start_date = get_last_day(self.available_for_use_date) - + self.validate_depreciation_start_date(row) self.validate_total_number_of_depreciations_and_frequency(row) if not self.is_existing_asset: self.opening_accumulated_depreciation = 0 self.opening_number_of_booked_depreciations = 0 else: - depreciable_amount = flt( - flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life), - self.precision("gross_purchase_amount"), - ) - if flt(self.opening_accumulated_depreciation) > depreciable_amount: + self.validate_opening_depreciation_values(row) + + def validate_depreciation_start_date(self, row): + if row.depreciation_start_date: + if getdate(row.depreciation_start_date) < getdate(self.purchase_date): frappe.throw( - _("Opening Accumulated Depreciation must be less than or equal to {0}").format( - depreciable_amount + _("Row #{0}: Next Depreciation Date cannot be before Purchase Date").format(row.idx) + ) + + if getdate(row.depreciation_start_date) < getdate(self.available_for_use_date): + frappe.throw( + _("Row #{0}: Next Depreciation Date cannot be before Available-for-use Date").format( + row.idx ) ) - - if self.opening_accumulated_depreciation: - if not self.opening_number_of_booked_depreciations: - frappe.throw(_("Please set Opening Number of Booked Depreciations")) - else: - self.opening_number_of_booked_depreciations = 0 - - if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations): - frappe.throw( - _( - "Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations" - ).format(row.idx), - title=_("Invalid Schedule"), - ) - - if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date): + else: frappe.throw( - _("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date").format( - row.idx - ) - ) - - if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate( - self.available_for_use_date - ): - frappe.throw( - _( - "Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date" - ).format(row.idx) + _("Row #{0}: Depreciation Start Date is required").format(row.idx), + title=_("Invalid Schedule"), ) def validate_total_number_of_depreciations_and_frequency(self, row): @@ -562,6 +542,36 @@ class Asset(AccountsController): if row.frequency_of_depreciation <= 0: frappe.throw(_("Row #{0}: Frequency of Depreciation must be greater than zero").format(row.idx)) + def validate_opening_depreciation_values(self, row): + row.expected_value_after_useful_life = flt( + row.expected_value_after_useful_life, self.precision("gross_purchase_amount") + ) + + depreciable_amount = flt( + flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life), + self.precision("gross_purchase_amount"), + ) + if flt(self.opening_accumulated_depreciation) > depreciable_amount: + frappe.throw( + _("Opening Accumulated Depreciation must be less than or equal to {0}").format( + depreciable_amount + ) + ) + + if self.opening_accumulated_depreciation: + if not self.opening_number_of_booked_depreciations: + frappe.throw(_("Please set Opening Number of Booked Depreciations")) + else: + self.opening_number_of_booked_depreciations = 0 + + if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations): + frappe.throw( + _( + "Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations" + ).format(row.idx), + title=_("Invalid Schedule"), + ) + def set_total_booked_depreciations(self): # set value of total number of booked depreciations field for fb_row in self.get("finance_books"): From 93c1a3f8f30a00b2ef5413b74ab653f39d080526 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Wed, 24 Dec 2025 11:52:48 +0530 Subject: [PATCH 03/17] fix(repost accounting ledger): prevent preview generation when no vouchers are selected (cherry picked from commit bd9f5fca084dfbdf37a59b561f8ac5216d52af29) --- .../repost_accounting_ledger/repost_accounting_ledger.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index 11614467472..e9e29d2cb31 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -115,6 +115,10 @@ class RepostAccountingLedger(Document): def generate_preview(self): from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns + if not self.vouchers: + frappe.msgprint(_("Add vouchers to generate preview.")) + return + gl_columns = [] gl_data = [] From a04f5600483fea35cec317b16027df930475a48b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:11:28 +0530 Subject: [PATCH 04/17] =?UTF-8?q?fix(accounts-payable-summary):=20add=20Sh?= =?UTF-8?q?ow=20GL=20Balance=20check=20similar=20to=20A=E2=80=A6=20(backpo?= =?UTF-8?q?rt=20#50802)=20(#50805)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../accounts_payable_summary.js | 5 +++++ .../accounts_receivable_summary.py | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 18a85af95be..6a043f5e185 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -102,6 +102,11 @@ frappe.query_reports["Accounts Payable Summary"] = { label: __("Revaluation Journals"), fieldtype: "Check", }, + { + fieldname: "show_gl_balance", + label: __("Show GL Balance"), + fieldtype: "Check", + }, ], onload: function (report) { 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 19fd7dc96ef..19d2faddf44 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -53,7 +53,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): ) if self.filters.show_gl_balance: - gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company) + gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company, self.account_type) for party, party_dict in self.party_total.items(): if flt(party_dict.outstanding, self.currency_precision) == 0: @@ -206,11 +206,15 @@ class AccountsReceivableSummary(ReceivablePayableReport): ) -def get_gl_balance(report_date, company): +def get_gl_balance(report_date, company, account_type): + if account_type == "Payable": + balance_calc_fields = ["party", "SUM(credit - debit) AS balance"] + else: + balance_calc_fields = ["party", "SUM(debit - credit) AS balance"] return frappe._dict( frappe.db.get_all( "GL Entry", - fields=["party", "sum(debit - credit)"], + fields=balance_calc_fields, filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company}, group_by="party", as_list=1, From 4753594a26901a9d852d01d65690a4af514bfa70 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 24 Dec 2025 14:31:12 +0530 Subject: [PATCH 05/17] perf: index for warehouse field (cherry picked from commit 23c70332dfd694f204dd4815bf91c7a96f9941ef) # Conflicts: # erpnext/stock/doctype/serial_no/serial_no.json --- erpnext/stock/doctype/serial_no/serial_no.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 924277b30ea..5df48737973 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -234,7 +234,8 @@ "in_list_view": 1, "label": "Warehouse", "options": "Warehouse", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "batch_no", @@ -282,7 +283,11 @@ "icon": "fa fa-barcode", "idx": 1, "links": [], +<<<<<<< HEAD "modified": "2025-07-15 13:40:21.938700", +======= + "modified": "2025-12-24 14:30:43.599590", +>>>>>>> 23c70332df (perf: index for warehouse field) "modified_by": "Administrator", "module": "Stock", "name": "Serial No", From 35ae839ab70f8ac470d372d1d3007dc0e886e415 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 24 Dec 2025 15:06:48 +0530 Subject: [PATCH 06/17] chore: fix conflicts --- erpnext/stock/doctype/serial_no/serial_no.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 5df48737973..a81517cf7eb 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -283,11 +283,7 @@ "icon": "fa fa-barcode", "idx": 1, "links": [], -<<<<<<< HEAD - "modified": "2025-07-15 13:40:21.938700", -======= "modified": "2025-12-24 14:30:43.599590", ->>>>>>> 23c70332df (perf: index for warehouse field) "modified_by": "Administrator", "module": "Stock", "name": "Serial No", From fd718833b1e931c3d4435ab41da2a569a0739dfd Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 1 Dec 2025 13:12:23 +0530 Subject: [PATCH 07/17] refactor: optimize picked quantity updates using bulk_update (cherry picked from commit 5f986e40326062fd3274e06f57ec33d9a32e82c3) --- erpnext/stock/doctype/pick_list/pick_list.py | 28 ++++++-------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 8d251d143a9..a4a335d2c5c 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -383,35 +383,23 @@ class PickList(TransactionBase): picked_items = get_picked_items_qty(packed_items, contains_packed_items=True) self.validate_picked_qty(picked_items) - picked_qty = frappe._dict() + doc_updates = {} for d in picked_items: - picked_qty[d.product_bundle_item] = d.picked_qty + doc_updates[d.product_bundle_item] = {"picked_qty": flt(d.picked_qty)} - for packed_item in packed_items: - frappe.db.set_value( - "Packed Item", - packed_item, - "picked_qty", - flt(picked_qty.get(packed_item)), - update_modified=False, - ) + if doc_updates: + frappe.db.bulk_update("Packed Item", doc_updates, update_modified=False) def update_sales_order_item_qty(self, so_items): picked_items = get_picked_items_qty(so_items) self.validate_picked_qty(picked_items) - picked_qty = frappe._dict() + doc_updates = {} for d in picked_items: - picked_qty[d.sales_order_item] = d.picked_qty + doc_updates[d.sales_order_item] = {"picked_qty": flt(d.picked_qty)} - for so_item in so_items: - frappe.db.set_value( - "Sales Order Item", - so_item, - "picked_qty", - flt(picked_qty.get(so_item)), - update_modified=False, - ) + if doc_updates: + frappe.db.bulk_update("Sales Order Item", doc_updates, update_modified=False) def update_sales_order_picking_status(self) -> None: sales_orders = [] From 9d2e0f67d5de35b3f4413f0be6b553fda2978bc6 Mon Sep 17 00:00:00 2001 From: Nishka Gosalia Date: Wed, 24 Dec 2025 17:27:04 +0530 Subject: [PATCH 08/17] fix: updating base amounts through python for timesheet for v15 --- .../projects/doctype/timesheet/test_timesheet.py | 10 ++++++++++ erpnext/projects/doctype/timesheet/timesheet.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 1e2688daf4d..e17fa3d622c 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -17,6 +17,15 @@ class TestTimesheet(unittest.TestCase): def setUp(self): frappe.db.delete("Timesheet") + def test_timesheet_base_amount(self): + emp = make_employee("test_employee_6@salary.com") + timesheet = make_timesheet(emp, simulate=True, is_billable=1) + + self.assertEqual(timesheet.time_logs[0].base_billing_rate, 50) + self.assertEqual(timesheet.time_logs[0].base_costing_rate, 20) + self.assertEqual(timesheet.time_logs[0].base_billing_amount, 100) + self.assertEqual(timesheet.time_logs[0].base_costing_amount, 40) + def test_timesheet_billing_amount(self): emp = make_employee("test_employee_6@salary.com") timesheet = make_timesheet(emp, simulate=True, is_billable=1) @@ -236,4 +245,5 @@ def make_timesheet( def update_activity_type(activity_type): activity_type = frappe.get_doc("Activity Type", activity_type) activity_type.billing_rate = 50.0 + activity_type.costing_rate = 20.0 activity_type.save(ignore_permissions=True) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 0b4b99ba35b..ec58c55f020 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -296,6 +296,20 @@ class Timesheet(Document): data.billing_amount = data.billing_rate * hours data.costing_amount = data.costing_rate * costing_hours + exchange_rate = flt(self.get("exchange_rate")) or 1.0 + data.base_billing_rate = flt( + data.billing_rate * exchange_rate, data.precision("base_billing_rate") + ) + data.base_costing_rate = flt( + data.costing_rate * exchange_rate, data.precision("base_costing_rate") + ) + data.base_billing_amount = flt( + data.billing_amount * exchange_rate, data.precision("base_billing_amount") + ) + data.base_costing_amount = flt( + data.costing_amount * exchange_rate, data.precision("base_costing_amount") + ) + def update_time_rates(self, ts_detail): if not ts_detail.is_billable: ts_detail.billing_rate = 0.0 From 507a561922bdd63273c15bd1dbdb4ea7fb1dbbf4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 24 Dec 2025 20:10:38 +0530 Subject: [PATCH 09/17] perf: composite index for serial no (cherry picked from commit 734d55333834aef10976d464ba1de45c8e125605) --- erpnext/stock/doctype/serial_no/serial_no.json | 5 ++--- erpnext/stock/doctype/serial_no/serial_no.py | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index a81517cf7eb..9dfa2cc15c0 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -234,8 +234,7 @@ "in_list_view": 1, "label": "Warehouse", "options": "Warehouse", - "read_only": 1, - "search_index": 1 + "read_only": 1 }, { "fieldname": "batch_no", @@ -283,7 +282,7 @@ "icon": "fa fa-barcode", "idx": 1, "links": [], - "modified": "2025-12-24 14:30:43.599590", + "modified": "2025-12-24 20:14:52.942251", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 896323d6529..9622cc725f6 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -302,3 +302,7 @@ def get_serial_nos_for_outward(kwargs): return [] return [d.serial_no for d in serial_nos] + + +def on_doctype_update(): + frappe.db.add_index("Serial No", ["item_code", "warehouse"]) From d9888d51955e8136d0750ad7612ae06aea191467 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 25 Dec 2025 13:24:04 +0530 Subject: [PATCH 10/17] fix(stock): remove total bar in chart view (cherry picked from commit 7df349844ab27299452b0ab634160404f128be44) --- .../purchase_receipt_trends/purchase_receipt_trends.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py index b62a6ee6fd8..9d313b477a3 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py @@ -20,6 +20,9 @@ def execute(filters=None): def get_chart_data(data, filters): + def wrap_in_quotes(label): + return f"'{label}'" + if not data: return [] @@ -36,6 +39,9 @@ def get_chart_data(data, filters): data = data[:10] for row in data: + if row[0] == wrap_in_quotes(_("Total")): + continue + labels.append(row[0]) datapoints.append(row[-1]) From 24f6f1e434eeaad548a1ae464ded6277d5e51d1c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 5 Nov 2025 15:09:50 +0530 Subject: [PATCH 11/17] fix: prevent reuse of serial no in manufacture and repack entry (cherry picked from commit 48b537dc8cf065637b28e07d8a1fbf990436de07) --- .../serial_and_batch_bundle.py | 24 ++++++++++++ .../doctype/stock_entry/test_stock_entry.py | 39 +++++++++++++++++++ erpnext/stock/serial_batch_bundle.py | 24 +++++++++--- 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 55facceae13..aa22e6b8bde 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -312,6 +312,30 @@ class SerialandBatchBundle(Document): SerialNoDuplicateError, ) + if ( + self.voucher_type == "Stock Entry" + and self.type_of_transaction == "Inward" + and frappe.get_cached_value("Stock Entry", self.voucher_no, "purpose") + in ["Manufacture", "Repack"] + ): + serial_nos = frappe.get_all( + "Serial No", filters={"name": ("in", serial_nos), "status": "Delivered"}, pluck="name" + ) + + if serial_nos: + if len(serial_nos) == 1: + frappe.throw( + _( + "Serial No {0} is already Delivered. You cannot use them again in Manufacture / Repack entry." + ).format(bold(serial_nos[0])) + ) + else: + frappe.throw( + _( + "Serial Nos {0} are already Delivered. You cannot use them again in Manufacture / Repack entry." + ).format(bold(", ".join(serial_nos))) + ) + def throw_error_message(self, message, exception=frappe.ValidationError): frappe.throw(_(message), exception, title=_("Error")) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 87e018d6683..e12e816db7d 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2088,6 +2088,45 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(incoming_rate, 125.0) + def test_prevent_reuse_delivered_serial_no_in_repack(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + item = "Test Prevent Reuse Delivered Serial No" + warehouse = "_Test Warehouse - _TC" + + item_doc = make_item(item, {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SHGJ.####"}) + + make_stock_entry(item_code="_Test Item", target=warehouse, qty=2, rate=100) + make_stock_entry(item_code=item, target=warehouse, qty=2, rate=100) + + dn = create_delivery_note(item_code=item, qty=2) + delivered_serial_no = get_serial_nos_from_bundle(dn.get("items")[0].serial_and_batch_bundle)[0] + + se = make_stock_entry( + item_code="_Test Item", source=warehouse, qty=1, purpose="Repack", do_not_save=True + ) + se.append( + "items", + { + "item_code": item_doc.name, + "item_name": item_doc.item_name, + "s_warehouse": None, + "t_warehouse": warehouse, + "description": item_doc.description, + "uom": item_doc.stock_uom, + "qty": 1, + "use_serial_batch_fields": 1, + "serial_no": delivered_serial_no, + }, + ) + + se.save() + status = frappe.db.get_value("Serial No", delivered_serial_no, "status") + + self.assertEqual(status, "Delivered") + self.assertEqual(se.purpose, "Repack") + self.assertRaises(frappe.ValidationError, se.submit) + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index dab6614b514..44d54141e09 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -406,12 +406,7 @@ class SerialBatchBundle: self.update_serial_no_status_warehouse(self.sle, serial_nos) - def update_serial_no_status_warehouse(self, sle, serial_nos): - warehouse = sle.warehouse if sle.actual_qty > 0 else None - - if isinstance(serial_nos, str): - serial_nos = [serial_nos] - + def get_status_for_serial_nos(self, sle): status = "Inactive" if sle.actual_qty < 0: status = "Delivered" @@ -425,6 +420,23 @@ class SerialBatchBundle: ]: status = "Consumed" + if sle.is_cancelled == 1 and ( + sle.voucher_type in ["Purchase Invoice", "Purchase Receipt"] or status == "Consumed" + ): + status = "Inactive" + + return status + + def update_serial_no_status_warehouse(self, sle, serial_nos): + warehouse = sle.warehouse if sle.actual_qty > 0 else None + + if isinstance(serial_nos, str): + serial_nos = [serial_nos] + + status = "Active" + if not warehouse: + status = self.get_status_for_serial_nos(sle) + customer = None if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0: customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer") From 1c40a61d236a6fe2bd4c3dc99714bb95ce7e58f1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 17 Dec 2025 15:26:36 +0530 Subject: [PATCH 12/17] fix: validate party's existing transaction currency before merging (cherry picked from commit f48b90c6009a0ad6331fd62e57f57a4dc8d6c73a) --- erpnext/accounts/party.py | 18 ++++++++++++++++++ erpnext/buying/doctype/supplier/supplier.py | 5 +++++ erpnext/selling/doctype/customer/customer.py | 10 +++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 38dc1e7502d..b8bcc3a4160 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -1056,3 +1056,21 @@ def add_party_account(party_type, party, company, account): def render_address(address, check_permissions=True): return frappe.call(_render_address, address, check_permissions=check_permissions) + + +def validate_party_currency_before_merging(party_type, old_party, new_party): + for company in frappe.get_all("Company"): + old_party_currency = get_party_gle_currency(party_type, old_party, company.name) + new_party_currency = get_party_gle_currency(party_type, new_party, company.name) + + if old_party_currency and new_party_currency and old_party_currency != new_party_currency: + frappe.throw( + _( + "Cannot merge {0} '{1}' into '{2}' as both have existing accounting entries in different currencies for company '{3}'." + ).format( + party_type, + old_party, + new_party, + company.name, + ) + ) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 07a2d31166b..f0e85523ea3 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -14,6 +14,7 @@ from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_ from erpnext.accounts.party import ( get_dashboard_info, validate_party_accounts, + validate_party_currency_before_merging, ) from erpnext.controllers.website_list_for_contact import add_role_for_portal_user from erpnext.utilities.transaction_base import TransactionBase @@ -208,6 +209,10 @@ class Supplier(TransactionBase): delete_contact_and_address("Supplier", self.name) + def before_rename(self, olddn, newdn, merge=False): + if merge: + validate_party_currency_before_merging("Supplier", olddn, newdn) + def after_rename(self, olddn, newdn, merge=False): if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name": self.db_set("supplier_name", newdn) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 449b56de3b4..1c1ae08b280 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -18,7 +18,11 @@ from frappe.utils import cint, cstr, flt, get_formatted_email, today from frappe.utils.deprecations import deprecated from frappe.utils.user import get_users_with_role -from erpnext.accounts.party import get_dashboard_info, validate_party_accounts +from erpnext.accounts.party import ( + get_dashboard_info, + validate_party_accounts, + validate_party_currency_before_merging, +) from erpnext.controllers.website_list_for_contact import add_role_for_portal_user from erpnext.utilities.transaction_base import TransactionBase @@ -367,6 +371,10 @@ class Customer(TransactionBase): if self.lead_name: frappe.db.sql("update `tabLead` set status='Interested' where name=%s", self.lead_name) + def before_rename(self, olddn, newdn, merge=False): + if merge: + validate_party_currency_before_merging("Customer", olddn, newdn) + def after_rename(self, olddn, newdn, merge=False): if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": self.db_set("customer_name", newdn) From 431e68741baded5b8d634b75cb8dc41d932d05f1 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Mon, 29 Dec 2025 12:04:13 +0530 Subject: [PATCH 13/17] fix(payment entry): clear party_name for internal transfer (cherry picked from commit aae0448e1f6846ff9bc8273f677d2872d7cd9d96) # Conflicts: # erpnext/accounts/doctype/payment_entry/payment_entry.js --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 4146b4aebb2..852fcc6807b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -435,6 +435,7 @@ frappe.ui.form.on("Payment Entry", { "paid_to", "references", "total_allocated_amount", + "party_name", ], function (i, field) { frm.set_value(field, null); From e6acdf36e2003d2110de29a4e0e5c7b0b10eb00a Mon Sep 17 00:00:00 2001 From: Ponnusamy Date: Tue, 30 Dec 2025 01:43:45 +0530 Subject: [PATCH 14/17] fix: start reposting accounting ledger after commit (cherry picked from commit 469a1ade79e060ab1282d0a878441087c36b882f) --- .../doctype/repost_accounting_ledger/repost_accounting_ledger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index e9e29d2cb31..0aeec6905eb 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -146,6 +146,7 @@ class RepostAccountingLedger(Document): account_repost_doc=self.name, is_async=True, job_name=job_name, + enqueue_after_commit=True, ) frappe.msgprint(_("Repost has started in the background")) else: From 6f3904a20a4fdd117b6d8a4802d622b5e9415d8f Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 30 Dec 2025 12:05:48 +0530 Subject: [PATCH 15/17] fix: expense_account query override in Purchase Receipt (cherry picked from commit 292a51c160fa46b3ec2984e6c7238c1c79fa208d) --- .../purchase_receipt/purchase_receipt.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index ce462f73039..b5c1c38729f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -17,13 +17,6 @@ frappe.ui.form.on("Purchase Receipt", { "Landed Cost Voucher": "Landed Cost Voucher", }; - frm.set_query("expense_account", "items", function () { - return { - query: "erpnext.controllers.queries.get_expense_account", - filters: { company: frm.doc.company }, - }; - }); - frm.set_query("wip_composite_asset", "items", function () { return { filters: { is_composite_asset: 1, docstatus: 0 }, @@ -171,6 +164,16 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend this.setup_accounting_dimension_triggers(); this.setup_posting_date_time_check(); super.setup(doc); + + this.frm.set_query("expense_account", "items", () => { + return { + query: "erpnext.controllers.queries.get_expense_account", + filters: { + company: this.frm.doc.company, + disabled: 0, + }, + }; + }); } refresh() { From cd930c05b8f27df65a2cbc0533fa7935d10406ba Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Fri, 26 Dec 2025 18:00:08 +0530 Subject: [PATCH 16/17] fix(bank reconciliation tool): carry bank account to payment entry (cherry picked from commit 6fc96366420303a3ea3b485d35ffca2aab1265a7) --- .../bank_reconciliation_tool.py | 2 ++ .../bank_reconciliation_tool/dialog_manager.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 3ce867dc96e..8ebbc1e7c0b 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -304,6 +304,7 @@ def create_payment_entry_bts( project=None, cost_center=None, allow_edit=None, + company_bank_account=None, ): # Create a new payment entry based on the bank transaction bank_transaction = frappe.db.get_values( @@ -344,6 +345,7 @@ def create_payment_entry_bts( pe.mode_of_payment = mode_of_payment pe.project = project pe.cost_center = cost_center + pe.bank_account = company_bank_account pe.validate() diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index bab25b6eca2..9879ede4ef3 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -361,6 +361,21 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { mandatory_depends_on: "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'", }, + { + fieldname: "bank_account", + fieldtype: "Link", + label: "Company Bank Account", + options: "Bank Account", + depends_on: "eval:doc.party", + get_query: function () { + return { + filters: { + is_company_account: 1, + company: this.company, + }, + }; + }, + }, { fieldname: "project", fieldtype: "Link", @@ -511,6 +526,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { mode_of_payment: values.mode_of_payment, project: values.project, cost_center: values.cost_center, + company_bank_account: values?.company_bank_account || this?.bank_account, }, callback: (response) => { const alert_string = __("Bank Transaction {0} added as Payment Entry", [ @@ -582,6 +598,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { project: values.project, cost_center: values.cost_center, allow_edit: true, + company_bank_account: values?.company_bank_account || this?.bank_account, }, callback: (r) => { const doc = frappe.model.sync(r.message); From 9ef0e8beb7c22085831fb91d60ec3c2edfdac6a7 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Fri, 26 Dec 2025 20:41:01 +0530 Subject: [PATCH 17/17] fix(bank reconciliation tool): fix incorrect bank account field mapping (cherry picked from commit 9dfb0fdcbb9d5242f5406d17a5560459b30092c8) --- .../bank_reconciliation_tool/bank_reconciliation_tool.py | 4 +++- erpnext/public/js/bank_reconciliation_tool/dialog_manager.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 8ebbc1e7c0b..9ea87ef0ae7 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -345,7 +345,9 @@ def create_payment_entry_bts( pe.mode_of_payment = mode_of_payment pe.project = project pe.cost_center = cost_center - pe.bank_account = company_bank_account + + if company_bank_account: + pe.bank_account = company_bank_account pe.validate() diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index 9879ede4ef3..16d4e9971d8 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -526,7 +526,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { mode_of_payment: values.mode_of_payment, project: values.project, cost_center: values.cost_center, - company_bank_account: values?.company_bank_account || this?.bank_account, + company_bank_account: values?.bank_account || this?.bank_account, }, callback: (response) => { const alert_string = __("Bank Transaction {0} added as Payment Entry", [ @@ -598,7 +598,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { project: values.project, cost_center: values.cost_center, allow_edit: true, - company_bank_account: values?.company_bank_account || this?.bank_account, + company_bank_account: values?.bank_account || this?.bank_account, }, callback: (r) => { const doc = frappe.model.sync(r.message);