From 1cb22f9d057612e4e592494e472c4d962236d828 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 25 Dec 2025 10:22:37 +0100 Subject: [PATCH 01/30] fix: don't duplicate default income account to Item (#50413) * fix: don't duplicate default income account to Item Only store _Default Income Account_ in **Item** if it's different from the **Company**'s _Default Income Account_. Resolves #48231 * refactor: move db call out of loop * docs: add docstring (cherry picked from commit b6cb9d47990554d332574cc52c02daea566fbada) --- erpnext/controllers/selling_controller.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index d74ea55450a..3667a7a7e76 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -1004,10 +1004,19 @@ class SellingController(StockController): def set_default_income_account_for_item(obj): - for d in obj.get("items"): - if d.item_code: - if getattr(d, "income_account", None): - set_item_default(d.item_code, obj.company, "income_account", d.income_account) + """Set income account as default for items in the transaction. + + Updates the item default income account for each item in the transaction + if it differs from the company's default income account. + + Args: + obj: Transaction document containing items table with income_account field + """ + company_default = frappe.get_cached_value("Company", obj.company, "default_income_account") + for d in obj.get("items", default=[]): + income_account = getattr(d, "income_account", None) + if d.item_code and income_account and income_account != company_default: + set_item_default(d.item_code, obj.company, "income_account", income_account) def get_serial_and_batch_bundle(child, parent, delivery_note_child=None): From 3cc41cf643aca0bfacdf6dc3b302432ddabea5b5 Mon Sep 17 00:00:00 2001 From: Nishka Gosalia Date: Tue, 6 Jan 2026 12:51:47 +0530 Subject: [PATCH 02/30] fix: correct uom reflecting in sales order when fetching from barcode --- erpnext/public/js/controllers/transaction.js | 6 ++++-- erpnext/public/js/utils/barcode_scanner.js | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1b23042769e..7abdbe75fd5 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -555,10 +555,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe var item = frappe.get_doc(cdt, cdn); var update_stock = 0, show_batch_dialog = 0; - item.weight_per_unit = 0; item.weight_uom = ''; - item.uom = null // make UOM blank to update the existing UOM when item changes + if(!item.barcode){ + item.uom = null // make UOM blank to update the existing UOM when item changes + } item.conversion_factor = 0; if(['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) { @@ -574,6 +575,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe show_batch_dialog = 0; } + item.barcode = null; diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index 822a9902a38..9f83f04b53f 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -404,6 +404,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { async set_barcode(row, barcode) { if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) { await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode); + } else { + row.barcode = barcode; } } From d3f2da0d594434e7d6a18654898a5a23a1e3fbf6 Mon Sep 17 00:00:00 2001 From: kavin-114 Date: Wed, 7 Jan 2026 01:00:29 +0530 Subject: [PATCH 03/30] fix: remove posting date & time on SRE batch validation --- erpnext/stock/stock_ledger.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index d00d092e795..0040aec77dc 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2337,8 +2337,6 @@ def validate_reserved_batch_nos(kwargs): { "item_code": kwargs.item_code, "warehouse": kwargs.warehouse, - "posting_date": kwargs.posting_date, - "posting_time": kwargs.posting_time, "ignore_voucher_nos": kwargs.ignore_voucher_nos, "ignore_reserved_stock": True, } From d17debabf70fd307cb334d3f18f2951f446e6a26 Mon Sep 17 00:00:00 2001 From: trustedcomputer Date: Mon, 5 Jan 2026 14:21:31 -0800 Subject: [PATCH 04/30] fix: change float types in payment entry reference table to currency (cherry picked from commit 8ba71300dbabdd96df7c3fd3344da07d26674797) # Conflicts: # erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json --- .../payment_entry_reference/payment_entry_reference.json | 8 ++++---- .../payment_entry_reference/payment_entry_reference.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 111de2ebd4e..e8411fffb02 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -68,7 +68,7 @@ { "columns": 2, "fieldname": "total_amount", - "fieldtype": "Float", + "fieldtype": "Currency", "in_list_view": 1, "label": "Grand Total", "print_hide": 1, @@ -77,7 +77,7 @@ { "columns": 2, "fieldname": "outstanding_amount", - "fieldtype": "Float", + "fieldtype": "Currency", "in_list_view": 1, "label": "Outstanding", "read_only": 1 @@ -85,7 +85,7 @@ { "columns": 2, "fieldname": "allocated_amount", - "fieldtype": "Float", + "fieldtype": "Currency", "in_list_view": 1, "label": "Allocated" }, @@ -174,7 +174,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-07-25 04:32:11.040025", + "modified": "2026-01-05 14:18:03.286224", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py index a5e0b21a9af..0091d792f33 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -18,12 +18,12 @@ class PaymentEntryReference(Document): account_type: DF.Data | None advance_voucher_no: DF.DynamicLink | None advance_voucher_type: DF.Link | None - allocated_amount: DF.Float + allocated_amount: DF.Currency bill_no: DF.Data | None due_date: DF.Date | None exchange_gain_loss: DF.Currency exchange_rate: DF.Float - outstanding_amount: DF.Float + outstanding_amount: DF.Currency parent: DF.Data parentfield: DF.Data parenttype: DF.Data @@ -34,7 +34,7 @@ class PaymentEntryReference(Document): reconcile_effect_on: DF.Date | None reference_doctype: DF.Link reference_name: DF.DynamicLink - total_amount: DF.Float + total_amount: DF.Currency # end: auto-generated types @property From 1179514118a07465867e06ed336f7e885eab6c66 Mon Sep 17 00:00:00 2001 From: Logesh Periyasamy Date: Thu, 8 Jan 2026 12:05:36 +0530 Subject: [PATCH 05/30] fix(accounting-dimension): System-generated round-off GL entries fail to set the accounting dimension (#51167) * chore: remove disabled condition statement * fix: add default dimension for round off gle * fix: validate report type to handle opening entries roundoff (cherry picked from commit bc63c85dafff1e927526e9e3325e4738bfd910b7) --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 2 -- .../payment_ledger_entry/payment_ledger_entry.py | 2 -- erpnext/accounts/general_ledger.py | 13 +++++++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 965f90a1266..cbf8745390e 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -187,7 +187,6 @@ class GLEntry(Document): account_type == "Profit and Loss" and self.company == dimension.company and dimension.mandatory_for_pl - and not dimension.disabled and not self.is_cancelled ): if not self.get(dimension.fieldname): @@ -201,7 +200,6 @@ class GLEntry(Document): account_type == "Balance Sheet" and self.company == dimension.company and dimension.mandatory_for_bs - and not dimension.disabled and not self.is_cancelled ): if not self.get(dimension.fieldname): diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py index 2bc44893c20..c7cc97d7197 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py @@ -133,7 +133,6 @@ class PaymentLedgerEntry(Document): account_type == "Profit and Loss" and self.company == dimension.company and dimension.mandatory_for_pl - and not dimension.disabled ): if not self.get(dimension.fieldname): frappe.throw( @@ -146,7 +145,6 @@ class PaymentLedgerEntry(Document): account_type == "Balance Sheet" and self.company == dimension.company and dimension.mandatory_for_bs - and not dimension.disabled ): if not self.get(dimension.fieldname): frappe.throw( diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 13201f4bcbc..60b3efd3cbc 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -12,6 +12,7 @@ from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, + get_checks_for_pl_and_bs_accounts, ) from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( get_dimension_filter_map, @@ -612,6 +613,18 @@ def update_accounting_dimensions(round_off_gle): for dimension in dimensions: round_off_gle[dimension] = dimension_values.get(dimension) + else: + report_type = frappe.get_cached_value("Account", round_off_gle.account, "report_type") + for dimension in get_checks_for_pl_and_bs_accounts(): + if ( + round_off_gle.company == dimension.company + and ( + (report_type == "Profit and Loss" and dimension.mandatory_for_pl) + or (report_type == "Balance Sheet" and dimension.mandatory_for_bs) + ) + and dimension.default_dimension + ): + round_off_gle[dimension.fieldname] = dimension.default_dimension def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False): From 8f1509dca18a949c415da23f5db908a62cd2a80d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 8 Jan 2026 13:14:34 +0530 Subject: [PATCH 06/30] fix: allow all users of supplier to create purchase invoices (cherry picked from commit 190204a939741d3c7a20d8967669400ec47d559f) --- erpnext/buying/doctype/purchase_order/purchase_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 5cbba410f86..751254babf1 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -794,7 +794,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None): @frappe.whitelist() def make_purchase_invoice_from_portal(purchase_order_name): doc = get_mapped_purchase_invoice(purchase_order_name, ignore_permissions=True) - if doc.contact_email != frappe.session.user: + if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"): frappe.throw(_("Not Permitted"), frappe.PermissionError) doc.save() frappe.db.commit() From 7db6ae8bda7a6ad88206083f9139028824e3e4e0 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 8 Jan 2026 13:31:50 +0530 Subject: [PATCH 07/30] fix: closed WO becomes open when RM is returned (cherry picked from commit d0ba365aaab2624d29e3a6e55f1f0eefae1f39fb) --- .../manufacturing/doctype/work_order/work_order.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 20f8a4ab553..41716b015a3 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -351,15 +351,16 @@ class WorkOrder(Document): def update_status(self, status=None): """Update status of work order if unknown""" - if status != "Stopped" and status != "Closed": - status = self.get_status(status) + if self.status != "Closed": + if status not in ["Stopped", "Closed"]: + status = self.get_status(status) - if status != self.status: - self.db_set("status", status) + if status != self.status: + self.db_set("status", status) - self.update_required_items() + self.update_required_items() - return status + return status or self.status def get_status(self, status=None): """Return the status based on stock entries against this work order""" From 1bbeecff12ee22efccf0c1e8583d52aecd78bbb7 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 8 Jan 2026 13:39:03 +0530 Subject: [PATCH 08/30] fix: negative stock issue for higher precision (cherry picked from commit 87be020c783f99f21dbbdcb653f538b97431ebdd) # Conflicts: # erpnext/stock/doctype/delivery_note/test_delivery_note.py --- .../delivery_note/test_delivery_note.py | 19 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 6 +++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 5e28362c509..933b39989fc 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2721,6 +2721,7 @@ class TestDeliveryNote(FrappeTestCase): serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no], ) +<<<<<<< HEAD @change_settings("Stock Settings", {"allow_negative_stock": 0, "enable_stock_reservation": 1}) def test_partial_delivery_note_against_reserved_stock(self): from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( @@ -2789,6 +2790,24 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(sre_details[0].status, "Partially Delivered") self.assertEqual(sre_details[0].reserved_qty, so.items[0].qty) self.assertEqual(sre_details[0].delivered_qty, dn.items[0].qty) +======= + def test_negative_stock_with_higher_precision(self): + original_flt_precision = frappe.db.get_default("float_precision") + frappe.db.set_single_value("System Settings", "float_precision", 7) + + item_code = make_item( + "Test Negative Stock High Precision Item", properties={"is_stock_item": 1, "valuation_rate": 1} + ).name + dn = create_delivery_note( + item_code=item_code, + qty=0.0000010, + do_not_submit=True, + ) + + self.assertRaises(frappe.ValidationError, dn.submit) + + frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision) +>>>>>>> 87be020c78 (fix: negative stock issue for higher precision) def create_delivery_note(**args): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 0040aec77dc..55e30258044 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1170,7 +1170,11 @@ class update_entries_after: diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock) diff = flt(diff, self.flt_precision) # respect system precision - if diff < 0 and abs(diff) > 0.0001: + diff_threshold = 0.0001 + if self.flt_precision > 4: + diff_threshold = 10 ** (-1 * self.flt_precision) + + if diff < 0 and abs(diff) > diff_threshold: # negative stock! exc = sle.copy().update({"diff": diff}) self.exceptions.setdefault(sle.warehouse, []).append(exc) From 5193dbba9b630daf53fdefe92d4a5dac783dda91 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 8 Jan 2026 14:45:39 +0530 Subject: [PATCH 09/30] chore: fix conflicts Refactor test cases for delivery notes to handle negative stock and higher precision. --- erpnext/stock/doctype/delivery_note/test_delivery_note.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 933b39989fc..29eabe2670e 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2721,7 +2721,6 @@ class TestDeliveryNote(FrappeTestCase): serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no], ) -<<<<<<< HEAD @change_settings("Stock Settings", {"allow_negative_stock": 0, "enable_stock_reservation": 1}) def test_partial_delivery_note_against_reserved_stock(self): from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( @@ -2790,7 +2789,7 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(sre_details[0].status, "Partially Delivered") self.assertEqual(sre_details[0].reserved_qty, so.items[0].qty) self.assertEqual(sre_details[0].delivered_qty, dn.items[0].qty) -======= + def test_negative_stock_with_higher_precision(self): original_flt_precision = frappe.db.get_default("float_precision") frappe.db.set_single_value("System Settings", "float_precision", 7) @@ -2807,7 +2806,6 @@ class TestDeliveryNote(FrappeTestCase): self.assertRaises(frappe.ValidationError, dn.submit) frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision) ->>>>>>> 87be020c78 (fix: negative stock issue for higher precision) def create_delivery_note(**args): From 9d5a493609ed6c4e2741b393b2bdede64b1d06f6 Mon Sep 17 00:00:00 2001 From: Pandiyan5273 Date: Wed, 7 Jan 2026 19:27:16 +0530 Subject: [PATCH 10/30] fix(stock): enable allow on submit for tracking status field (cherry picked from commit 1bfb62465f847afcbe902ca334756502a3018050) --- erpnext/stock/doctype/shipment/shipment.json | 3 ++- erpnext/stock/doctype/shipment/shipment.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json index 8b6e70ac65b..91648a2060a 100644 --- a/erpnext/stock/doctype/shipment/shipment.json +++ b/erpnext/stock/doctype/shipment/shipment.json @@ -382,6 +382,7 @@ "print_hide": 1 }, { + "allow_on_submit": 1, "fieldname": "tracking_status", "fieldtype": "Select", "label": "Tracking Status", @@ -440,7 +441,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2025-02-20 16:55:20.076418", + "modified": "2026-01-07 19:24:23.566312", "modified_by": "Administrator", "module": "Stock", "name": "Shipment", diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index cf9d165fdd3..880f6b5e1c1 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -20,9 +20,7 @@ class Shipment(Document): if TYPE_CHECKING: from frappe.types import DF - from erpnext.stock.doctype.shipment_delivery_note.shipment_delivery_note import ( - ShipmentDeliveryNote, - ) + from erpnext.stock.doctype.shipment_delivery_note.shipment_delivery_note import ShipmentDeliveryNote from erpnext.stock.doctype.shipment_parcel.shipment_parcel import ShipmentParcel amended_from: DF.Link | None From 4c53af049464b7458a971e3f1231359255f9aaf4 Mon Sep 17 00:00:00 2001 From: Pandiyan5273 Date: Tue, 6 Jan 2026 12:58:54 +0530 Subject: [PATCH 11/30] fix(accounts): correct sales order item deletion message for MR and PO linkage (cherry picked from commit 5a47503611e9fd63c7e6b5a317f7b1a23aa06dad) --- erpnext/controllers/accounts_controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1f6b6ca17c9..172c63bebf6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3714,9 +3714,9 @@ def validate_child_on_delete(row, parent): ) if flt(row.ordered_qty): frappe.throw( - _("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format( - row.idx, row.item_code - ) + _( + "Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order." + ).format(row.idx, row.item_code) ) if parent.doctype == "Purchase Order" and flt(row.received_qty): From d83365734e988d241ce03ffa14f4ac1ecf627e1d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:48:28 +0000 Subject: [PATCH 12/30] Merge pull request #51624 from frappe/mergify/bp/version-15-hotfix/pr-50869 fix: do cancellation procedures on WO close (backport #50869) --- erpnext/manufacturing/doctype/work_order/work_order.py | 9 ++++++--- erpnext/selling/doctype/sales_order/sales_order.py | 1 + .../stock/doctype/material_request/material_request.py | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 41716b015a3..b904d2f13f4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -316,7 +316,7 @@ class WorkOrder(Document): # already ordered qty ordered_qty_against_so = frappe.db.sql( """select sum(qty) from `tabWork Order` - where production_item = %s and sales_order = %s and docstatus < 2 and name != %s""", + where production_item = %s and sales_order = %s and docstatus < 2 and status != 'Closed' and name != %s""", (self.production_item, self.sales_order, self.name), )[0][0] @@ -516,6 +516,9 @@ class WorkOrder(Document): self.validate_cancel() self.db_set("status", "Cancelled") + self.on_close_or_cancel() + + def on_close_or_cancel(self): if self.production_plan and frappe.db.exists( "Production Plan Item Reference", {"parent": self.production_plan} ): @@ -843,7 +846,7 @@ class WorkOrder(Document): qty = frappe.db.sql( f""" select sum(qty) from - `tabWork Order` where sales_order = %s and docstatus = 1 and {cond} + `tabWork Order` where sales_order = %s and docstatus = 1 and status <> 'Closed' and {cond} """, (self.sales_order, (self.product_bundle_item or self.production_item)), as_list=1, @@ -1604,8 +1607,8 @@ def close_work_order(work_order, status): ) ) + work_order.on_close_or_cancel() work_order.update_status(status) - work_order.update_planned_qty() frappe.msgprint(_("Work Order has been {0}").format(status)) work_order.notify_update() return work_order.status diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 8be426fa1a5..0079db289a5 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1875,6 +1875,7 @@ def get_work_order_items(sales_order, for_raw_material_request=0): & (wo.sales_order == so.name) & (wo.sales_order_item == i.name) & (wo.docstatus.lt(2)) + & (wo.status != "Closed") ) .run()[0][0] ) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index e9c7a813698..7d160b24e2d 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -273,6 +273,9 @@ class MaterialRequest(BuyingController): .groupby(doctype.material_request_item) ) + if self.material_request_type == "Manufacture": + query = query.where(doctype.status != "Closed") + mr_items_ordered_qty = frappe._dict(query.run()) return mr_items_ordered_qty From ee9debe581432dbaec3c83e780a6e61c0360e89a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 28 Dec 2025 10:36:43 +0530 Subject: [PATCH 13/30] perf: SABB taking time to save the record (cherry picked from commit 20320c4a6c161d9d2c63fa71199dad4d074a1515) # Conflicts: # erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py # erpnext/stock/serial_batch_bundle.py --- .../doctype/pos_invoice/test_pos_invoice.py | 3 + .../tests/test_subcontracting_controller.py | 3 +- .../serial_and_batch_bundle.py | 45 ++++++- .../test_serial_and_batch_bundle.py | 1 + erpnext/stock/serial_batch_bundle.py | 110 +++++++++++++++--- 5 files changed, 142 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index b0c16ac27d1..97b3e87770f 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -481,6 +481,7 @@ class TestPOSInvoice(unittest.TestCase): rate=1000, serial_no=[serial_nos[0]], do_not_save=1, + ignore_sabb_validation=True, ) pos2.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000}) @@ -956,6 +957,7 @@ class TestPOSInvoice(unittest.TestCase): qty=1, rate=100, do_not_submit=True, + ignore_sabb_validation=True, ) self.assertRaises(frappe.ValidationError, pos_inv.submit) @@ -1097,6 +1099,7 @@ def create_pos_invoice(**args): "posting_time": pos_inv.posting_time, "type_of_transaction": type_of_transaction, "do_not_submit": True, + "ignore_sabb_validation": args.ignore_sabb_validation, } ) ).name diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index dc80a23198a..106b1cd0c42 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -778,9 +778,8 @@ class TestSubcontractingController(FrappeTestCase): row.serial_no = "ABC" break - bundle.save() + self.assertRaises(frappe.ValidationError, bundle.save) - self.assertRaises(frappe.ValidationError, scr1.save) bundle.load_from_db() for row in bundle.entries: if row.idx == 1: 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 ffd0a0a137d..fb8430b7437 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 @@ -116,10 +116,24 @@ class SerialandBatchBundle(Document): return self.allow_existing_serial_nos() +<<<<<<< HEAD if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test: self.validate_serial_nos_duplicate() +======= + if self.docstatus == 1: + if not self.flags.ignore_validate_serial_batch or frappe.in_test: + self.validate_serial_nos_duplicate() + + self.check_future_entries_exists() + elif ( + self.has_serial_no + and self.type_of_transaction == "Outward" + and self.voucher_type != "Stock Reconciliation" + and self.voucher_no + ): + self.validate_serial_no_status() +>>>>>>> 20320c4a6c (perf: SABB taking time to save the record) - self.check_future_entries_exists() self.set_is_outward() self.calculate_total_qty() self.set_warehouse() @@ -129,6 +143,25 @@ class SerialandBatchBundle(Document): self.calculate_qty_and_amount() + def validate_serial_no_status(self): + serial_nos = [d.serial_no for d in self.entries if d.serial_no] + invalid_serial_nos = frappe.get_all( + "Serial No", + filters={ + "name": ("in", serial_nos), + "warehouse": ("!=", self.warehouse), + }, + pluck="name", + ) + + if invalid_serial_nos: + msg = _( + "You cannot outward following {0} as either they are Delivered, Inactive or located in a different warehouse." + ).format(_("Serial Nos") if len(invalid_serial_nos) > 1 else _("Serial No")) + msg += "
" + msg += ", ".join(sn for sn in invalid_serial_nos) + frappe.throw(msg) + def validate_voucher_detail_no(self): if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [ "Installation Note", @@ -702,10 +735,16 @@ class SerialandBatchBundle(Document): "Buying Settings", "set_valuation_rate_for_rejected_materials" ) + precision = frappe.get_precision("Serial and Batch Entry", "incoming_rate") for d in self.entries: if self.is_rejected and not set_valuation_rate_for_rejected_materials: rate = 0.0 - elif (d.incoming_rate == rate) and not stock_queue and d.qty and d.stock_value_difference: + elif ( + (flt(d.incoming_rate, precision) == flt(rate, precision)) + and not stock_queue + and d.qty + and d.stock_value_difference + ): continue if is_packed_item and d.incoming_rate: @@ -766,7 +805,7 @@ class SerialandBatchBundle(Document): self.calculate_total_qty(save=True) # If user has changed the rate in the child table - if self.docstatus == 0: + if self.docstatus == 0 and self.type_of_transaction == "Inward": self.set_incoming_rate(parent=parent, row=row, save=True) if self.docstatus == 0 and parent.get("is_return") and parent.is_new(): diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index eec91b2c282..64563625297 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -982,6 +982,7 @@ def make_serial_batch_bundle(kwargs): "type_of_transaction": type_of_transaction, "company": kwargs.company or "_Test Company", "do_not_submit": kwargs.do_not_submit, + "ignore_sabb_validation": kwargs.ignore_sabb_validation or False, } ) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 44d54141e09..df29ec03b73 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -4,8 +4,13 @@ import frappe from frappe import _, bold from frappe.model.naming import NamingSeries, make_autoname, parse_naming_series from frappe.query_builder import Case +<<<<<<< HEAD from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today +======= +from frappe.query_builder.functions import Max, Sum +from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, now, nowtime, today +>>>>>>> 20320c4a6c (perf: SABB taking time to save the record) from pypika import Order from pypika.terms import ExistsCriterion @@ -616,8 +621,9 @@ class SerialNoValuation(DeprecatedSerialNoValuation): self.old_serial_nos = [] serial_nos = self.get_serial_nos() + result = self.get_serial_no_wise_incoming_rate(serial_nos) for serial_no in serial_nos: - incoming_rate = self.get_incoming_rate_from_bundle(serial_no) + incoming_rate = result.get(serial_no) if incoming_rate is None: self.old_serial_nos.append(serial_no) continue @@ -627,32 +633,100 @@ class SerialNoValuation(DeprecatedSerialNoValuation): self.calculate_stock_value_from_deprecarated_ledgers() - def get_incoming_rate_from_bundle(self, serial_no) -> float: + def get_serial_no_wise_incoming_rate(self, serial_nos): bundle = frappe.qb.DocType("Serial and Batch Bundle") bundle_child = frappe.qb.DocType("Serial and Batch Entry") + def get_latest_based_on_posting_datetime(): + # Get latest inward record based on posting datetime for each serial no + + latest_posting = ( + frappe.qb.from_(bundle) + .inner_join(bundle_child) + .on(bundle.name == bundle_child.parent) + .select( + bundle_child.serial_no, + Max(bundle.posting_datetime).as_("max_posting_dt"), + ) + .where( + (bundle.is_cancelled == 0) + & (bundle.docstatus == 1) + & (bundle.type_of_transaction == "Inward") + & (bundle_child.qty > 0) + & (bundle.item_code == self.sle.item_code) + & (bundle_child.warehouse == self.sle.warehouse) + & (bundle_child.serial_no.isin(serial_nos)) + ) + .groupby(bundle_child.serial_no) + ) + + # Important to exclude the current voucher to calculate correct the stock value difference + if self.sle.voucher_no: + latest_posting = latest_posting.where(bundle.voucher_no != self.sle.voucher_no) + + if self.sle.posting_datetime: + timestamp_condition = bundle.posting_datetime <= self.sle.posting_datetime + + latest_posting = latest_posting.where(timestamp_condition) + + latest_posting = latest_posting.as_("latest_posting") + + return latest_posting + + def get_latest_based_on_creation(latest_posting): + # Get latest inward record based on creation for each serial no + latest_creation = ( + frappe.qb.from_(bundle) + .join(bundle_child) + .on(bundle.name == bundle_child.parent) + .join(latest_posting) + .on( + (latest_posting.serial_no == bundle_child.serial_no) + & (latest_posting.max_posting_dt == bundle.posting_datetime) + ) + .select( + bundle_child.serial_no, + Max(bundle.creation).as_("max_creation"), + ) + .where( + (bundle.is_cancelled == 0) + & (bundle.docstatus == 1) + & (bundle.type_of_transaction == "Inward") + & (bundle_child.qty > 0) + & (bundle.item_code == self.sle.item_code) + & (bundle_child.warehouse == self.sle.warehouse) + ) + .groupby(bundle_child.serial_no) + ).as_("latest_creation") + + return latest_creation + + latest_posting = get_latest_based_on_posting_datetime() + latest_creation = get_latest_based_on_creation(latest_posting) + query = ( frappe.qb.from_(bundle) - .inner_join(bundle_child) + .join(bundle_child) .on(bundle.name == bundle_child.parent) - .select((bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate")) - .where( - (bundle.is_cancelled == 0) - & (bundle.docstatus == 1) - & (bundle_child.serial_no == serial_no) - & (bundle.type_of_transaction == "Inward") - & (bundle_child.qty > 0) - & (bundle.item_code == self.sle.item_code) - & (bundle_child.warehouse == self.sle.warehouse) + .join(latest_creation) + .on( + (latest_creation.serial_no == bundle_child.serial_no) + & (latest_creation.max_creation == bundle.creation) ) + .select( + bundle_child.serial_no, + (bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"), + ) +<<<<<<< HEAD .orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc) .limit(1) +======= +>>>>>>> 20320c4a6c (perf: SABB taking time to save the record) ) - # Important to exclude the current voucher to calculate correct the stock value difference - if self.sle.voucher_no: - query = query.where(bundle.voucher_no != self.sle.voucher_no) + result = query.run(as_list=1) +<<<<<<< HEAD if self.sle.posting_date: if self.sle.posting_time is None: self.sle.posting_time = nowtime() @@ -665,6 +739,9 @@ class SerialNoValuation(DeprecatedSerialNoValuation): incoming_rate = query.run() return flt(incoming_rate[0][0]) if incoming_rate else None +======= + return frappe._dict(result) if result else frappe._dict({}) +>>>>>>> 20320c4a6c (perf: SABB taking time to save the record) def get_serial_nos(self): if self.sle.get("serial_nos"): @@ -1131,6 +1208,9 @@ class SerialBatchCreation: doc.submit() else: + if self.get("ignore_sabb_validation"): + doc.flags.ignore_validate = True + doc.save() self.validate_qty(doc) From 01af6c876262bf03077503ad7c12e23b9def9fca Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 9 Jan 2026 17:29:09 +0530 Subject: [PATCH 14/30] fix: incoming rate calculation (cherry picked from commit 8e143d68b4bc769836305256902511c7b89a3fd0) --- 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 df29ec03b73..e7ad3c5de5b 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -715,7 +715,7 @@ class SerialNoValuation(DeprecatedSerialNoValuation): ) .select( bundle_child.serial_no, - (bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"), + bundle_child.incoming_rate, ) <<<<<<< HEAD .orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc) From aa43715de69a7df75d2e9574f82a4fea31663c6e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 9 Jan 2026 18:03:28 +0530 Subject: [PATCH 15/30] chore: fix conflicts --- .../serial_and_batch_bundle.py | 8 +--- erpnext/stock/serial_batch_bundle.py | 43 ++++++------------- 2 files changed, 15 insertions(+), 36 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 fb8430b7437..ad88b571a91 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 @@ -116,15 +116,12 @@ class SerialandBatchBundle(Document): return self.allow_existing_serial_nos() -<<<<<<< HEAD - if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test: - self.validate_serial_nos_duplicate() -======= if self.docstatus == 1: - if not self.flags.ignore_validate_serial_batch or frappe.in_test: + if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test: self.validate_serial_nos_duplicate() self.check_future_entries_exists() + elif ( self.has_serial_no and self.type_of_transaction == "Outward" @@ -132,7 +129,6 @@ class SerialandBatchBundle(Document): and self.voucher_no ): self.validate_serial_no_status() ->>>>>>> 20320c4a6c (perf: SABB taking time to save the record) self.set_is_outward() self.calculate_total_qty() diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index e7ad3c5de5b..f2639998d3e 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -4,13 +4,8 @@ import frappe from frappe import _, bold from frappe.model.naming import NamingSeries, make_autoname, parse_naming_series from frappe.query_builder import Case -<<<<<<< HEAD -from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp +from frappe.query_builder.functions import CombineDatetime, Max, Sum, Timestamp from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today -======= -from frappe.query_builder.functions import Max, Sum -from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, now, nowtime, today ->>>>>>> 20320c4a6c (perf: SABB taking time to save the record) from pypika import Order from pypika.terms import ExistsCriterion @@ -646,7 +641,7 @@ class SerialNoValuation(DeprecatedSerialNoValuation): .on(bundle.name == bundle_child.parent) .select( bundle_child.serial_no, - Max(bundle.posting_datetime).as_("max_posting_dt"), + Max(CombineDatetime(bundle.posting_date, bundle.posting_time)).as_("max_posting_dt"), ) .where( (bundle.is_cancelled == 0) @@ -664,8 +659,13 @@ class SerialNoValuation(DeprecatedSerialNoValuation): if self.sle.voucher_no: latest_posting = latest_posting.where(bundle.voucher_no != self.sle.voucher_no) - if self.sle.posting_datetime: - timestamp_condition = bundle.posting_datetime <= self.sle.posting_datetime + if self.sle.posting_date: + if self.sle.posting_time is None: + self.sle.posting_time = nowtime() + + timestamp_condition = CombineDatetime( + bundle.posting_date, bundle.posting_time + ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time) latest_posting = latest_posting.where(timestamp_condition) @@ -682,7 +682,10 @@ class SerialNoValuation(DeprecatedSerialNoValuation): .join(latest_posting) .on( (latest_posting.serial_no == bundle_child.serial_no) - & (latest_posting.max_posting_dt == bundle.posting_datetime) + & ( + latest_posting.max_posting_dt + == CombineDatetime(bundle.posting_date, bundle.posting_time) + ) ) .select( bundle_child.serial_no, @@ -717,31 +720,11 @@ class SerialNoValuation(DeprecatedSerialNoValuation): bundle_child.serial_no, bundle_child.incoming_rate, ) -<<<<<<< HEAD - .orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc) - .limit(1) -======= ->>>>>>> 20320c4a6c (perf: SABB taking time to save the record) ) result = query.run(as_list=1) -<<<<<<< HEAD - if self.sle.posting_date: - if self.sle.posting_time is None: - self.sle.posting_time = nowtime() - - timestamp_condition = CombineDatetime( - bundle.posting_date, bundle.posting_time - ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time) - - query = query.where(timestamp_condition) - - incoming_rate = query.run() - return flt(incoming_rate[0][0]) if incoming_rate else None -======= return frappe._dict(result) if result else frappe._dict({}) ->>>>>>> 20320c4a6c (perf: SABB taking time to save the record) def get_serial_nos(self): if self.sle.get("serial_nos"): From f9be364bd1fca6bfb5af362aa17b9a5e9a9c6ae9 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 10 Jan 2026 17:27:39 +0530 Subject: [PATCH 16/30] fix: pick list qty does not reset when pick list is cancelled (cherry picked from commit 1d6d9c204005878a1013b45fb371b69234d40544) --- erpnext/stock/doctype/pick_list/pick_list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index a4a335d2c5c..f82c53bd14f 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -383,7 +383,7 @@ class PickList(TransactionBase): picked_items = get_picked_items_qty(packed_items, contains_packed_items=True) self.validate_picked_qty(picked_items) - doc_updates = {} + doc_updates = {item: {"picked_qty": 0} for item in set(packed_items)} for d in picked_items: doc_updates[d.product_bundle_item] = {"picked_qty": flt(d.picked_qty)} @@ -394,7 +394,7 @@ class PickList(TransactionBase): picked_items = get_picked_items_qty(so_items) self.validate_picked_qty(picked_items) - doc_updates = {} + doc_updates = {item: {"picked_qty": 0} for item in set(so_items)} for d in picked_items: doc_updates[d.sales_order_item] = {"picked_qty": flt(d.picked_qty)} From d8232c4503cb3e6e97f633a074b65f7623a4ff48 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 10 Jan 2026 22:22:10 +0530 Subject: [PATCH 17/30] chore: v16 release announcement for v15 users --- erpnext/change_log/v15/v15_94_2.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 erpnext/change_log/v15/v15_94_2.md diff --git a/erpnext/change_log/v15/v15_94_2.md b/erpnext/change_log/v15/v15_94_2.md new file mode 100644 index 00000000000..b237422ee77 --- /dev/null +++ b/erpnext/change_log/v15/v15_94_2.md @@ -0,0 +1,9 @@ +# Version 16 Released! + +ERPNext version 16 has been released! + +Since it's the latest version of ERPNext, we recommend you to update to it to get the latest features, bug fixes and other improvements. + +[Click here to know more about v16](https://frappe.io/) + +If you're on [Frappe Cloud](https://frappe.io/cloud), [click here to know how to update to v16](https://frappe.io/) \ No newline at end of file From 159d1d61b5bc58e8c180e1e48590c1841adcdf94 Mon Sep 17 00:00:00 2001 From: NaviN <118178330+Navin-S-R@users.noreply.github.com> Date: Sun, 11 Jan 2026 18:57:39 +0530 Subject: [PATCH 18/30] fix(payment reconciliation): handle adhoc payment returns (#51311) * fix(payment reconciliation): handle reverse payments * test: validate payment return gain or loss * chore: typo (cherry picked from commit cecd07bbf43451415fb971ad13ab3b305d39aa87) --- .../payment_reconciliation.py | 42 +++- .../test_payment_reconciliation.py | 204 ++++++++++++++++++ 2 files changed, 241 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index b73100c78f2..c81863f044f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -6,7 +6,7 @@ import frappe from frappe import _, msgprint, qb from frappe.model.document import Document from frappe.model.meta import get_field_precision -from frappe.query_builder import Criterion +from frappe.query_builder import Case, Criterion from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today @@ -393,6 +393,9 @@ class PaymentReconciliation(Document): inv.outstanding_amount = flt(entry.get("outstanding_amount")) def get_difference_amount(self, payment_entry, invoice, allocated_amount): + party_account_defaults = frappe.get_cached_value( + "Account", self.receivable_payable_account, ["account_type", "account_currency"], as_dict=True + ) allocated_amount_precision = get_field_precision( frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount") ) @@ -400,9 +403,9 @@ class PaymentReconciliation(Document): frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount") ) difference_amount = 0 - if frappe.get_cached_value( - "Account", self.receivable_payable_account, "account_currency" - ) != frappe.get_cached_value("Company", self.company, "default_currency"): + if party_account_defaults.get("account_currency") != frappe.get_cached_value( + "Company", self.company, "default_currency" + ): if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get( "exchange_rate", 1 ): @@ -414,7 +417,14 @@ class PaymentReconciliation(Document): invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision), difference_amount_precision, ) - difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate + + # Added If clause to handle return Adhoc payments for account type holders ("Payable") + if party_account_defaults.get("account_type") in ("Payable") and invoice.get( + "invoice_type" + ) in ["Payment Entry", "Journal Entry"]: + difference_amount = allocated_amount_in_inv_rate - allocated_amount_in_ref_rate + else: + difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate return difference_amount @@ -677,6 +687,28 @@ class PaymentReconciliation(Document): ) invoice_exchange_map.update(journals_map) + payment_entries = [ + d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Payment Entry" + ] + payment_entries.extend( + [d.get("reference_name") for d in payments if d.get("reference_type") == "Payment Entry"] + ) + if payment_entries: + pe = frappe.qb.DocType("Payment Entry") + query = ( + frappe.qb.from_(pe) + .select( + pe.name, + Case() + .when(pe.payment_type == "Receive", pe.source_exchange_rate) + .else_(pe.target_exchange_rate) + .as_("exchange_rate"), + ) + .where(pe.name.isin(payment_entries)) + ) + payment_entries = query.run(as_list=1) + invoice_exchange_map.update(payment_entries) + return invoice_exchange_map def validate_allocation(self): diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 679eb90f19a..59c385855fa 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -2336,6 +2336,210 @@ class TestPaymentReconciliation(FrappeTestCase): frappe.db.set_value("Company", self.company, default_settings) + def test_foreign_currency_reverse_payment_entry_against_payment_entry_for_customer(self): + transaction_date = nowdate() + customer = self.customer3 + amount = 1000 + exchange_rate_at_payment = 100 + exchange_rate_at_reverse_payment = 95 + + # Receive amount from customer - 1,00,000 + pe = self.create_payment_entry(amount=amount, posting_date=transaction_date, customer=customer) + pe.payment_type = "Receive" + pe.paid_from = self.debtors_eur + pe.paid_from_account_currency = "EUR" + pe.source_exchange_rate = exchange_rate_at_payment + pe.paid_amount = amount + pe.received_amount = exchange_rate_at_payment * amount + pe.paid_to = self.cash + pe.paid_to_account_currency = "INR" + pe = pe.save().submit() + + # Pay amount to customer - 95,000 + reverse_pe = self.create_payment_entry( + amount=amount, posting_date=transaction_date, customer=customer + ) + reverse_pe.payment_type = "Pay" + reverse_pe.paid_from = self.cash + reverse_pe.paid_from_account_currency = "INR" + reverse_pe.target_exchange_rate = exchange_rate_at_reverse_payment + reverse_pe.paid_amount = exchange_rate_at_reverse_payment * amount + reverse_pe.received_amount = amount + reverse_pe.paid_to = self.debtors_eur + reverse_pe.paid_to_account_currency = "EUR" + reverse_pe.save().submit() + + # Reconcile payments + pr = self.create_payment_reconciliation() + pr.party = customer + pr.receivable_payable_account = self.debtors_eur + pr.get_unreconciled_entries() + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + self.assertEqual(len(pr.get("invoices")), 1) + self.assertEqual(len(pr.get("payments")), 1) + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Check the difference_amount is a gain of 5000 + self.assertEqual(flt(pr.allocation[0].get("difference_amount")), 5000.0) + pr.reconcile() + + def test_foreign_currency_reverse_payment_entry_against_payment_entry_for_supplier(self): + transaction_date = nowdate() + self.supplier = "_Test Supplier USD" + amount = 1000 + exchange_rate_at_payment = 100 + exchange_rate_at_reverse_payment = 95 + + # Pay amount to supplier - 1,00,000 + pe = self.create_payment_entry(amount=amount, posting_date=transaction_date) + pe.payment_type = "Pay" + pe.party_type = "Supplier" + pe.party = self.supplier + pe.paid_from = self.cash + pe.paid_from_account_currency = "INR" + pe.target_exchange_rate = exchange_rate_at_payment + pe.paid_amount = exchange_rate_at_payment * amount + pe.received_amount = amount + pe.paid_to = self.creditors_usd + pe.paid_to_account_currency = "USD" + pe.save().submit() + + # Receive amount from supplier - 95,000 + reverse_pe = self.create_payment_entry(amount=amount, posting_date=transaction_date) + reverse_pe.payment_type = "Receive" + reverse_pe.party_type = "Supplier" + reverse_pe.party = self.supplier + reverse_pe.paid_from = self.creditors_usd + reverse_pe.paid_from_account_currency = "USD" + reverse_pe.source_exchange_rate = exchange_rate_at_reverse_payment + reverse_pe.paid_amount = amount + reverse_pe.received_amount = exchange_rate_at_reverse_payment * amount + reverse_pe.paid_to = self.cash + reverse_pe.paid_to_account_currency = "INR" + reverse_pe = reverse_pe.save().submit() + + # Reconcile payments + pr = self.create_payment_reconciliation(party_is_customer=False) + pr.party = self.supplier + pr.receivable_payable_account = self.creditors_usd + pr.get_unreconciled_entries() + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + + self.assertEqual(len(pr.get("invoices")), 1) + self.assertEqual(len(pr.get("payments")), 1) + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Check the difference_amount is a loss of 5000 + self.assertEqual(flt(pr.allocation[0].get("difference_amount")), -5000.0) + pr.reconcile() + + def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_customer(self): + transaction_date = nowdate() + customer = self.customer3 + amount = 1000 + exchange_rate_at_payment = 95 + exchange_rate_at_reverse_payment = 100 + + # Receive amount from customer - 95,000 + je1 = self.create_journal_entry(self.cash, self.debtors_eur, amount, transaction_date) + je1.multi_currency = 1 + je1.accounts[0].exchange_rate = 1 + je1.accounts[0].debit_in_account_currency = exchange_rate_at_payment * amount + je1.accounts[0].debit = exchange_rate_at_payment * amount + je1.accounts[1].party_type = "Customer" + je1.accounts[1].party = customer + je1.accounts[1].exchange_rate = exchange_rate_at_payment + je1.accounts[1].credit_in_account_currency = amount + je1.accounts[1].credit = exchange_rate_at_payment * amount + je1.save() + je1.submit() + + # Pay amount to customer - 1,00,000 + je2 = self.create_journal_entry(self.debtors_eur, self.cash, amount, transaction_date) + je2.multi_currency = 1 + je2.accounts[0].party_type = "Customer" + je2.accounts[0].party = customer + je2.accounts[0].exchange_rate = exchange_rate_at_reverse_payment + je2.accounts[0].debit_in_account_currency = amount + je2.accounts[0].debit = exchange_rate_at_reverse_payment * amount + je2.accounts[1].exchange_rate = 1 + je2.accounts[1].credit_in_account_currency = exchange_rate_at_reverse_payment * amount + je2.accounts[1].credit = exchange_rate_at_reverse_payment * amount + je2.save() + je2.submit() + + # Reconcile payments + pr = self.create_payment_reconciliation() + pr.party = customer + pr.receivable_payable_account = self.debtors_eur + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Check the difference_amount is a loss of 5000 + self.assertEqual(flt(pr.allocation[0].difference_amount), -5000.0) + pr.reconcile() + + def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_supplier(self): + transaction_date = nowdate() + self.supplier = "_Test Supplier USD" + amount = 1000 + exchange_rate_at_payment = 95 + exchange_rate_at_reverse_payment = 100 + + # Pay amount to supplier - 95,000 + je1 = self.create_journal_entry(self.creditors_usd, self.cash, amount, transaction_date) + je1.multi_currency = 1 + je1.accounts[0].party_type = "Supplier" + je1.accounts[0].party = self.supplier + je1.accounts[0].exchange_rate = exchange_rate_at_payment + je1.accounts[0].debit_in_account_currency = amount + je1.accounts[0].debit = exchange_rate_at_payment * amount + je1.accounts[1].exchange_rate = 1 + je1.accounts[1].credit = exchange_rate_at_payment * amount + je1.accounts[1].credit_in_account_currency = exchange_rate_at_payment * amount + je1.save() + je1.submit() + + # Receive amount from supplier - 1,00,000 + je2 = self.create_journal_entry(self.cash, self.creditors_usd, amount, transaction_date) + je2.multi_currency = 1 + je2.accounts[0].exchange_rate = 1 + je2.accounts[0].debit = exchange_rate_at_reverse_payment * amount + je2.accounts[0].debit_in_account_currency = exchange_rate_at_reverse_payment * amount + je2.accounts[1].party_type = "Supplier" + je2.accounts[1].party = self.supplier + je2.accounts[1].exchange_rate = exchange_rate_at_reverse_payment + je2.accounts[1].credit_in_account_currency = amount + je2.accounts[1].credit = exchange_rate_at_reverse_payment * amount + je2.save() + je2.submit() + + # Reconcile payments + pr = self.create_payment_reconciliation() + pr.party_type = "Supplier" + pr.party = self.supplier + pr.receivable_payable_account = self.creditors_usd + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Check the difference_amount is a gain of 5000 + self.assertEqual(flt(pr.allocation[0].difference_amount), 5000.0) + pr.reconcile() + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): From c7f79d16e94ab7fd0ad3888962ca077fd43881ab Mon Sep 17 00:00:00 2001 From: nivithamerlin Date: Fri, 9 Jan 2026 16:36:12 +0530 Subject: [PATCH 19/30] fix(asset): remove references for composite and existing asset (cherry picked from commit c1d50c492b4b24d1dca28db60b85619cf197e0e4) --- erpnext/assets/doctype/asset/asset.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index e2ba7814a3d..7f027f003b5 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -241,6 +241,8 @@ frappe.ui.form.on("Asset", { } else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) { frm.toggle_reqd("purchase_receipt", 0); frm.toggle_reqd("purchase_invoice", 0); + frm.set_value("purchase_receipt", ""); + frm.set_value("purchase_invoice", ""); } else if (frm.doc.purchase_receipt) { // if purchase receipt link is set then set PI disabled frm.toggle_reqd("purchase_invoice", 0); @@ -484,7 +486,6 @@ frappe.ui.form.on("Asset", { } else { frm.set_df_property("gross_purchase_amount", "read_only", 0); } - frm.trigger("toggle_reference_doc"); }, From ea0b76831f8c202f88234d792ebef0876b5ebedc Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Mon, 12 Jan 2026 13:01:54 +0530 Subject: [PATCH 20/30] fix(asset): properly reset purchase reference and item fields (cherry picked from commit 671610db1e3307b53d73ea4e1e9c4dfd8417e048) --- erpnext/assets/doctype/asset/asset.js | 78 +++++++++++++++++++-------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 7f027f003b5..f81b26b2c8d 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -235,28 +235,64 @@ frappe.ui.form.on("Asset", { }, toggle_reference_doc: function (frm) { - if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) { - frm.set_df_property("purchase_invoice", "read_only", 1); - frm.set_df_property("purchase_receipt", "read_only", 1); - } else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) { - frm.toggle_reqd("purchase_receipt", 0); - frm.toggle_reqd("purchase_invoice", 0); - frm.set_value("purchase_receipt", ""); - frm.set_value("purchase_invoice", ""); - } else if (frm.doc.purchase_receipt) { - // if purchase receipt link is set then set PI disabled - frm.toggle_reqd("purchase_invoice", 0); - frm.set_df_property("purchase_invoice", "read_only", 1); - } else if (frm.doc.purchase_invoice) { - // if purchase invoice link is set then set PR disabled - frm.toggle_reqd("purchase_receipt", 0); - frm.set_df_property("purchase_receipt", "read_only", 1); - } else { - frm.toggle_reqd("purchase_receipt", 1); - frm.set_df_property("purchase_receipt", "read_only", 0); - frm.toggle_reqd("purchase_invoice", 1); - frm.set_df_property("purchase_invoice", "read_only", 0); + const is_submitted = frm.doc.docstatus === 1; + const is_special_asset = frm.doc.is_existing_asset || frm.doc.is_composite_asset; + + const clear_field = (field) => { + if (frm.doc[field]) { + frm.set_value(field, ""); + } + }; + + ["purchase_receipt", "purchase_receipt_item", "purchase_invoice", "purchase_invoice_item"].forEach( + (field) => { + frm.toggle_reqd(field, 0); + frm.set_df_property(field, "read_only", 0); + } + ); + + if (is_submitted) { + [ + "purchase_receipt", + "purchase_receipt_item", + "purchase_invoice", + "purchase_invoice_item", + ].forEach((field) => { + frm.set_df_property(field, "read_only", 1); + }); + return; } + + if (is_special_asset) { + clear_field("purchase_receipt"); + clear_field("purchase_receipt_item"); + clear_field("purchase_invoice"); + clear_field("purchase_invoice_item"); + return; + } + + if (frm.doc.purchase_receipt) { + frm.toggle_reqd("purchase_receipt_item", 1); + + ["purchase_invoice", "purchase_invoice_item"].forEach((field) => { + clear_field(field); + frm.set_df_property(field, "read_only", 1); + }); + return; + } + + if (frm.doc.purchase_invoice) { + frm.toggle_reqd("purchase_invoice_item", 1); + + ["purchase_receipt", "purchase_receipt_item"].forEach((field) => { + clear_field(field); + frm.set_df_property(field, "read_only", 1); + }); + return; + } + + frm.toggle_reqd("purchase_receipt", 1); + frm.toggle_reqd("purchase_invoice", 1); }, make_journal_entry: function (frm) { From d7bf1a179afe6c33249543e552dd9abf0b471da5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 13 Jan 2026 10:43:34 +0530 Subject: [PATCH 21/30] chore: update links --- erpnext/change_log/v15/v15_94_2.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/change_log/v15/v15_94_2.md b/erpnext/change_log/v15/v15_94_2.md index b237422ee77..d6563b12348 100644 --- a/erpnext/change_log/v15/v15_94_2.md +++ b/erpnext/change_log/v15/v15_94_2.md @@ -4,6 +4,6 @@ ERPNext version 16 has been released! Since it's the latest version of ERPNext, we recommend you to update to it to get the latest features, bug fixes and other improvements. -[Click here to know more about v16](https://frappe.io/) +[Click here to know more about v16](https://frappe.io/erpnext/version-16) -If you're on [Frappe Cloud](https://frappe.io/cloud), [click here to know how to update to v16](https://frappe.io/) \ No newline at end of file +If you're on [Frappe Cloud](https://frappe.io/cloud), [click here to learn how to update to v16](https://docs.frappe.io/cloud/sites/version-upgrade) From 4e94e3726cbe6376e3a5f0ecbb4b9b25060dcdf3 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 13 Jan 2026 10:50:15 +0530 Subject: [PATCH 22/30] chore: grammar fix --- erpnext/change_log/v15/v15_94_2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/change_log/v15/v15_94_2.md b/erpnext/change_log/v15/v15_94_2.md index d6563b12348..865ecacb86f 100644 --- a/erpnext/change_log/v15/v15_94_2.md +++ b/erpnext/change_log/v15/v15_94_2.md @@ -2,7 +2,7 @@ ERPNext version 16 has been released! -Since it's the latest version of ERPNext, we recommend you to update to it to get the latest features, bug fixes and other improvements. +Since it's the latest version of ERPNext, we recommend that you update to it to get the latest features, bug fixes and other improvements. [Click here to know more about v16](https://frappe.io/erpnext/version-16) From dae6adfe13a07061ee89a772fa5e04903ea0d63c Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Sun, 11 Jan 2026 19:20:01 +0530 Subject: [PATCH 23/30] fix(asset value adjustment): skip cancelling revaluation journal entry if already cancelled (cherry picked from commit b1704ccef15c4f36cf41197f781f571cec8f0f11) # Conflicts: # erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py --- .../asset_value_adjustment.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 31fd62095df..2f6326628d8 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -57,7 +57,7 @@ class AssetValueAdjustment(Document): ) def on_cancel(self): - frappe.get_doc("Journal Entry", self.journal_entry).cancel() + self.cancel_asset_revaluation_entry() self.update_asset() add_asset_activity( self.asset, @@ -180,6 +180,7 @@ class AssetValueAdjustment(Document): get_link_to_form(self.get("doctype"), self.get("name")), ) +<<<<<<< HEAD make_new_active_asset_depr_schedules_and_cancel_current_ones( asset, notes, @@ -189,6 +190,33 @@ class AssetValueAdjustment(Document): ) asset.flags.ignore_validate_update_after_submit = True asset.save() +======= + return credit_entry, debit_entry + + def update_accounting_dimensions(self, credit_entry, debit_entry): + accounting_dimensions = get_checks_for_pl_and_bs_accounts() + + for dimension in accounting_dimensions: + dimension_value = self.get(dimension["fieldname"]) or dimension.get("default_dimension") + if dimension.get("mandatory_for_bs"): + credit_entry.update({dimension["fieldname"]: dimension_value}) + + if dimension.get("mandatory_for_pl"): + debit_entry.update({dimension["fieldname"]: dimension_value}) + + def cancel_asset_revaluation_entry(self): + if not self.journal_entry: + return + + revaluation_entry = frappe.get_doc("Journal Entry", self.journal_entry) + if revaluation_entry.docstatus == 1: + revaluation_entry.cancel() + + def update_asset(self): + asset = self.update_asset_value_after_depreciation() + note = self.get_adjustment_note() + reschedule_depreciation(asset, note) +>>>>>>> b1704ccef1 (fix(asset value adjustment): skip cancelling revaluation journal entry if already cancelled) asset.set_status() def update_asset_value_after_depreciation(self, difference_amount): From 426516a1ee65d7eeb5b419f3984fc9ad9a2e2b37 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Sun, 11 Jan 2026 19:25:22 +0530 Subject: [PATCH 24/30] refactor(journal entry): replace raw SQL with query builder to unlink asset value adjustment (cherry picked from commit 5f00239bbae8e6251244e97c54459f9a5f4400db) --- .../accounts/doctype/journal_entry/journal_entry.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 0aa48dcd721..06ee475361f 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -448,11 +448,12 @@ class JournalEntry(AccountsController): frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "") def unlink_asset_adjustment_entry(self): - frappe.db.sql( - """ update `tabAsset Value Adjustment` - set journal_entry = null where journal_entry = %s""", - self.name, - ) + AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment") + ( + frappe.qb.update(AssetValueAdjustment) + .set(AssetValueAdjustment.journal_entry, None) + .where(AssetValueAdjustment.journal_entry == self.name) + ).run() def validate_party(self): for d in self.get("accounts"): From 129457b2ce1766566a94b00faad2849cdf064d67 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Sun, 11 Jan 2026 21:30:09 +0530 Subject: [PATCH 25/30] fix: ignore permissions when cancelling revaluation journal entry (cherry picked from commit 500c44e3f51fcfa6b8463de5c73110e3a5b73094) --- .../doctype/asset_value_adjustment/asset_value_adjustment.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 2f6326628d8..c9f12490c33 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -210,6 +210,8 @@ class AssetValueAdjustment(Document): revaluation_entry = frappe.get_doc("Journal Entry", self.journal_entry) if revaluation_entry.docstatus == 1: + # Ignore permissions to match Journal Entry submission behavior + revaluation_entry.flags.ignore_permissions = True revaluation_entry.cancel() def update_asset(self): From 07de3f43915f40644e3e32862899c45bbbd2b819 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Mon, 12 Jan 2026 18:18:22 +0530 Subject: [PATCH 26/30] fix: prevent manual cancellation of the linked Revaluation Journal Entry (cherry picked from commit 73b038084b43a02f614f8148eb2353f4cf78c4fa) # Conflicts: # erpnext/accounts/doctype/journal_entry/journal_entry.py --- .../doctype/journal_entry/journal_entry.py | 22 +++++++++++++++++++ .../asset_value_adjustment.py | 1 + 2 files changed, 23 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 06ee475361f..8e6a99716b0 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -177,6 +177,9 @@ class JournalEntry(AccountsController): else: return self._submit() + def before_cancel(self): + pass + def cancel(self): if len(self.accounts) > 100: queue_submission(self, "_cancel") @@ -224,6 +227,11 @@ class JournalEntry(AccountsController): "Advance Payment Ledger Entry", ) self.make_gl_entries(1) +<<<<<<< HEAD +======= + JournalTaxWithholding(self).on_cancel() + self.has_asset_adjustment_entry() +>>>>>>> 73b038084b (fix: prevent manual cancellation of the linked Revaluation Journal Entry) self.unlink_advance_entry_reference() self.unlink_asset_reference() self.unlink_inter_company_jv() @@ -447,6 +455,20 @@ class JournalEntry(AccountsController): ) frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "") + def has_asset_adjustment_entry(self): + if self.flags.get("via_asset_value_adjustment"): + return + + asset_value_adjustment = frappe.db.get_value( + "Asset Value Adjustment", {"docstatus": 1, "journal_entry": self.name}, "name" + ) + if asset_value_adjustment: + frappe.throw( + _( + "Cannot cancel this document as it is linked with the submitted Asset Value Adjustment {0}. Please cancel the Asset Value Adjustment to continue." + ).format(frappe.utils.get_link_to_form("Asset Value Adjustment", asset_value_adjustment)) + ) + def unlink_asset_adjustment_entry(self): AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment") ( diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index c9f12490c33..4d554d4faea 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -212,6 +212,7 @@ class AssetValueAdjustment(Document): if revaluation_entry.docstatus == 1: # Ignore permissions to match Journal Entry submission behavior revaluation_entry.flags.ignore_permissions = True + revaluation_entry.flags.via_asset_value_adjustment = True revaluation_entry.cancel() def update_asset(self): From 11d23e1a4a3ada7549640aabba04a9a838d6db3d Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Tue, 13 Jan 2026 12:16:52 +0530 Subject: [PATCH 27/30] fix: move validation to before_cancel (cherry picked from commit d65cd605a1bbc85fd0935c40c7a2c19b9016d736) # Conflicts: # erpnext/accounts/doctype/journal_entry/journal_entry.py --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 8e6a99716b0..14f4954c0e3 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -178,7 +178,7 @@ class JournalEntry(AccountsController): return self._submit() def before_cancel(self): - pass + self.has_asset_adjustment_entry() def cancel(self): if len(self.accounts) > 100: @@ -230,8 +230,11 @@ class JournalEntry(AccountsController): <<<<<<< HEAD ======= JournalTaxWithholding(self).on_cancel() +<<<<<<< HEAD self.has_asset_adjustment_entry() >>>>>>> 73b038084b (fix: prevent manual cancellation of the linked Revaluation Journal Entry) +======= +>>>>>>> d65cd605a1 (fix: move validation to before_cancel) self.unlink_advance_entry_reference() self.unlink_asset_reference() self.unlink_inter_company_jv() From 3365bc3ba37599ad7b5cd58afa6834a04ebfe62b Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 13 Jan 2026 16:23:00 +0530 Subject: [PATCH 28/30] chore: rebase with v15 branch --- .../doctype/journal_entry/journal_entry.py | 8 ---- .../asset_value_adjustment.py | 41 +++++-------------- 2 files changed, 10 insertions(+), 39 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 14f4954c0e3..e7c0832554f 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -227,14 +227,6 @@ class JournalEntry(AccountsController): "Advance Payment Ledger Entry", ) self.make_gl_entries(1) -<<<<<<< HEAD -======= - JournalTaxWithholding(self).on_cancel() -<<<<<<< HEAD - self.has_asset_adjustment_entry() ->>>>>>> 73b038084b (fix: prevent manual cancellation of the linked Revaluation Journal Entry) -======= ->>>>>>> d65cd605a1 (fix: move validation to before_cancel) self.unlink_advance_entry_reference() self.unlink_asset_reference() self.unlink_inter_company_jv() diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 4d554d4faea..756b95f8fbd 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -159,6 +159,16 @@ class AssetValueAdjustment(Document): self.db_set("journal_entry", je.name) + def cancel_asset_revaluation_entry(self): + if not self.journal_entry: + return + + revaluation_entry = frappe.get_doc("Journal Entry", self.journal_entry) + if revaluation_entry.docstatus == 1: + revaluation_entry.flags.ignore_permissions = True + revaluation_entry.flags.via_asset_value_adjustment = True + revaluation_entry.cancel() + def update_asset(self, asset_value=None): difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount asset = self.update_asset_value_after_depreciation(difference_amount) @@ -180,7 +190,6 @@ class AssetValueAdjustment(Document): get_link_to_form(self.get("doctype"), self.get("name")), ) -<<<<<<< HEAD make_new_active_asset_depr_schedules_and_cancel_current_ones( asset, notes, @@ -190,36 +199,6 @@ class AssetValueAdjustment(Document): ) asset.flags.ignore_validate_update_after_submit = True asset.save() -======= - return credit_entry, debit_entry - - def update_accounting_dimensions(self, credit_entry, debit_entry): - accounting_dimensions = get_checks_for_pl_and_bs_accounts() - - for dimension in accounting_dimensions: - dimension_value = self.get(dimension["fieldname"]) or dimension.get("default_dimension") - if dimension.get("mandatory_for_bs"): - credit_entry.update({dimension["fieldname"]: dimension_value}) - - if dimension.get("mandatory_for_pl"): - debit_entry.update({dimension["fieldname"]: dimension_value}) - - def cancel_asset_revaluation_entry(self): - if not self.journal_entry: - return - - revaluation_entry = frappe.get_doc("Journal Entry", self.journal_entry) - if revaluation_entry.docstatus == 1: - # Ignore permissions to match Journal Entry submission behavior - revaluation_entry.flags.ignore_permissions = True - revaluation_entry.flags.via_asset_value_adjustment = True - revaluation_entry.cancel() - - def update_asset(self): - asset = self.update_asset_value_after_depreciation() - note = self.get_adjustment_note() - reschedule_depreciation(asset, note) ->>>>>>> b1704ccef1 (fix(asset value adjustment): skip cancelling revaluation journal entry if already cancelled) asset.set_status() def update_asset_value_after_depreciation(self, difference_amount): From ed05b4cc5cd026f2100ba7249b6007a913992dda Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 13 Jan 2026 17:26:32 +0530 Subject: [PATCH 29/30] fix(minor): hide target_qty field from the capitalization --- .../asset_capitalization/asset_capitalization.json | 14 ++++++++------ .../asset_capitalization/asset_capitalization.py | 14 ++++---------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json index c2de548b2de..fb7a556f2b7 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -177,6 +177,7 @@ "default": "1", "fieldname": "target_qty", "fieldtype": "Float", + "hidden": 1, "label": "Target Qty" }, { @@ -290,10 +291,10 @@ "options": "Cost Center" }, { - "fieldname": "project", - "fieldtype": "Link", - "label": "Project", - "options": "Project" + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" }, { "fieldname": "dimension_col_break", @@ -324,7 +325,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-01-08 13:14:33.008458", + "modified": "2026-01-13 17:25:01.352568", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization", @@ -362,10 +363,11 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], "title_field": "title", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 1f671333cfa..387d5227350 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -54,18 +54,11 @@ class AssetCapitalization(StockController): from typing import TYPE_CHECKING if TYPE_CHECKING: + from erpnext.assets.doctype.asset_capitalization_asset_item.asset_capitalization_asset_item import AssetCapitalizationAssetItem + from erpnext.assets.doctype.asset_capitalization_service_item.asset_capitalization_service_item import AssetCapitalizationServiceItem + from erpnext.assets.doctype.asset_capitalization_stock_item.asset_capitalization_stock_item import AssetCapitalizationStockItem from frappe.types import DF - from erpnext.assets.doctype.asset_capitalization_asset_item.asset_capitalization_asset_item import ( - AssetCapitalizationAssetItem, - ) - from erpnext.assets.doctype.asset_capitalization_service_item.asset_capitalization_service_item import ( - AssetCapitalizationServiceItem, - ) - from erpnext.assets.doctype.asset_capitalization_stock_item.asset_capitalization_stock_item import ( - AssetCapitalizationStockItem, - ) - amended_from: DF.Link | None asset_items: DF.Table[AssetCapitalizationAssetItem] asset_items_total: DF.Currency @@ -76,6 +69,7 @@ class AssetCapitalization(StockController): naming_series: DF.Literal["ACC-ASC-.YYYY.-"] posting_date: DF.Date posting_time: DF.Time + project: DF.Link | None service_items: DF.Table[AssetCapitalizationServiceItem] service_items_total: DF.Currency set_posting_time: DF.Check From a66129af29d150fbf7120058a4f16723be827a93 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 13 Jan 2026 17:28:51 +0530 Subject: [PATCH 30/30] chore: run pre-commit --- .../asset_capitalization/asset_capitalization.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 387d5227350..674cb3ffa3d 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -54,11 +54,18 @@ class AssetCapitalization(StockController): from typing import TYPE_CHECKING if TYPE_CHECKING: - from erpnext.assets.doctype.asset_capitalization_asset_item.asset_capitalization_asset_item import AssetCapitalizationAssetItem - from erpnext.assets.doctype.asset_capitalization_service_item.asset_capitalization_service_item import AssetCapitalizationServiceItem - from erpnext.assets.doctype.asset_capitalization_stock_item.asset_capitalization_stock_item import AssetCapitalizationStockItem from frappe.types import DF + from erpnext.assets.doctype.asset_capitalization_asset_item.asset_capitalization_asset_item import ( + AssetCapitalizationAssetItem, + ) + from erpnext.assets.doctype.asset_capitalization_service_item.asset_capitalization_service_item import ( + AssetCapitalizationServiceItem, + ) + from erpnext.assets.doctype.asset_capitalization_stock_item.asset_capitalization_stock_item import ( + AssetCapitalizationStockItem, + ) + amended_from: DF.Link | None asset_items: DF.Table[AssetCapitalizationAssetItem] asset_items_total: DF.Currency