From 4ec5b28fd27a56a238d27273642581a8f0742b97 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Wed, 8 Oct 2025 08:43:42 +0530 Subject: [PATCH 01/52] fix(asset movement): clear custodian if not present (cherry picked from commit 323d8eaccdb213e12cdbdbde3fe8e8b24323c4ea) # Conflicts: # erpnext/assets/doctype/asset_movement/asset_movement.py --- erpnext/assets/doctype/asset_movement/asset_movement.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index cfe9768ef4b..e1a1ce0871e 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import get_link_to_form +from frappe.utils import cstr, get_link_to_form from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity @@ -142,6 +142,7 @@ class AssetMovement(Document): def update_asset_location_and_custodian(self, asset_id, location, employee): asset = frappe.get_doc("Asset", asset_id) +<<<<<<< HEAD updates = {} if employee and employee != asset.custodian: updates["custodian"] = employee @@ -149,6 +150,10 @@ class AssetMovement(Document): elif not employee and asset.custodian: updates["custodian"] = "" +======= + if cstr(employee) != asset.custodian: + frappe.db.set_value("Asset", asset_id, "custodian", cstr(employee)) +>>>>>>> 323d8eaccd (fix(asset movement): clear custodian if not present) if location and location != asset.location: updates["location"] = location From 38e1ca13627a5a922d9df96335df1eedbee628a0 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:53:25 +0530 Subject: [PATCH 02/52] fix: resolve conflict --- .../doctype/asset_movement/asset_movement.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index e1a1ce0871e..44aa271846c 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -142,23 +142,10 @@ class AssetMovement(Document): def update_asset_location_and_custodian(self, asset_id, location, employee): asset = frappe.get_doc("Asset", asset_id) -<<<<<<< HEAD - updates = {} - if employee and employee != asset.custodian: - updates["custodian"] = employee - - elif not employee and asset.custodian: - updates["custodian"] = "" - -======= if cstr(employee) != asset.custodian: frappe.db.set_value("Asset", asset_id, "custodian", cstr(employee)) ->>>>>>> 323d8eaccd (fix(asset movement): clear custodian if not present) if location and location != asset.location: - updates["location"] = location - - if updates: - frappe.db.set_value("Asset", asset_id, updates) + frappe.db.set_value("Asset", asset_id, "location", location) def log_asset_activity(self, asset_id, location, employee): if location and employee: From 93df11a0cf3865c94f6342dc79d88027f7e8a629 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 8 Oct 2025 18:27:39 +0530 Subject: [PATCH 03/52] fix: incorrect field valuation_rate (cherry picked from commit 630d8732143b1273cc1f7ee4f2d350f80a888d27) --- erpnext/stock/stock_ledger.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index e92d45ec5dd..a06412373e2 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1070,16 +1070,15 @@ class update_entries_after: for d in sabb_data: incoming_rate = get_incoming_rate_for_serial_and_batch(self.item_code, d, sn_obj) - - if flt(incoming_rate, self.currency_precision) == flt( - d.valuation_rate, self.currency_precision - ) and not getattr(d, "stock_queue", None): - continue - amount = incoming_rate * flt(d.qty) tot_amt += flt(amount) total_qty += flt(d.qty) + if flt(incoming_rate, self.currency_precision) == flt( + d.incoming_rate, self.currency_precision + ) and not getattr(d, "stock_queue", None): + continue + values_to_update = { "incoming_rate": incoming_rate, "stock_value_difference": amount, From f4816e4960c1fd0cafb34cdac6a8c0361cf62a2b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 8 Oct 2025 18:37:40 +0530 Subject: [PATCH 04/52] fix: batch qty for expired batches (cherry picked from commit ff2faf36a7640cda4a9fe4bfb5b1380752af7f97) --- erpnext/stock/doctype/batch/batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 46618a2ca4a..9955b485c22 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -158,7 +158,7 @@ class Batch(Document): @frappe.whitelist() def recalculate_batch_qty(self): - batches = get_batch_qty(batch_no=self.name, item_code=self.item) + batches = get_batch_qty(batch_no=self.name, item_code=self.item, for_stock_levels=True) batch_qty = 0.0 if batches: for row in batches: From 81ed32ff51eff145293eca55203a2f131b8a41b5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 8 Oct 2025 21:46:17 +0530 Subject: [PATCH 05/52] fix: Reset Raw Materials Table button not working (cherry picked from commit 128e243945457d36e720ffb4633113fdba172d93) --- erpnext/controllers/subcontracting_controller.py | 6 ++++++ .../subcontracting_receipt/subcontracting_receipt.js | 4 ++++ .../subcontracting_receipt/subcontracting_receipt.json | 4 +++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index da03f907e3b..4666c953fbe 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -201,6 +201,9 @@ class SubcontractingController(StockController): self.set(self.raw_material_table, []) return + if not self.get(self.raw_material_table): + return + item_dict = self.__get_data_before_save() if not item_dict: return True @@ -651,6 +654,9 @@ class SubcontractingController(StockController): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward from erpnext.stock.get_item_details import get_filtered_serial_nos + if self.is_return: + return + for row in self.supplied_items: item_details = frappe.get_cached_value( "Item", row.rm_item_code, ["has_batch_no", "has_serial_no"], as_dict=1 diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index c9fe457ef79..fee1cac2542 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -336,6 +336,10 @@ frappe.ui.form.on("Subcontracting Receipt", { reset_raw_materials_table: (frm) => { frm.clear_table("supplied_items"); + frm.doc.__unsaved = true; + if (!frm.doc.set_posting_time) { + frm.set_value("posting_time", frappe.datetime.now_time()); + } frm.call({ method: "reset_raw_materials", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json index e975f384c25..dcc02f7a5a1 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -649,6 +649,7 @@ "label": "Raw Materials Actions" }, { + "description": "Click this button if you encounter a negative stock error for a serial or batch item. The system will fetch the available serials or batches automatically.", "fieldname": "reset_raw_materials_table", "fieldtype": "Button", "label": "Reset Raw Materials Table" @@ -678,7 +679,7 @@ "in_create": 1, "is_submittable": 1, "links": [], - "modified": "2024-12-06 15:24:38.384232", + "modified": "2025-10-08 21:43:27.065640", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt", @@ -739,6 +740,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "status, posting_date, supplier", "show_name_in_global_search": 1, "sort_field": "modified", From ac46b3d1cab3a0bdf08c7e772bbbc28b7c859d73 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 9 Oct 2025 01:00:31 +0530 Subject: [PATCH 06/52] fix: sales return for product bundle items (cherry picked from commit 13ce7279a87a538fae272748b0e84e4efb673b7c) --- .../controllers/sales_and_purchase_return.py | 22 +++++++++++++--- erpnext/controllers/selling_controller.py | 9 ++++++- erpnext/controllers/stock_controller.py | 26 ++++++++++++++++++- .../serial_and_batch_bundle.py | 14 ++++++++++ 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index a45b5813584..f45b06b01b0 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -852,10 +852,10 @@ def get_available_serial_batches(field, doctype, reference_ids, is_rejected=Fals if not _bundle_ids: return frappe._dict({}) - return get_serial_batches_based_on_bundle(field, _bundle_ids) + return get_serial_batches_based_on_bundle(doctype, field, _bundle_ids) -def get_serial_batches_based_on_bundle(field, _bundle_ids): +def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids): available_dict = frappe._dict({}) batch_serial_nos = frappe.get_all( "Serial and Batch Bundle", @@ -867,6 +867,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): "`tabSerial and Batch Bundle`.`voucher_detail_no`", "`tabSerial and Batch Bundle`.`voucher_type`", "`tabSerial and Batch Bundle`.`voucher_no`", + "`tabSerial and Batch Bundle`.`item_code`", ], filters=[ ["Serial and Batch Bundle", "name", "in", _bundle_ids], @@ -880,6 +881,9 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"): key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field) + if doctype == "Packed Item": + key = (row.item_code, key) + if row.voucher_type in ["Sales Invoice", "Delivery Note"]: row.qty = -1 * row.qty @@ -908,6 +912,8 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False): filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")} + if doctype == "Packed Item": + filters = {"docstatus": 1, field: ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")} pluck_field = "serial_and_batch_bundle" if is_rejected: @@ -921,10 +927,14 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False pluck=pluck_field, ) + if _bundle_ids and doctype == "Packed Item": + return _bundle_ids + if not _bundle_ids: return {} - del filters["name"] + if "name" in filters: + del filters["name"] filters[field] = ("in", reference_ids) @@ -971,6 +981,9 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field if not qty_field: qty_field = "stock_qty" + if not hasattr(row, qty_field): + qty_field = "qty" + if not warehouse_field: warehouse_field = "warehouse" @@ -1060,6 +1073,9 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f if not qty_field: qty_field = "stock_qty" + if not hasattr(child_doc, qty_field): + qty_field = "qty" + warehouse = child_doc.get(warehouse_field) if parent_doc.get("is_internal_customer"): warehouse = child_doc.get("target_warehouse") diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index d85c1b28f97..8fc49d09c0e 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -517,8 +517,15 @@ class SellingController(StockController): if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"): continue + item_details = frappe.get_cached_value( + "Item", d.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 + ) + if not self.get("return_against") or ( - get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") + get_valuation_method(d.item_code) == "Moving Average" + and self.get("is_return") + and not item_details.has_serial_no + and not item_details.has_batch_no ): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty")) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 3b5dbf2ee81..d7c9bb61a20 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -335,10 +335,20 @@ class StockController(AccountsController): return child_doctype = self.doctype + " Item" + if table_name == "packed_items": + field = "parent_detail_docname" + child_doctype = "Packed Item" + available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids) for row in self.get(table_name): - if data := available_dict.get(row.get(field)): + value = row.get(field) + if table_name == "packed_items" and row.get("parent_detail_docname"): + value = self.get_value_for_packed_item(row) + if not value: + continue + + if data := available_dict.get(value): data = filter_serial_batches(self, data, row) bundle = make_serial_batch_bundle_for_return(data, row, self) row.db_set( @@ -354,6 +364,14 @@ class StockController(AccountsController): "incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") ) + def get_value_for_packed_item(self, row): + parent_items = self.get("items", {"name": row.parent_detail_docname}) + if parent_items: + ref = parent_items[0].get("dn_detail") + return (row.item_code, ref) + + return None + def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]: field = { "Sales Invoice": "sales_invoice_item", @@ -388,6 +406,12 @@ class StockController(AccountsController): ): reference_ids.append(row.get(field)) + if table_name == "packed_items" and row.get("parent_detail_docname"): + parent_rows = self.get("items", {"name": row.parent_detail_docname}) or [] + for d in parent_rows: + if d.get(field) and not d.get(bundle_field): + reference_ids.append(d.get(field)) + return field, reference_ids @frappe.request_cache 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 9c69d856d45..d8d68f34fbb 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 @@ -638,6 +638,17 @@ class SerialandBatchBundle(Document): if not rate and self.voucher_detail_no and self.voucher_no: rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) + is_packed_item = False + if rate is None and child_table == "Delivery Note Item": + rate = frappe.db.get_value( + "Packed Item", + self.voucher_detail_no, + "incoming_rate", + ) + + if rate is not None: + is_packed_item = True + stock_queue = [] batches = [] if prev_sle and prev_sle.stock_queue: @@ -659,6 +670,9 @@ class SerialandBatchBundle(Document): elif (d.incoming_rate == rate) and not stock_queue and d.qty and d.stock_value_difference: continue + if is_packed_item and d.incoming_rate: + rate = d.incoming_rate + d.incoming_rate = flt(rate) if d.qty: d.stock_value_difference = flt(d.qty) * d.incoming_rate From 6ba55bbee09b813d31aac76472643d1a8ce65e08 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 9 Oct 2025 13:56:04 +0530 Subject: [PATCH 07/52] test: test case for sales return for product bundle (cherry picked from commit 1d57bbca1105a041a8be88d0daf6f593465cbd7f) # Conflicts: # erpnext/stock/doctype/delivery_note/test_delivery_note.py --- .../controllers/sales_and_purchase_return.py | 26 ++- erpnext/controllers/selling_controller.py | 3 + .../delivery_note/test_delivery_note.py | 167 ++++++++++++++++++ .../serial_and_batch_bundle.py | 2 +- 4 files changed, 196 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index f45b06b01b0..5c0f78ac986 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -849,6 +849,7 @@ def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False): _bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected) + if not _bundle_ids: return frappe._dict({}) @@ -882,6 +883,13 @@ def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids): key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field) if doctype == "Packed Item": + if key is None: + key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field) + if row.voucher_type == "Delivery Note": + key = frappe.get_cached_value("Delivery Note Item", key, "dn_detail") + elif row.voucher_type == "Sales Invoice": + key = frappe.get_cached_value("Sales Invoice Item", key, "sales_invoice_item") + key = (row.item_code, key) if row.voucher_type in ["Sales Invoice", "Delivery Note"]: @@ -913,7 +921,7 @@ def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids): def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False): filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")} if doctype == "Packed Item": - filters = {"docstatus": 1, field: ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")} + filters = get_filters_for_packed_item(field, reference_ids) pluck_field = "serial_and_batch_bundle" if is_rejected: @@ -977,6 +985,22 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False return _bundle_ids +def get_filters_for_packed_item(field, reference_ids): + names = [] + filters = {"docstatus": 1, "dn_detail": ("in", reference_ids)} + if dns := frappe.get_all("Delivery Note Item", filters=filters, pluck="name"): + names.extend(dns) + + filters = {"docstatus": 1, "sales_invoice_item": ("in", reference_ids)} + if sis := frappe.get_all("Sales Invoice Item", filters=filters, pluck="name"): + names.extend(sis) + + if names: + reference_ids.extend(names) + + return {"docstatus": 1, field: ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")} + + def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None): if not qty_field: qty_field = "stock_qty" diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 8fc49d09c0e..e4e2ee29d9b 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -1004,6 +1004,9 @@ def set_default_income_account_for_item(obj): def get_serial_and_batch_bundle(child, parent, delivery_note_child=None): from erpnext.stock.serial_batch_bundle import SerialBatchCreation + if parent.get("is_return") and parent.get("packed_items"): + return + if child.get("use_serial_batch_fields"): return diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 8e1a38b5ea7..407e005a1b8 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2596,6 +2596,173 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn.per_billed, 100) self.assertEqual(dn.per_returned, 100) +<<<<<<< HEAD +======= + def test_packed_item_serial_no_status(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.item.test_item import make_item + + # test Update Items with product bundle + if not frappe.db.exists("Item", "_Test Product Bundle Item New 1"): + bundle_item = make_item("_Test Product Bundle Item New 1", {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + make_item( + "_Packed Item New Sn Item", + {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-PACKED-NEW-.#####"}, + ) + make_product_bundle("_Test Product Bundle Item New 1", ["_Packed Item New Sn Item"], 1) + + make_stock_entry(item="_Packed Item New Sn Item", target="_Test Warehouse - _TC", qty=5, rate=100) + + dn = create_delivery_note( + item_code="_Test Product Bundle Item New 1", + warehouse="_Test Warehouse - _TC", + qty=5, + ) + + dn.reload() + + serial_nos = [] + for row in dn.packed_items: + self.assertTrue(row.serial_and_batch_bundle) + doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) + for row in doc.entries: + status = frappe.db.get_value("Serial No", row.serial_no, "status") + self.assertEqual(status, "Delivered") + serial_nos.append(row.serial_no) + + dn.cancel() + + for row in serial_nos: + status = frappe.db.get_value("Serial No", row, "status") + self.assertEqual(status, "Active") + + def test_sales_return_for_product_bundle(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + from erpnext.stock.doctype.item.test_item import make_item + + rm_items = [] + for item_code, properties in { + "_Packed Service Item": {"is_stock_item": 0}, + "_Packed FG Item New 1": { + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SN-PACKED-1-.#####", + }, + "_Packed FG Item New 2": { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BATCH-PACKED-2-.#####", + }, + "_Packed FG Item New 3": { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BATCH-PACKED-3-.#####", + "has_serial_no": 1, + "serial_no_series": "SN-PACKED-3-.#####", + }, + }.items(): + if not frappe.db.exists("Item", item_code): + make_item(item_code, properties) + + if item_code != "_Packed Service Item": + rm_items.append(item_code) + + for rate in [100, 200]: + make_stock_entry(item=item_code, target="_Test Warehouse - _TC", qty=5, rate=rate) + + make_product_bundle("_Packed Service Item", rm_items) + dn = create_delivery_note( + item_code="_Packed Service Item", + warehouse="_Test Warehouse - _TC", + qty=5, + ) + + serial_batch_map = {} + for row in dn.packed_items: + self.assertTrue(row.serial_and_batch_bundle) + if row.item_code not in serial_batch_map: + serial_batch_map[row.item_code] = frappe._dict( + { + "serial_nos": [], + "batches": defaultdict(int), + "serial_no_valuation": defaultdict(float), + "batch_no_valuation": defaultdict(float), + } + ) + + doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) + for entry in doc.entries: + if entry.serial_no: + serial_batch_map[row.item_code].serial_nos.append(entry.serial_no) + serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no] = entry.incoming_rate + if entry.batch_no: + serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty + serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no] = entry.incoming_rate + + dn1 = make_sales_return(dn.name) + dn1.items[0].qty = -2 + dn1.submit() + dn1.reload() + + for row in dn1.packed_items: + doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) + for entry in doc.entries: + if entry.serial_no: + self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos) + self.assertEqual( + entry.incoming_rate, + serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no], + ) + serial_batch_map[row.item_code].serial_nos.remove(entry.serial_no) + serial_batch_map[row.item_code].serial_no_valuation.pop(entry.serial_no) + + elif entry.batch_no: + serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty + self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches) + self.assertEqual(entry.qty, 2.0) + self.assertEqual( + entry.incoming_rate, + serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no], + ) + + dn2 = make_sales_return(dn.name) + dn2.items[0].qty = -3 + dn2.submit() + dn2.reload() + + for row in dn2.packed_items: + doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) + for entry in doc.entries: + if entry.serial_no: + self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos) + self.assertEqual( + entry.incoming_rate, + serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no], + ) + serial_batch_map[row.item_code].serial_nos.remove(entry.serial_no) + serial_batch_map[row.item_code].serial_no_valuation.pop(entry.serial_no) + + elif entry.batch_no: + serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty + self.assertEqual(serial_batch_map[row.item_code].batches[entry.batch_no], 0.0) + + self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches) + + self.assertEqual(entry.qty, 3.0) + self.assertEqual( + entry.incoming_rate, + serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no], + ) + +>>>>>>> 1d57bbca11 (test: test case for sales return for product bundle) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") 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 d8d68f34fbb..ca3e7ec1ad7 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 @@ -639,7 +639,7 @@ class SerialandBatchBundle(Document): rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) is_packed_item = False - if rate is None and child_table == "Delivery Note Item": + if rate is None and child_table in ["Delivery Note Item", "Sales Invoice Item"]: rate = frappe.db.get_value( "Packed Item", self.voucher_detail_no, From 1fbc03c1045d7832ffdd35f2aecdd00cbe458c8c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 9 Oct 2025 15:26:08 +0530 Subject: [PATCH 08/52] chore: fix conflicts --- .../delivery_note/test_delivery_note.py | 48 +------------------ erpnext/stock/serial_batch_bundle.py | 22 ++++++++- 2 files changed, 23 insertions(+), 47 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 407e005a1b8..dd9d247902b 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2596,51 +2596,6 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn.per_billed, 100) self.assertEqual(dn.per_returned, 100) -<<<<<<< HEAD -======= - def test_packed_item_serial_no_status(self): - from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle - from erpnext.stock.doctype.item.test_item import make_item - - # test Update Items with product bundle - if not frappe.db.exists("Item", "_Test Product Bundle Item New 1"): - bundle_item = make_item("_Test Product Bundle Item New 1", {"is_stock_item": 0}) - bundle_item.append( - "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} - ) - bundle_item.save(ignore_permissions=True) - - make_item( - "_Packed Item New Sn Item", - {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-PACKED-NEW-.#####"}, - ) - make_product_bundle("_Test Product Bundle Item New 1", ["_Packed Item New Sn Item"], 1) - - make_stock_entry(item="_Packed Item New Sn Item", target="_Test Warehouse - _TC", qty=5, rate=100) - - dn = create_delivery_note( - item_code="_Test Product Bundle Item New 1", - warehouse="_Test Warehouse - _TC", - qty=5, - ) - - dn.reload() - - serial_nos = [] - for row in dn.packed_items: - self.assertTrue(row.serial_and_batch_bundle) - doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) - for row in doc.entries: - status = frappe.db.get_value("Serial No", row.serial_no, "status") - self.assertEqual(status, "Delivered") - serial_nos.append(row.serial_no) - - dn.cancel() - - for row in serial_nos: - status = frappe.db.get_value("Serial No", row, "status") - self.assertEqual(status, "Active") - def test_sales_return_for_product_bundle(self): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return @@ -2685,6 +2640,8 @@ class TestDeliveryNote(FrappeTestCase): qty=5, ) + dn.reload() + serial_batch_map = {} for row in dn.packed_items: self.assertTrue(row.serial_and_batch_bundle) @@ -2762,7 +2719,6 @@ class TestDeliveryNote(FrappeTestCase): serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no], ) ->>>>>>> 1d57bbca11 (test: test case for sales return for product bundle) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 2408c178f2b..589861809d4 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -209,7 +209,20 @@ class SerialBatchBundle: elif sn_doc.has_batch_no and len(sn_doc.entries) == 1: values_to_update["batch_no"] = sn_doc.entries[0].batch_no - frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, values_to_update) + if self.child_doctype == "Packed Item": + name = frappe.db.get_value( + "Packed Item", + { + "parent_detail_docname": sn_doc.voucher_detail_no, + "item_code": self.sle.item_code, + "serial_and_batch_bundle": ("is", "not set"), + }, + "name", + ) + + frappe.db.set_value(self.child_doctype, name, values_to_update) + else: + frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, values_to_update) @property def child_doctype(self): @@ -227,6 +240,13 @@ class SerialBatchBundle: if self.sle.voucher_type == "Asset Repair": child_doctype = "Asset Repair Consumed Item" + if self.sle.voucher_type in ["Delivery Note", "Sales Invoice"] and self.sle.voucher_detail_no: + if ( + frappe.db.get_value(self.sle.voucher_type + " Item", self.sle.voucher_detail_no, "item_code") + != self.sle.item_code + ): + child_doctype = "Packed Item" + return child_doctype def is_rejected_entry(self): From 4370a591830fc2604e27f7118e90687a3240dee9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 9 Oct 2025 19:42:54 +0530 Subject: [PATCH 09/52] fix: consider negative qty in batch qty calculation (cherry picked from commit 912ffc2d64a753b00101799634dda3bad4314007) --- erpnext/stock/doctype/batch/batch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 9955b485c22..37530682db4 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -158,7 +158,9 @@ class Batch(Document): @frappe.whitelist() def recalculate_batch_qty(self): - batches = get_batch_qty(batch_no=self.name, item_code=self.item, for_stock_levels=True) + batches = get_batch_qty( + batch_no=self.name, item_code=self.item, for_stock_levels=True, consider_negative_batches=True + ) batch_qty = 0.0 if batches: for row in batches: From 0776b300e82edd4b8c05bdf4dcb41bca11bb4330 Mon Sep 17 00:00:00 2001 From: Rehan Ansari Date: Thu, 9 Oct 2025 23:40:56 +0530 Subject: [PATCH 10/52] feat: add asset name to Asset Depreciations and Balances report (cherry picked from commit b4cf6a1fb96b7e93bfcab27d8ada9083ad528353) --- .../asset_depreciations_and_balances.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index d7884b3e973..c196d52d744 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -354,7 +354,7 @@ def get_asset_details_for_grouped_by_category(filters): # nosemgrep return frappe.db.sql( f""" - SELECT a.name, + SELECT a.name, a.asset_name, ifnull(sum(case when a.purchase_date < %(from_date)s then case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then a.gross_purchase_amount @@ -583,6 +583,14 @@ def get_columns(filters): "width": 120, } ) + columns.append( + { + "label": _("Asset Name"), + "fieldname": "asset_name", + "fieldtype": "Data", + "width": 140, + } + ) columns += [ { From 1ea6e1db1293726e15ce42bff185059f390236e8 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Thu, 9 Oct 2025 13:49:06 +0530 Subject: [PATCH 11/52] fix: fixed asset register showing opening entries (cherry picked from commit c9d98eb4f0f782f28ac160e29f44058d8ec0206a) --- .../assets/report/fixed_asset_register/fixed_asset_register.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index b66adca1343..857588bd93e 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -319,6 +319,7 @@ def get_asset_value_adjustment_map(filters, finance_book): .select(asset.name.as_("asset"), Sum(gle.debit - gle.credit).as_("adjustment_amount")) .where(gle.account == aca.fixed_asset_account) .where(gle.is_cancelled == 0) + .where(gle.is_opening == "No") .where(company.name == filters.company) .where(asset.docstatus == 1) ) From 97b89da7c70f110ba8e1aadee35808a3592b5da4 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Thu, 9 Oct 2025 15:30:20 +0530 Subject: [PATCH 12/52] Merge pull request #49764 from elshafei-developer/add-employee-name-to-session-user feat: Cache employee name in session data on boot --- erpnext/startup/boot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index 9701a37b6dc..03cbd99f5c4 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -74,6 +74,8 @@ def update_page_info(bootinfo): def bootinfo(bootinfo): if bootinfo.get("user") and bootinfo["user"].get("name"): bootinfo["user"]["employee"] = "" + frappe.session.data.employee = "" employee = frappe.db.get_value("Employee", {"user_id": bootinfo["user"]["name"]}, "name") if employee: bootinfo["user"]["employee"] = employee + frappe.session.data.employee = employee From 1e91c0f5aa50f3028682f4be00be45feb9866095 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:18:30 +0000 Subject: [PATCH 13/52] Merge pull request #50006 from frappe/mergify/bp/version-15-hotfix/pr-49993 fix: incorrect PR status when using set landed cost based on PI rate (backport #49993) --- .../purchase_invoice/test_purchase_invoice.py | 32 +++++++++++++++++++ .../purchase_receipt/purchase_receipt.py | 6 +++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 90654b42d02..f8e12eda182 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2640,6 +2640,38 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + @change_settings( + "Buying Settings", {"maintain_same_rate": 0, "set_landed_cost_based_on_purchase_invoice_rate": 1} + ) + def test_pr_status_rate_adjusted_from_pi(self): + pr = make_purchase_receipt(qty=5, rate=100) + pi = create_purchase_invoice_from_receipt(pr.name) + pi.submit() + pr.reload() + + # Inital check + self.assertEqual(pr.status, "Completed") + + pi.reload() + pi.cancel() + pi = create_purchase_invoice_from_receipt(pr.name) + pi.items[0].rate = 80 + pi.submit() + pr.reload() + + # Test 1 : Adjustment amount is negative + self.assertEqual(pr.status, "Completed") + + pi.reload() + pi.cancel() + pi = create_purchase_invoice_from_receipt(pr.name) + pi.items[0].rate = 120 + pi.submit() + pr.reload() + + # Test 2 : Adjustment amount is positive + self.assertEqual(pr.status, "Completed") + def test_opening_invoice_rounding_adjustment_validation(self): pi = make_purchase_invoice(do_not_save=1) pi.items[0].rate = 99.98 diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index bc7023fbebb..c4f878fe85d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1115,7 +1115,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate buying_settings = frappe.get_single("Buying Settings") over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") - total_amount, total_billed_amount = 0, 0 + total_amount, total_billed_amount, pi_landed_cost_amount = 0, 0, 0 item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) if adjust_incoming_rate: @@ -1155,6 +1155,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate ) * item.qty adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount")) + pi_landed_cost_amount += adjusted_amt item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False) elif amount and item.billed_amt > amount: per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100 @@ -1165,6 +1166,9 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate ) ) + if pi_landed_cost_amount < 0: + total_billed_amount += abs(pi_landed_cost_amount) + percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) pr_doc.db_set("per_billed", percent_billed) From 8020159c14c0cce1d86b6773bcefbfd1dae6d5cc Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 12 Oct 2025 11:11:50 +0530 Subject: [PATCH 14/52] fix: stock ledger adjustment entry (cherry picked from commit 8b6e58d02ab1a7553f5614e578fcf44de4b2d613) --- erpnext/stock/stock_ledger.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index a06412373e2..9f5f296b39e 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -888,9 +888,8 @@ class update_entries_after: sle.stock_value = self.wh_data.stock_value sle.stock_queue = json.dumps(self.wh_data.stock_queue) - if not sle.is_adjustment_entry: - sle.stock_value_difference = stock_value_difference - elif sle.is_adjustment_entry and not self.args.get("sle_id"): + sle.stock_value_difference = stock_value_difference + if sle.is_adjustment_entry and flt(sle.qty_after_transaction, self.flt_precision) == 0: sle.stock_value_difference = ( get_stock_value_difference( sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no From c93fbf3982d7ba035ab2f42db609a087edd917ac Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Mon, 13 Oct 2025 01:35:26 +0530 Subject: [PATCH 15/52] fix: set default roles on role_profile during reinstallation (cherry picked from commit 12c1b8a9102b9cddd589bb4f7a3015dfc478e376) --- erpnext/setup/install.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 9491da7ec3b..b826c52f20e 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -262,6 +262,20 @@ def update_roles(): def create_default_role_profiles(): for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items(): + if frappe.db.exists("Role Profile", role_profile_name): + role_profile = frappe.get_doc("Role Profile", role_profile_name) + existing_roles = [row.role for row in role_profile.roles] + + role_profile.roles = [row for row in role_profile.roles if row.role in roles] + + for role in roles: + if role not in existing_roles: + role_profile.append("roles", {"role": role}) + + role_profile.save(ignore_permissions=True) + + continue + role_profile = frappe.new_doc("Role Profile") role_profile.role_profile = role_profile_name for role in roles: From 557d53a9535397abc76ed735ba0c0d4166012921 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Sat, 11 Oct 2025 12:27:51 +0530 Subject: [PATCH 16/52] fix(deferred revenue): validate service stop date (cherry picked from commit 58203a89f13f8032aec15bfab5f78f99dea2e7ff) --- erpnext/accounts/deferred_revenue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index a88764cf1b2..748a9a14397 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -46,7 +46,8 @@ def validate_service_stop_date(doc): if ( old_stop_dates and old_stop_dates.get(item.name) - and item.service_stop_date != old_stop_dates.get(item.name) + and item.service_stop_date + and getdate(item.service_stop_date) != getdate(old_stop_dates.get(item.name)) ): frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx)) From 5652e926d7fcdc9bb5e759a132d420add9f704ae Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Mon, 13 Oct 2025 13:56:30 +0530 Subject: [PATCH 17/52] perf: add composite indexes to Advance Payment Ledger Entry table (cherry picked from commit 7fcf2770558d274a6547a1fec9ee1bfcb87a7811) --- .../advance_payment_ledger_entry.json | 3 ++- .../advance_payment_ledger_entry.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json index 5ad2479e858..2add704e394 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json @@ -82,7 +82,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-07-29 11:37:42.678556", + "modified": "2025-10-13 13:23:21.205945", "modified_by": "Administrator", "module": "Accounts", "name": "Advance Payment Ledger Entry", @@ -116,6 +116,7 @@ "share": 1 } ], + "read_only": 1, "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py index fa863741d51..599bd2c5e4c 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py @@ -34,3 +34,15 @@ class AdvancePaymentLedgerEntry(Document): and not frappe.flags.is_reverse_depr_entry ): update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None) + + +def on_doctype_update(): + frappe.db.add_index( + "Advance Payment Ledger Entry", + ["against_voucher_type", "against_voucher_no"], + ) + + frappe.db.add_index( + "Advance Payment Ledger Entry", + ["voucher_type", "voucher_no"], + ) From c4cba781246d40b54f07d7b3b61317b6aee656ea Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Mon, 13 Oct 2025 15:15:48 +0530 Subject: [PATCH 18/52] fix: revert unrelated manual modified timestamp change (cherry picked from commit 59bd35c64d5c0e09c2de3eb0ba4e589cc3490e35) --- .../advance_payment_ledger_entry.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json index 2add704e394..35a3196c140 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json @@ -82,7 +82,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-10-13 13:23:21.205945", + "modified": "2025-10-13 15:11:58.300836", "modified_by": "Administrator", "module": "Accounts", "name": "Advance Payment Ledger Entry", @@ -116,7 +116,6 @@ "share": 1 } ], - "read_only": 1, "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", From aaf470cf5c223fd780d0e065a6d3cf478e42510f Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Tue, 7 Oct 2025 12:46:09 +0530 Subject: [PATCH 19/52] fix: preserve address if present (cherry picked from commit 067863810629169698acc7fca5ab69370e247fbf) # Conflicts: # erpnext/public/js/controllers/transaction.js --- erpnext/public/js/controllers/buying.js | 14 ++++++++------ erpnext/public/js/controllers/transaction.js | 9 +++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 1afca307d94..8c0be8042ac 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -171,13 +171,15 @@ erpnext.buying = { shipping_address: this.frm.doc.shipping_address }, callback: (r) => { - this.frm.set_value("billing_address", r.message.primary_address || ""); + if (!this.frm.doc.billing_address) + this.frm.set_value("billing_address", r.message.primary_address || ""); - if (!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return; - this.frm.set_value( - "shipping_address", - r.message.shipping_address || this.frm.doc.shipping_address || "" - ); + if ( + !frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") || + this.frm.doc.shipping_address + ) + return; + this.frm.set_value("shipping_address", r.message.shipping_address || ""); }, }); erpnext.utils.set_letter_head(this.frm) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index ae3b7404f7f..6f74f5c81d1 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1023,6 +1023,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe set_pricing(); } +<<<<<<< HEAD } if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && @@ -1034,6 +1035,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe set_party_account(set_pricing); }); } +======= + if ( + frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && + ["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doctype) && + !this.frm.doc.shipping_address + ) { + let is_drop_ship = me.frm.doc.items.some((item) => item.delivered_by_supplier); +>>>>>>> 0678638106 (fix: preserve address if present) } else { set_party_account(set_pricing); From 5d1aa4050d37304247a4f00a5a7e6e75c06b6e2f Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Mon, 13 Oct 2025 16:36:36 +0530 Subject: [PATCH 20/52] chore: resolve conflicts --- erpnext/public/js/controllers/transaction.js | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 6f74f5c81d1..66c54ec6639 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1022,28 +1022,20 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } else { set_pricing(); } + }; -<<<<<<< HEAD - } - - if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && - ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) { - let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier); - - if (!is_drop_ship) { - erpnext.utils.get_shipping_address(this.frm, function() { - set_party_account(set_pricing); - }); - } -======= if ( frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && ["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doctype) && !this.frm.doc.shipping_address ) { let is_drop_ship = me.frm.doc.items.some((item) => item.delivered_by_supplier); ->>>>>>> 0678638106 (fix: preserve address if present) + if (!is_drop_ship) { + erpnext.utils.get_shipping_address(this.frm, function() { + set_party_account(set_pricing); + }); + } } else { set_party_account(set_pricing); } From 467fcea728f0c70ca001d83acdbb45bdedc8c605 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 13 Oct 2025 16:16:01 +0530 Subject: [PATCH 21/52] fix: enhance sub-assembly item handling in raw material request calculations (cherry picked from commit f912c8419a9dab8e9a564b07c8e713e1a60167f0) # Conflicts: # erpnext/manufacturing/doctype/production_plan/production_plan.py --- .../production_plan/production_plan.py | 19 ++++++++ .../production_plan/test_production_plan.py | 44 +++++++++++++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index d44f48a08ed..abbbab6f4ce 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1543,6 +1543,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d include_safety_stock = doc.get("include_safety_stock") so_item_details = frappe._dict() + existing_sub_assembly_items = set() sub_assembly_items = defaultdict(int) if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"): @@ -1572,6 +1573,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx"))) if bom_no: +<<<<<<< HEAD if data.get("include_exploded_items") and doc.get("skip_available_sub_assembly_item"): item_details = {} if doc.get("sub_assembly_items"): @@ -1584,6 +1586,22 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d sub_assembly_items, planned_qty=planned_qty, ) +======= + if ( + data.get("include_exploded_items") + and doc.get("skip_available_sub_assembly_item") + and doc.get("sub_assembly_items") + ): + item_details = get_raw_materials_of_sub_assembly_items( + existing_sub_assembly_items, + item_details, + company, + bom_no, + include_non_stock_items, + sub_assembly_items, + planned_qty=planned_qty, + ) +>>>>>>> f912c8419a (fix: enhance sub-assembly item handling in raw material request calculations) elif data.get("include_exploded_items") and include_subcontracted_items: # fetch exploded items from BOM @@ -1955,6 +1973,7 @@ def get_raw_materials_of_sub_assembly_items( sub_assembly_items, planned_qty=planned_qty, ) + existing_sub_assembly_items.add(item.item_code) else: if not item.conversion_factor and item.purchase_uom: item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 3c9e29b2811..28c9e63bc1f 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1637,11 +1637,17 @@ class TestProductionPlan(FrappeTestCase): def test_calculation_of_sub_assembly_items(self): make_item("Sub Assembly Item ", properties={"is_stock_item": 1}) + make_item("Sub Assembly Item 2", properties={"is_stock_item": 1}) make_item("RM Item 1", properties={"is_stock_item": 1}) make_item("RM Item 2", properties={"is_stock_item": 1}) + make_item("_Test FG Item 3", properties={"is_stock_item": 1}) + make_item("_Test FG Item 4", properties={"is_stock_item": 1}) make_bom(item="Sub Assembly Item", raw_materials=["RM Item 1", "RM Item 2"]) + make_bom(item="Sub Assembly Item 2", raw_materials=["RM Item 2"]) make_bom(item="_Test FG Item", raw_materials=["Sub Assembly Item", "RM Item 1"]) make_bom(item="_Test FG Item 2", raw_materials=["Sub Assembly Item"]) + make_bom(item="_Test FG Item 3", raw_materials=["RM Item 1"]) + make_bom(item="_Test FG Item 4", raw_materials=["Sub Assembly Item 2"]) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -1677,12 +1683,39 @@ class TestProductionPlan(FrappeTestCase): "warehouse": "_Test Warehouse - _TC", }, ) + # Assembly item with similar RM item + plan.append( + "po_items", + { + "use_multi_level_bom": 1, + "item_code": "_Test FG Item 3", + "bom_no": frappe.db.get_value("Item", "_Test FG Item 3", "default_bom"), + "planned_qty": 10, + "planned_start_date": now_datetime(), + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + ) + # Sub-assembly item with similar RM item + plan.append( + "po_items", + { + "use_multi_level_bom": 1, + "item_code": "_Test FG Item 4", + "bom_no": frappe.db.get_value("Item", "_Test FG Item 4", "default_bom"), + "planned_qty": 10, + "planned_start_date": now_datetime(), + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + ) plan.save() plan.get_sub_assembly_items() - self.assertEqual(plan.sub_assembly_items[0].qty, 20) - self.assertEqual(plan.sub_assembly_items[1].qty, 50) + self.assertEqual(plan.sub_assembly_items[0].qty, 20) # Sub Assembly For FG 1 + self.assertEqual(plan.sub_assembly_items[1].qty, 50) # Sub Assembly For FG 2 + self.assertEqual(plan.sub_assembly_items[2].qty, 10) # Sub Assembly For FG 4 from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_items_for_material_requests, @@ -1690,8 +1723,11 @@ class TestProductionPlan(FrappeTestCase): mr_items = get_items_for_material_requests(plan.as_dict()) - self.assertEqual(mr_items[0].get("quantity"), 80) - self.assertEqual(mr_items[1].get("quantity"), 70) + # RM Item 1 (FG1 (100 + 100) + FG2 (50) + FG3 (10) - 90 in stock - 80 sub assembly stock) + self.assertEqual(mr_items[0].get("quantity"), 90) + + # RM Item 2 (FG1 (100) + FG2 (50) + FG4 (10) - 80 sub assembly stock) + self.assertEqual(mr_items[1].get("quantity"), 80) def test_production_plan_for_partial_sub_assembly_items(self): from erpnext.controllers.status_updater import OverAllowanceError From 75d14985e0455c28c2e1feaaa5bff19046af9bfd Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 13 Oct 2025 19:45:04 +0530 Subject: [PATCH 22/52] chore: resolve conflicts --- .../production_plan/production_plan.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index abbbab6f4ce..16eb29b085a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1573,12 +1573,11 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx"))) if bom_no: -<<<<<<< HEAD if data.get("include_exploded_items") and doc.get("skip_available_sub_assembly_item"): item_details = {} if doc.get("sub_assembly_items"): item_details = get_raw_materials_of_sub_assembly_items( - so_item_details[doc.get("sales_order")].keys() if so_item_details else [], + existing_sub_assembly_items, item_details, company, bom_no, @@ -1586,22 +1585,6 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d sub_assembly_items, planned_qty=planned_qty, ) -======= - if ( - data.get("include_exploded_items") - and doc.get("skip_available_sub_assembly_item") - and doc.get("sub_assembly_items") - ): - item_details = get_raw_materials_of_sub_assembly_items( - existing_sub_assembly_items, - item_details, - company, - bom_no, - include_non_stock_items, - sub_assembly_items, - planned_qty=planned_qty, - ) ->>>>>>> f912c8419a (fix: enhance sub-assembly item handling in raw material request calculations) elif data.get("include_exploded_items") and include_subcontracted_items: # fetch exploded items from BOM From 50266d3b6bb899ec9536e6909972a42e164563f4 Mon Sep 17 00:00:00 2001 From: thomasantony12 Date: Sun, 12 Oct 2025 11:18:03 +0530 Subject: [PATCH 23/52] fix: Batch ordering based on the method mentioned in settings (cherry picked from commit 7fa800b87449fea05306f2ae31e8e03801e9ead8) --- erpnext/stock/doctype/batch/batch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 37530682db4..e81e9a2ff5a 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -253,6 +253,7 @@ def get_batch_qty( get_auto_batch_nos, ) + stock_settings = frappe.get_cached_doc("Stock Settings") batchwise_qty = defaultdict(float) kwargs = frappe._dict( { @@ -262,6 +263,7 @@ def get_batch_qty( "posting_date": posting_date, "posting_time": posting_time, "batch_no": batch_no, + "based_on": stock_settings.pick_serial_and_batch_based_on, "ignore_voucher_nos": ignore_voucher_nos, "for_stock_levels": for_stock_levels, "consider_negative_batches": consider_negative_batches, From e23616f9ea0711137dd2ce3db054d317f266a6f9 Mon Sep 17 00:00:00 2001 From: thomasantony12 Date: Sun, 12 Oct 2025 18:05:29 +0530 Subject: [PATCH 24/52] chore: use get_single_value instead of get_cached_doc (cherry picked from commit fab7f9ee53ef1db331269c4ff087bced16820c3f) --- erpnext/stock/doctype/batch/batch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index e81e9a2ff5a..6b3aa422772 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -253,7 +253,7 @@ def get_batch_qty( get_auto_batch_nos, ) - stock_settings = frappe.get_cached_doc("Stock Settings") + based_on = frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on") batchwise_qty = defaultdict(float) kwargs = frappe._dict( { @@ -263,7 +263,7 @@ def get_batch_qty( "posting_date": posting_date, "posting_time": posting_time, "batch_no": batch_no, - "based_on": stock_settings.pick_serial_and_batch_based_on, + "based_on": based_on, "ignore_voucher_nos": ignore_voucher_nos, "for_stock_levels": for_stock_levels, "consider_negative_batches": consider_negative_batches, From 5603467cee69b51b3d8017cba95507afe833ec97 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 13 Oct 2025 19:26:34 +0530 Subject: [PATCH 25/52] refactor: move value inline (cherry picked from commit 1717a7c983d79602da02db5ad125288076400870) --- erpnext/stock/doctype/batch/batch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 6b3aa422772..699b7a9562d 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -253,7 +253,6 @@ def get_batch_qty( get_auto_batch_nos, ) - based_on = frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on") batchwise_qty = defaultdict(float) kwargs = frappe._dict( { @@ -263,7 +262,7 @@ def get_batch_qty( "posting_date": posting_date, "posting_time": posting_time, "batch_no": batch_no, - "based_on": based_on, + "based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"), "ignore_voucher_nos": ignore_voucher_nos, "for_stock_levels": for_stock_levels, "consider_negative_batches": consider_negative_batches, From b7c2405113fbc19dd560f3c26424576a651bf4bc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 00:36:15 +0530 Subject: [PATCH 26/52] fix(Supplier Quotation Comparison): add a missing translate function (backport #49497) (#50055) fix(Supplier Quotation Comparison): add a missing translate function (#49497) * Update supplier_quotation_comparison.py * refactor: text cleaning (cherry picked from commit 6cacead7263c092fbb05d26c4e2ac0144a286efe) Co-authored-by: El-Shafei H. --- .../supplier_quotation_comparison.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index 20267e9ae10..db93a3d7e79 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -284,15 +284,15 @@ def get_columns(filters): def get_message(): - return """ - Valid till :    + return f""" + {_("Valid Till")}:   - Expires in a week or less + {_("Expires in a week or less")}    - Expires today / Already Expired + {_("Expires today or already expired")} """ From 957b47f351abd792ec82132f500034ad7937ee2d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:36:15 +0530 Subject: [PATCH 27/52] fix: skip party validation for payroll & it's journal & GL entry submission (backport #49638) (#49826) * fix: skip party validation for payroll & it's journal & GL entry submission (#49638) * fix: skip validation for manual je & gl submission linked with payroll entry * refactor: change condition * fix: add checkbox in jouranl entry account and passed it true from payroll to skip party validation * refactor: add checkbox to skip party validation in journal entry (cherry picked from commit 35474d997dad7353fe2adb47dee059e04122aa5c) # Conflicts: # erpnext/accounts/doctype/journal_entry/journal_entry.json # erpnext/accounts/doctype/journal_entry/journal_entry.py * fix: conflicts raised because of cherry pick while backporting * fix: conflicts --------- Co-authored-by: Raheel Khan --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 4 ++-- .../doctype/journal_entry/journal_entry.json | 11 ++++++++++- .../accounts/doctype/journal_entry/journal_entry.py | 13 ++++++++++--- .../journal_entry_account.json | 2 +- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 510e6b1dcbc..0f3216ae772 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -131,8 +131,8 @@ class GLEntry(Document): if not self.is_cancelled and not (self.party_type and self.party): account_type = frappe.get_cached_value("Account", self.account, "account_type") - # skipping validation for payroll entry creation in case party is not required - if not frappe.flags.party_not_required_for_receivable_payable: + + if not frappe.flags.party_not_required: # skipping validation if party is not required if account_type == "Receivable": frappe.throw( _("{0} {1}: Customer is required against Receivable account {2}").format( diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 4184bdaabb9..4f904d87998 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -59,6 +59,7 @@ "addtional_info", "mode_of_payment", "payment_order", + "party_not_required", "column_break3", "is_opening", "stock_entry", @@ -543,6 +544,14 @@ "label": "Is System Generated", "no_copy": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "party_not_required", + "fieldtype": "Check", + "hidden": 1, + "label": "Party Not Required", + "no_copy": 1 } ], "icon": "fa fa-file-text", @@ -557,7 +566,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2024-07-18 15:32:29.413598", + "modified": "2025-09-29 13:05:46.982277", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 82132ce70be..9d3c9eb501e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -72,6 +72,7 @@ class JournalEntry(AccountsController): multi_currency: DF.Check naming_series: DF.Literal["ACC-JV-.YYYY.-"] paid_loan: DF.Data | None + party_not_required: DF.Check pay_to_recd_from: DF.Data | None payment_order: DF.Link | None posting_date: DF.Date @@ -543,10 +544,10 @@ class JournalEntry(AccountsController): for d in self.get("accounts"): account_type = frappe.get_cached_value("Account", d.account, "account_type") - # skipping validation for payroll entry creation - skip_validation = frappe.flags.party_not_required_for_receivable_payable if account_type in ["Receivable", "Payable"]: - if not (d.party_type and d.party) and not skip_validation: + if ( + not (d.party_type and d.party) and not self.party_not_required + ): # skipping validation if party_not_required is passed via payroll entry frappe.throw( _( "Row {0}: Party Type and Party is required for Receivable / Payable account {1}" @@ -1139,6 +1140,11 @@ class JournalEntry(AccountsController): } ) + # set flag to skip party validation + account_type = frappe.get_cached_value("Account", d.account, "account_type") + if account_type in ["Receivable", "Payable"] and self.party_not_required: + frappe.flags.party_not_required = True + gl_map.append( self.get_gl_dict( row, @@ -1166,6 +1172,7 @@ class JournalEntry(AccountsController): merge_entries=merge_entries, update_outstanding=update_outstanding, ) + frappe.flags.party_not_required = False if cancel: cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 45c2b4ce764..5d31c627381 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -286,7 +286,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2025-07-25 04:45:28.117715", + "modified": "2025-09-29 13:01:48.916517", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", From db93e50f16da1624ff8108d5db8af3626a5438b0 Mon Sep 17 00:00:00 2001 From: "matteo.arosti" Date: Tue, 14 Oct 2025 00:27:27 +0200 Subject: [PATCH 28/52] fix: warehouse source reference in production report (cherry picked from commit 451651e350f014b77f20d0787c0069ce380ff72f) --- .../production_planning_report.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py index 63af3e5cbe6..9867db0dd1c 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py @@ -113,6 +113,13 @@ class ProductionPlanReport: self.orders = query.run(as_dict=True) def get_raw_materials(self): + """Retrieve raw materials and source warehouses for production orders. + + This method collects BOM or Work Order items depending on the selected + filter and updates `self.raw_materials_dict`, `self.warehouses`, + and `self.item_codes` accordingly. + """ + if not self.orders: return self.warehouses = [d.warehouse for d in self.orders] @@ -135,7 +142,7 @@ class ProductionPlanReport: ) or [] ) - self.warehouses.extend([d.source_warehouse for d in raw_materials]) + self.warehouses.extend([d.warehouse for d in raw_materials]) else: bom_nos = [] From 20c2809437494a72111c01e5d262ca7d50dd723a Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Wed, 3 Sep 2025 16:53:20 +0530 Subject: [PATCH 29/52] fix(production plan): filter sales orders by item (cherry picked from commit bfff945fb130127c63fdfa9bb2a960da93e20c83) --- .../manufacturing/doctype/production_plan/production_plan.js | 1 + .../manufacturing/doctype/production_plan/production_plan.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 0c535af5be2..a8a7769b2de 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -24,6 +24,7 @@ frappe.ui.form.on("Production Plan", { query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query", filters: { company: frm.doc.company, + item_code: frm.doc.item_code, }, }; }); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 16eb29b085a..37cd5d771e5 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1994,6 +1994,9 @@ def sales_order_query(doctype=None, txt=None, searchfield=None, start=None, page if filters.get("sales_orders"): query = query.where(so_table.name.isin(filters.get("sales_orders"))) + if filters.get("item_code"): + query = query.where(table.item_code == filters.get("item_code")) + if txt: query = query.where(table.parent.like(f"%{txt}%")) From e9d71e013ae110ce49f38fdbf0edffb8d94a2a67 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 14 Oct 2025 11:48:42 +0530 Subject: [PATCH 30/52] fix: do reposting of first transfer entry based on item-wh combination (cherry picked from commit 2f25b445abdcdde3a9232ed7e8bbf91e3c8732f1) --- erpnext/stock/stock_ledger.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9f5f296b39e..a7a319e296e 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -732,6 +732,10 @@ class update_entries_after: elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry( dependant_sle.voucher_no ): + if self.distinct_item_warehouses[key].get("transfer_entry_to_repost"): + return + + val["transfer_entry_to_repost"] = True self.distinct_item_warehouses[key] = val self.new_items_found = True From 2184a28e9143ca2da17c8120427ceaafdd216233 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 12 Oct 2025 13:10:39 +0530 Subject: [PATCH 31/52] fix: reset raw materials considering not available batches (cherry picked from commit ec1636db12579e6d4de38c4a404ed2b6af37f78a) --- erpnext/controllers/subcontracting_controller.py | 3 ++- .../doctype/subcontracting_receipt/subcontracting_receipt.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 4666c953fbe..36341a090dc 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -637,7 +637,8 @@ class SubcontractingController(StockController): if use_serial_batch_fields: rm_obj.use_serial_batch_fields = 1 - self.__set_batch_nos(bom_item, item_row, rm_obj, qty) + if not self.flags.get("reset_raw_materials"): + self.__set_batch_nos(bom_item, item_row, rm_obj, qty) if self.doctype == "Subcontracting Receipt": if not use_serial_batch_fields: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index f5dd1c4f16c..99a6abc8b91 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -195,6 +195,7 @@ class SubcontractingReceipt(SubcontractingController): @frappe.whitelist() def reset_raw_materials(self): self.supplied_items = [] + self.flags.reset_raw_materials = True self.create_raw_materials_supplied() def validate_closed_subcontracting_order(self): From e4fd49e9910a5ee51242e79638c130f6dd226f99 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 14 Oct 2025 13:02:56 +0530 Subject: [PATCH 32/52] fix: performance issue by adding index (cherry picked from commit 1afc75b15a72d3ea20da66cb0a572af2215eb6fb) # Conflicts: # erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json # erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json --- .../purchase_invoice_item/purchase_invoice_item.json | 9 +++++++-- .../purchase_receipt_item/purchase_receipt_item.json | 12 +++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 69d5da7b9a3..4db7d9efcd8 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -912,7 +912,8 @@ "label": "Rejected Serial and Batch Bundle", "no_copy": 1, "options": "Serial and Batch Bundle", - "print_hide": 1 + "print_hide": 1, + "search_index": 1 }, { "fieldname": "wip_composite_asset", @@ -983,7 +984,11 @@ "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2025-03-12 16:33:13.453290", +======= + "modified": "2025-10-14 13:00:54.441511", +>>>>>>> 1afc75b15a (fix: performance issue by adding index) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -993,4 +998,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 5f8a9b58e91..0747fba0035 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -778,7 +778,8 @@ "fieldtype": "Data", "hidden": 1, "label": "Material Request Item", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "expense_account", @@ -1038,7 +1039,8 @@ "fieldtype": "Link", "label": "Rejected Serial and Batch Bundle", "no_copy": 1, - "options": "Serial and Batch Bundle" + "options": "Serial and Batch Bundle", + "search_index": 1 }, { "depends_on": "eval:doc.use_serial_batch_fields === 0", @@ -1147,7 +1149,11 @@ "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2025-03-12 17:10:43.780622", +======= + "modified": "2025-10-14 12:58:20.384056", +>>>>>>> 1afc75b15a (fix: performance issue by adding index) "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", @@ -1158,4 +1164,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} From 967ee7841545fe81cbeebd36f94a9f8f091cde16 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 14 Oct 2025 13:33:24 +0530 Subject: [PATCH 33/52] chore: fix conflicts --- .../purchase_invoice_item/purchase_invoice_item.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 4db7d9efcd8..7acb36eca93 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -984,11 +984,7 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2025-03-12 16:33:13.453290", -======= - "modified": "2025-10-14 13:00:54.441511", ->>>>>>> 1afc75b15a (fix: performance issue by adding index) + "modified": "2025-10-14 13:01:54.441511", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", From 21a972ad95228fa79925e42213381df7d6bc4364 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Fri, 10 Oct 2025 18:47:15 +0530 Subject: [PATCH 34/52] fix(stock-reconciliation): include inventory dimensions in duplicate validation (cherry picked from commit 4b21c2cc4631bd63074f2a0f796160577a7d92d9) --- .../doctype/stock_reconciliation/stock_reconciliation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index bd0a0beceef..9140599e7ba 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -589,6 +589,10 @@ class StockReconciliation(StockController): if row.get(field): key.append(row.get(field)) + for dimension in get_inventory_dimensions(): + if row.get(dimension.get("fieldname")): + key.append(row.get(dimension.get("fieldname"))) + if key in item_warehouse_combinations: self.validation_messages.append( _get_msg(row_num, _("Same item and warehouse combination already entered.")) From 9854dedc06e96085866e74f728a4fd733c6c253d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 14 Oct 2025 12:01:46 +0530 Subject: [PATCH 35/52] fix: duplicate serial nos (cherry picked from commit c95465cba13f05d3ee95f8693fe5650a361c427c) --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index ca3e7ec1ad7..977a41f0369 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 @@ -294,7 +294,7 @@ class SerialandBatchBundle(Document): } ) - if self.returned_against and self.docstatus == 1: + if (self.returned_against or self.voucher_type == "Stock Reconciliation") and self.docstatus == 1: kwargs["ignore_voucher_detail_no"] = self.voucher_detail_no if self.docstatus == 1: @@ -2677,7 +2677,10 @@ def get_stock_ledgers_for_serial_nos(kwargs): else: query = query.where(stock_ledger_entry[field] == kwargs.get(field)) - if kwargs.voucher_no: + if kwargs.ignore_voucher_detail_no: + query = query.where(stock_ledger_entry.voucher_detail_no != kwargs.ignore_voucher_detail_no) + + elif kwargs.voucher_no: query = query.where(stock_ledger_entry.voucher_no != kwargs.voucher_no) return query.run(as_dict=True) From 61f7309695ca05342423d3df552b9f2fcc28756c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 14 Oct 2025 13:33:56 +0530 Subject: [PATCH 36/52] chore: fix conflicts --- .../purchase_receipt_item/purchase_receipt_item.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 0747fba0035..463544a9952 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -1149,11 +1149,7 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2025-03-12 17:10:43.780622", -======= - "modified": "2025-10-14 12:58:20.384056", ->>>>>>> 1afc75b15a (fix: performance issue by adding index) + "modified": "2025-10-14 12:59:20.384056", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", From af3d7ef3001bacadafa888b7851d65c803e31bf9 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Tue, 14 Oct 2025 13:19:37 +0530 Subject: [PATCH 37/52] fix(stock-entry): fetch empty batch for finished item (cherry picked from commit 74a7ddf66d6fb4f4139dd8a89e6a8a5ca4e0e06d) --- erpnext/public/js/utils/serial_no_batch_selector.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index e02d7a3d785..996ee949a13 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -457,7 +457,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { (["Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype) && !this.frm.doc.is_return) || (this.frm.doc.doctype === "Stock Entry" && - this.frm.doc.purpose === "Material Receipt") + (this.frm.doc.purpose === "Material Receipt" || + (this.frm.doc.purpose === "Manufacture" && this.item.is_finished_item))) ) { is_inward = true; } From c5dc81064283c9e85ff279d333da4f36499baac1 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:14:39 +0530 Subject: [PATCH 38/52] fix: swap warehouse labels for return entry (cherry picked from commit f0c3f0d0be492ecd03c5994a2f7d3b9e3e435fa5) --- erpnext/public/js/utils/sales_common.js | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index ffff536f068..bf4ef8666cd 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -109,6 +109,7 @@ erpnext.sales_common = { ); this.toggle_editable_price_list_rate(); + this.change_warehouse_labels_for_return(); } company() { @@ -500,6 +501,33 @@ erpnext.sales_common = { this.frm.set_value("discount_amount", 0); this.frm.set_value("additional_discount_percentage", 0); } + + is_return() { + let reset = !this.frm.doc.is_return; + this.change_warehouse_labels_for_return(reset); + } + + change_warehouse_labels_for_return(reset) { + // swap source and target warehouse labels for return + let source_warehouse_label = __("Source Warehouse"); + let target_warehouse_label = __("Set Target Warehouse"); + + if (this.frm.doc.doctype == "Delivery Note") { + source_warehouse_label = __("Set Source Warehouse"); + } + + if (reset) { + // reset to original labels + this.frm.set_df_property("set_warehouse", "label", source_warehouse_label); + this.frm.set_df_property("set_target_warehouse", "label", target_warehouse_label); + return; + } + + if (this.frm.doc.is_return) { + this.frm.set_df_property("set_warehouse", "label", target_warehouse_label); + this.frm.set_df_property("set_target_warehouse", "label", source_warehouse_label); + } + } }; }, }; From 01fe1c658ec9e79aaaa49256ebbbbe923ee97bdf Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 14 Oct 2025 13:55:14 +0530 Subject: [PATCH 39/52] chore: replace broken links with correct ones (cherry picked from commit 11be07086f18425d579be9cb16c2c8fdb03f780f) --- .github/CONTRIBUTING.md | 2 +- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 4 ++-- README.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 1cf9a5bd9b5..9f040b4e62e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -6,7 +6,7 @@ Feature requests are also a great way to take the product forward. New ideas can When you are raising an Issue, you should keep a few things in mind. Remember that the developer does not have access to your machine so you must give all the information you can while raising an Issue. If you are suggesting a feature, you should be very clear about what you want. -The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.erpnext.com](https://discuss.erpnext.com). +The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.frappe.io](https://discuss.frappe.io/c/erpnext/6). ### Reply and Closing Policy diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 4d61f1fb943..8b13b00c042 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -9,7 +9,7 @@ body: Welcome to ERPNext issue tracker! Before creating an issue, please heed the following: 1. This tracker should only be used to report bugs and request features / enhancements to ERPNext - - For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com) + - For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.frappe.io/c/erpnext/6) - For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly. 2. When making a bug report, make sure you provide all required information. The easier it is for maintainers to reproduce, the faster it'll be fixed. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 26bb7ab280c..7b58c0d3a3a 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Community Forum - url: https://discuss.erpnext.com/ + url: https://discuss.frappe.io/c/erpnext/6 about: For general QnA, discussions and community help. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 607e42d1b49..38d881e24ae 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -11,7 +11,7 @@ assignees: '' Welcome to ERPNext issue tracker! Before creating an issue, please heed the following: 1. This tracker should only be used to report bugs and request features / enhancements to ERPNext - - For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com + - For questions and general support, checkout the manual https://docs.erpnext.com or use https://discuss.frappe.io/c/erpnext/6 2. Use the search function before creating a new issue. Duplicates will be closed and directed to the original discussion. 3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen. @@ -21,7 +21,7 @@ Please keep in mind that we get many many requests and we can't possibly work on If you're in urgent need to a feature, please try the following channels to get paid developments done quickly: 1. Certified ERPNext partners: https://erpnext.com/partners -2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5 +2. Developer community on ERPNext forums: https://discuss.frappe.io/c/framework/5 3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps --> diff --git a/README.md b/README.md index 3b703d97831..021b1a3ab5c 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB 1. [Frappe School](https://school.frappe.io) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community. 2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext. -3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers. +3. [Discussion Forum](https://discuss.frappe.io/c/erpnext/6) - Engage with community of ERPNext users and service providers. 4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users. From b426b8c07f92a06a1c2f62a0fc4776755b2cb205 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Fri, 10 Oct 2025 13:31:57 +0530 Subject: [PATCH 40/52] fix: hide sales invoice creation for fully returned delivery notes (cherry picked from commit 1f831d87834aeb455f6ecd612f4b4b7671a675f6) --- erpnext/stock/doctype/delivery_note/delivery_note.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 0cd4e24ff93..527d672cc6a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -334,6 +334,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends ( if ( doc.docstatus == 1 && !doc.is_return && + doc.per_returned != 100 && doc.status != "Closed" && flt(doc.per_billed) < 100 && frappe.model.can_create("Sales Invoice") From 9aa9b181e5b44eed49ccb0b1fa3428246febcea8 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Mon, 13 Oct 2025 11:22:29 +0530 Subject: [PATCH 41/52] fix: add GROUP BY for dn_detail and convert SQL query to QB (cherry picked from commit fd9167f2af2ff74a71c81f9b71372653f909365f) # Conflicts: # erpnext/stock/doctype/delivery_note/delivery_note.py --- .../doctype/delivery_note/delivery_note.py | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 6cadbdc5e47..da80b829f4b 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -10,6 +10,8 @@ from frappe.contacts.doctype.address.address import get_company_address from frappe.desk.notifications import clear_doctype_notifications from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values +from frappe.query_builder import DocType +from frappe.query_builder.functions import Abs, Sum from frappe.utils import cint, flt from erpnext.accounts.party import get_due_date @@ -790,23 +792,27 @@ def get_list_context(context=None): def get_invoiced_qty_map(delivery_note): """returns a map: {dn_detail: invoiced_qty}""" - invoiced_qty_map = {} + sii = DocType("Sales Invoice Item") - for dn_detail, qty in frappe.db.sql( - """select dn_detail, qty from `tabSales Invoice Item` - where delivery_note=%s and docstatus=1""", - delivery_note, - ): - if not invoiced_qty_map.get(dn_detail): - invoiced_qty_map[dn_detail] = 0 - invoiced_qty_map[dn_detail] += qty + invoiced_qty_map = frappe._dict( + ( + frappe.qb.from_(sii) + .select(sii.dn_detail, Sum(sii.qty).as_("qty")) + .where((sii.delivery_note == delivery_note) & (sii.docstatus == 1)) + .groupby(sii.dn_detail) + ).run() + ) return invoiced_qty_map def get_returned_qty_map(delivery_note): """returns a map: {so_detail: returned_qty}""" + dn = DocType("Delivery Note") + dni = DocType("Delivery Note Item") + returned_qty_map = frappe._dict( +<<<<<<< HEAD frappe.db.sql( """select dn_item.dn_detail, sum(abs(dn_item.qty)) as qty from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn @@ -819,6 +825,21 @@ def get_returned_qty_map(delivery_note): """, delivery_note, ) +======= + ( + frappe.qb.from_(dni) + .join(dn) + .on(dn.name == dni.parent) + .select(dni.dn_detail, Sum(Abs(dni.qty)).as_("qty")) + .where( + (dn.docstatus == 1) + & (dn.is_return == 1) + & (dn.return_against == delivery_note) + & (dni.qty <= 0) + ) + .groupby(dni.dn_detail) + ).run() +>>>>>>> fd9167f2af (fix: add GROUP BY for dn_detail and convert SQL query to QB) ) return returned_qty_map From 7d533b7086e498229f79c54c3759f12cc90fc53b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 14 Oct 2025 14:57:28 +0530 Subject: [PATCH 42/52] chore: resolve conflicts --- .../stock/doctype/delivery_note/delivery_note.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index da80b829f4b..d2a15c2bd66 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -812,20 +812,6 @@ def get_returned_qty_map(delivery_note): dni = DocType("Delivery Note Item") returned_qty_map = frappe._dict( -<<<<<<< HEAD - frappe.db.sql( - """select dn_item.dn_detail, sum(abs(dn_item.qty)) as qty - from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn - where dn.name = dn_item.parent - and dn.docstatus = 1 - and dn.is_return = 1 - and dn.return_against = %s - and dn_item.qty <= 0 - group by dn_item.item_code - """, - delivery_note, - ) -======= ( frappe.qb.from_(dni) .join(dn) @@ -839,7 +825,6 @@ def get_returned_qty_map(delivery_note): ) .groupby(dni.dn_detail) ).run() ->>>>>>> fd9167f2af (fix: add GROUP BY for dn_detail and convert SQL query to QB) ) return returned_qty_map From 6d5f2b50241d2f4e5af8e04821fc26436b788ab6 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 14 Oct 2025 15:19:07 +0530 Subject: [PATCH 43/52] fix: skip auto-cancel of depreciation for components during asset capitalization --- erpnext/assets/doctype/asset/depreciation.py | 3 ++- .../doctype/asset_capitalization/asset_capitalization.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index b06244fd344..c00fc86d4b2 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -507,7 +507,8 @@ def depreciate_asset(asset_doc, date, notes): make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date) asset_doc.reload() - cancel_depreciation_entries(asset_doc, date) + if not frappe.flags.is_composite_component: + cancel_depreciation_entries(asset_doc, date) @erpnext.allow_regional diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index fc0625edf9b..7c520d9d352 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -492,6 +492,7 @@ class AssetCapitalization(StockController): asset = frappe.get_doc("Asset", item.asset) if asset.calculate_depreciation: + frappe.flags.is_composite_component = True notes = _( "This schedule was created when Asset {0} was consumed through Asset Capitalization {1}." ).format( @@ -499,6 +500,7 @@ class AssetCapitalization(StockController): get_link_to_form(self.doctype, self.get("name")), ) depreciate_asset(asset, self.posting_date, notes) + frappe.flags.is_composite_component = False asset.reload() fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( From 02ebde43bfaae627fcd0283ce9f5feff3b6a5f78 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 14 Oct 2025 16:32:32 +0530 Subject: [PATCH 44/52] refactor: Ensure flag cleanup with try-finally to prevent state corruption --- .../asset_capitalization.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 7c520d9d352..ab3bb9ab406 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -493,15 +493,17 @@ class AssetCapitalization(StockController): if asset.calculate_depreciation: frappe.flags.is_composite_component = True - notes = _( - "This schedule was created when Asset {0} was consumed through Asset Capitalization {1}." - ).format( - get_link_to_form(asset.doctype, asset.name), - get_link_to_form(self.doctype, self.get("name")), - ) - depreciate_asset(asset, self.posting_date, notes) - frappe.flags.is_composite_component = False - asset.reload() + try: + notes = _( + "This schedule was created when Asset {0} was consumed through Asset Capitalization {1}." + ).format( + get_link_to_form(asset.doctype, asset.name), + get_link_to_form(self.doctype, self.get("name")), + ) + depreciate_asset(asset, self.posting_date, notes) + asset.reload() + finally: + frappe.flags.is_composite_component = False fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( asset, From 1fc21d60c60a62a3431da82d16624c120740cd2c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 14 Oct 2025 16:29:20 +0530 Subject: [PATCH 45/52] fix: negative error not throw for backdated entry (cherry picked from commit 88a947ff4e8ed023e8b6507678184f0973b5cf4c) --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 11 +---------- erpnext/stock/serial_batch_bundle.py | 6 +++--- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 08ce705f26f..cde5be4d6ee 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1323,18 +1323,9 @@ class TestStockEntry(FrappeTestCase): posting_date="2021-07-02", # Illegal SE purpose="Material Transfer", ), - dict( - item_code=item_code, - qty=2, - from_warehouse=warehouse_names[0], - to_warehouse=warehouse_names[1], - batch_no=batch_no, - posting_date="2021-07-02", # Illegal SE - purpose="Material Transfer", - ), ] - self.assertRaises(frappe.ValidationError, create_stock_entries, sequence_of_entries) + self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_future_negative_sle_batch(self): diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 589861809d4..bea1cb7386e 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1385,12 +1385,12 @@ def get_batch_current_qty(batch): def throw_negative_batch_validation(batch_no, qty): - frappe.msgprint( + # This validation is important for backdated stock transactions with batch items + frappe.throw( _( "The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry." ).format(bold(get_link_to_form("Batch", batch_no)), bold(qty)), - title=_("Warning!"), - indicator="orange", + title=_("Negative Stock Error"), ) From 38efd5cb0b2e0a689ea1ee9f63c87705cb255845 Mon Sep 17 00:00:00 2001 From: Rehan Ansari Date: Sun, 12 Oct 2025 19:01:54 +0530 Subject: [PATCH 46/52] fix: filter sales team to show only active individual salespersons (cherry picked from commit 2fcd406b1827fc3d2f4919e23b6f9b942a0f4cf6) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.js --- .../accounts/doctype/sales_invoice/sales_invoice.js | 13 +++++++++++++ erpnext/selling/doctype/sales_order/sales_order.js | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index a19850af8f1..7de51134ac9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -797,7 +797,20 @@ frappe.ui.form.on("Sales Invoice", { is_group: 0, }, }; +<<<<<<< HEAD }; +======= + }); + + frm.set_query("sales_person", "sales_team", function () { + return { + filters: { + is_group: 0, + enabled: 1, + }, + }; + }); +>>>>>>> 2fcd406b18 (fix: filter sales team to show only active individual salespersons) }, onload: function (frm) { frm.redemption_conversion_factor = null; diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index f29cd253818..4e6f8203f58 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -44,6 +44,15 @@ frappe.ui.form.on("Sales Order", { }; }); + frm.set_query("sales_person", "sales_team", function () { + return { + filters: { + is_group: 0, + enabled: 1, + }, + }; + }); + frm.set_df_property("packed_items", "cannot_add_rows", true); frm.set_df_property("packed_items", "cannot_delete_rows", true); }, From 79a8e2656bf07e5d34b1469224a93abe8095b79f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 14 Oct 2025 14:09:29 +0530 Subject: [PATCH 47/52] perf: optimize sql query (cherry picked from commit e7b64175fd98d9d96ad91cfc410d5c46fe19dfd1) # Conflicts: # erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json --- .../stock_entry_detail/stock_entry_detail.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 9824911404e..dc457892f86 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -188,6 +188,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Basic Rate (as per Stock UOM)", + "non_negative": 1, "oldfieldname": "incoming_rate", "oldfieldtype": "Currency", "options": "Company:company:default_currency", @@ -446,7 +447,8 @@ "no_copy": 1, "options": "Stock Entry", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "ste_detail", @@ -454,7 +456,8 @@ "label": "Stock Entry Child", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "column_break_51", @@ -613,7 +616,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2025-03-26 21:01:58.544797", +======= + "modified": "2025-10-14 14:10:38.373099", +>>>>>>> e7b64175fd (perf: optimize sql query) "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From 8bf553bbce894e3b4c8ac2c8ae7ec377678c24e3 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 14 Oct 2025 17:17:12 +0530 Subject: [PATCH 48/52] chore: fix conflicts --- .../doctype/stock_entry_detail/stock_entry_detail.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index dc457892f86..cedf0872873 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -616,11 +616,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2025-03-26 21:01:58.544797", -======= - "modified": "2025-10-14 14:10:38.373099", ->>>>>>> e7b64175fd (perf: optimize sql query) + "modified": "2025-10-14 15:10:38.373099", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From dccc561eec5f952b85ba24331f12630f4de25b8a Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:25:48 +0530 Subject: [PATCH 49/52] fix: resolve conflict --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 7de51134ac9..193014d340d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -797,10 +797,7 @@ frappe.ui.form.on("Sales Invoice", { is_group: 0, }, }; -<<<<<<< HEAD }; -======= - }); frm.set_query("sales_person", "sales_team", function () { return { @@ -810,7 +807,6 @@ frappe.ui.form.on("Sales Invoice", { }, }; }); ->>>>>>> 2fcd406b18 (fix: filter sales team to show only active individual salespersons) }, onload: function (frm) { frm.redemption_conversion_factor = null; From fd72b55852684cfbb06915c387b44729b26d97f8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 12:12:14 +0000 Subject: [PATCH 50/52] Merge pull request #50081 from frappe/mergify/bp/version-15-hotfix/pr-49960 Fix/Support 50220 (backport #49960) --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 37cd5d771e5..a1422bcb265 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1840,7 +1840,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select(Sum(child.required_bom_qty)) + .select(Sum(child.quantity * child.conversion_factor)) .where( (table.docstatus == 1) & (child.item_code == item_code) From b67b29200c1609557a0f88c273422b16ef6f23b3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:49:27 +0530 Subject: [PATCH 51/52] fix: prevent empty Create dropdown when In Process (backport #49891) (#50063) --- .../doctype/production_plan/production_plan.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index a8a7769b2de..09abcb6351e 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -106,6 +106,8 @@ frappe.ui.form.on("Production Plan", { __("View") ); + let has_create_buttons = false; + if (frm.doc.status !== "Completed") { if (frm.doc.status === "Closed") { frm.add_custom_button( @@ -135,6 +137,7 @@ frappe.ui.form.on("Production Plan", { }, __("Create") ); + has_create_buttons = true; } if ( @@ -149,12 +152,13 @@ frappe.ui.form.on("Production Plan", { }, __("Create") ); + has_create_buttons = true; } } - } - if (frm.doc.status !== "Closed") { - frm.page.set_inner_btn_group_as_primary(__("Create")); + if (has_create_buttons && frm.doc.status !== "Closed") { + frm.page.set_inner_btn_group_as_primary(__("Create")); + } } frm.trigger("material_requirement"); From 432201f634517d8afed1fa234a5c1938b1bdde4f Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Tue, 14 Oct 2025 18:37:59 +0530 Subject: [PATCH 52/52] fix: sanitize projects field in tasks webform (#50089) Signed-off-by: Akhil Narang (cherry picked from commit f8b50d3ffadfdcf6b0468099871944cd11c1e042) --- erpnext/projects/web_form/tasks/tasks.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/projects/web_form/tasks/tasks.py b/erpnext/projects/web_form/tasks/tasks.py index b42297314a9..fbd0866e0ac 100644 --- a/erpnext/projects/web_form/tasks/tasks.py +++ b/erpnext/projects/web_form/tasks/tasks.py @@ -1,15 +1,17 @@ +import urllib.parse + import frappe def get_context(context): - if frappe.form_dict.project: - context.parents = [ - {"title": frappe.form_dict.project, "route": "/projects?project=" + frappe.form_dict.project} - ] - context.success_url = "/projects?project=" + frappe.form_dict.project + if project := frappe.form_dict.project: + title = frappe.utils.data.escape_html(project) + route = "/projects?" + urllib.parse.urlencode({"project": project}) + context.parents = [{"title": title, "route": route}] + context.success_url = route - elif context.doc and context.doc.get("project"): - context.parents = [ - {"title": context.doc.project, "route": "/projects?project=" + context.doc.project} - ] - context.success_url = "/projects?project=" + context.doc.project + elif context.doc and (project := context.doc.get("project")): + title = frappe.utils.data.escape_html(project) + route = "/projects?" + urllib.parse.urlencode({"project": project}) + context.parents = [{"title": title, "route": route}] + context.success_url = route