From 650f874fbdd7cc949c6e2088ed91df8249119e91 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 20 Jan 2026 13:27:14 +0530 Subject: [PATCH 01/38] fix: validation to check at-least one raw material for manufacture entry (cherry picked from commit f003b3c3787488deee4c8238ef94fc0122e6e234) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.py # erpnext/stock/doctype/stock_entry/test_stock_entry.py --- .../stock/doctype/stock_entry/stock_entry.py | 29 +++++++++ .../doctype/stock_entry/test_stock_entry.py | 61 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 21f3245bbc4..a4a699ab1de 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -244,6 +244,35 @@ class StockEntry(StockController): self.validate_same_source_target_warehouse_during_material_transfer() +<<<<<<< HEAD +======= + self.validate_closed_subcontracting_order() + self.validate_subcontract_order() + self.validate_raw_materials_exists() + + super().validate_subcontracting_inward() + + def validate_raw_materials_exists(self): + if self.purpose not in ["Manufacture", "Repack", "Disassemble"]: + return + + if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"): + return + + raw_materials = [] + for row in self.items: + if row.s_warehouse: + raw_materials.append(row.item_code) + + if not raw_materials: + frappe.throw( + _( + "At least one raw material item must be present in the stock entry for the type {0}" + ).format(bold(self.purpose)), + title=_("Raw Materials Missing"), + ) + +>>>>>>> f003b3c378 (fix: validation to check at-least one raw material for manufacture entry) def set_serial_batch_for_disassembly(self): if self.purpose != "Disassemble": return diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 0be02756207..076231193fa 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2233,7 +2233,68 @@ class TestStockEntry(FrappeTestCase): se.submit() +<<<<<<< HEAD def make_serialized_item(**args): +======= + warehouse = "_Test Warehouse - _TC" + retain_sample_item = make_item( + "Retain Sample Item", + properties={ + "is_stock_item": 1, + "retain_sample": 1, + "sample_quantity": 2, + "has_batch_no": 1, + "has_serial_no": 1, + "create_new_batch": 1, + "batch_number_series": "SAMPLE-RET-.#####", + "serial_no_series": "SAMPLE-RET-SN-.#####", + }, + ) + material_receipt = make_stock_entry( + item_code=retain_sample_item.item_code, target=warehouse, qty=10, purpose="Material Receipt" + ) + + source_sabb = frappe.get_doc( + "Serial and Batch Bundle", material_receipt.items[0].serial_and_batch_bundle + ) + batch = source_sabb.entries[0].batch_no + serial_nos = [entry.serial_no for entry in source_sabb.entries] + + sample_entry = frappe.get_doc( + move_sample_to_retention_warehouse(material_receipt.company, material_receipt.items) + ) + sample_entry.submit() + target_sabb = frappe.get_doc("Serial and Batch Bundle", sample_entry.items[0].serial_and_batch_bundle) + + self.assertEqual(sample_entry.items[0].transfer_qty, 2) + self.assertEqual(target_sabb.entries[0].batch_no, batch) + self.assertEqual([entry.serial_no for entry in target_sabb.entries], serial_nos[:2]) + + def test_raw_material_missing_validation(self): + original_value = frappe.db.get_single_value("Manufacturing Settings", "material_consumption") + frappe.db.set_single_value("Manufacturing Settings", "material_consumption", 0) + + stock_entry = make_stock_entry( + item_code="_Test Item", + qty=1, + target="_Test Warehouse - _TC", + do_not_save=True, + ) + + stock_entry.purpose = "Manufacture" + stock_entry.stock_entry_type = "Manufacture" + stock_entry.items[0].is_finished_item = 1 + + self.assertRaises( + frappe.ValidationError, + stock_entry.save, + ) + + frappe.db.set_single_value("Manufacturing Settings", "material_consumption", original_value) + + +def make_serialized_item(self, **args): +>>>>>>> f003b3c378 (fix: validation to check at-least one raw material for manufacture entry) args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) From 184fa889c33b13b184132dea6d35e0621e7124ff Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 20 Jan 2026 15:08:37 +0530 Subject: [PATCH 02/38] fix: allow creation of DN in SI for items not having DN reference (cherry picked from commit b691de0147cd6b5f19fa3fd9582fb18d97b74371) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.js # erpnext/accounts/doctype/sales_invoice/sales_invoice.py --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 7 +++++++ erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 9711c4637bc..217c42c1a14 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -111,6 +111,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( } if (cint(doc.update_stock) != 1) { +<<<<<<< HEAD // show Make Delivery Note button only if Sales Invoice is not created from Delivery Note var from_delivery_note = false; from_delivery_note = cur_frm.doc.items.some(function (item) { @@ -121,6 +122,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( cur_frm.add_custom_button( __("Delivery"), cur_frm.cscript["Make Delivery Note"], +======= + if (!is_delivered_by_supplier) { + this.frm.add_custom_button( + __("Delivery Note"), + this.frm.cscript["Make Delivery Note"], +>>>>>>> b691de0147 (fix: allow creation of DN in SI for items not having DN reference) __("Create") ); } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 9e15f8701bb..38641f00574 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2211,7 +2211,13 @@ def make_delivery_note(source_name, target_doc=None): "cost_center": "cost_center", }, "postprocess": update_item, +<<<<<<< HEAD "condition": lambda doc: doc.delivered_by_supplier != 1, +======= + "condition": lambda doc: doc.delivered_by_supplier != 1 + and not doc.scio_detail + and not doc.dn_detail, +>>>>>>> b691de0147 (fix: allow creation of DN in SI for items not having DN reference) }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": { From 11544818f1082763bf817da7b47886a9148eea92 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 08:51:41 +0000 Subject: [PATCH 03/38] Merge pull request #51950 from frappe/mergify/bp/version-15-hotfix/pr-51948 fix: warehouse permissions in MR incorrectly ignored (backport #51948) --- erpnext/stock/doctype/material_request/material_request.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 0c467df381c..c10b727bc48 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -281,7 +281,6 @@ { "fieldname": "set_warehouse", "fieldtype": "Link", - "ignore_user_permissions": 1, "in_list_view": 1, "label": "Set Target Warehouse", "options": "Warehouse" @@ -368,7 +367,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2025-07-31 17:19:01.166208", + "modified": "2026-01-21 12:48:40.792323", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", From d3440cf5450bfbb030aa31a3686f7012e6a5c104 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 21 Jan 2026 15:24:14 +0530 Subject: [PATCH 04/38] chore: resolve conflicts --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 217c42c1a14..be876727314 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -111,23 +111,16 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( } if (cint(doc.update_stock) != 1) { -<<<<<<< HEAD // show Make Delivery Note button only if Sales Invoice is not created from Delivery Note var from_delivery_note = false; from_delivery_note = cur_frm.doc.items.some(function (item) { return item.delivery_note ? true : false; }); - if (!from_delivery_note && !is_delivered_by_supplier) { - cur_frm.add_custom_button( - __("Delivery"), - cur_frm.cscript["Make Delivery Note"], -======= if (!is_delivered_by_supplier) { this.frm.add_custom_button( __("Delivery Note"), this.frm.cscript["Make Delivery Note"], ->>>>>>> b691de0147 (fix: allow creation of DN in SI for items not having DN reference) __("Create") ); } From 386567a6ea460314d2f7177e6275169530c2073d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 21 Jan 2026 15:27:12 +0530 Subject: [PATCH 05/38] chore: resolve conflicts --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 38641f00574..c202b70dc42 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2211,13 +2211,7 @@ def make_delivery_note(source_name, target_doc=None): "cost_center": "cost_center", }, "postprocess": update_item, -<<<<<<< HEAD - "condition": lambda doc: doc.delivered_by_supplier != 1, -======= - "condition": lambda doc: doc.delivered_by_supplier != 1 - and not doc.scio_detail - and not doc.dn_detail, ->>>>>>> b691de0147 (fix: allow creation of DN in SI for items not having DN reference) + "condition": lambda doc: doc.delivered_by_supplier != 1 and not doc.dn_detail, }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": { From 8d06ee3966271fbfec499a4409c00a73f8fca0e4 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 21 Jan 2026 11:46:00 +0530 Subject: [PATCH 06/38] fix: job cards should not be deleted on close of WO (cherry picked from commit c919b1de3836bde91a11b61adaedd93322c758c8) --- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index c26af9a74af..7d8b4843828 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -517,6 +517,7 @@ class WorkOrder(Document): self.db_set("status", "Cancelled") self.on_close_or_cancel() + self.delete_job_card() def on_close_or_cancel(self): if self.production_plan and frappe.db.exists( @@ -526,7 +527,6 @@ class WorkOrder(Document): else: self.update_work_order_qty_in_so() - self.delete_job_card() self.update_completed_qty_in_material_request() self.update_planned_qty() self.update_ordered_qty() From 83352b5a34ac5a127f2b6ff289b282a9746dcb42 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 21 Jan 2026 22:33:24 +0530 Subject: [PATCH 07/38] fix: rejected qty in PR doesn't consider conversion factor (cherry picked from commit 343ee9695b6992c613c89ab61279238b458ccaa6) --- erpnext/controllers/stock_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 035dfc6e264..bf91f9490a4 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -465,7 +465,7 @@ class StockController(AccountsController): if is_rejected: serial_nos = row.get("rejected_serial_no") type_of_transaction = "Inward" if not self.is_return else "Outward" - qty = row.get("rejected_qty") + qty = row.get("rejected_qty") * row.get("conversion_factor", 1.0) warehouse = row.get("rejected_warehouse") if ( From c35426b9f936ae2aae740503708a20d8588ec6ed Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:27:37 +0000 Subject: [PATCH 08/38] Merge pull request #51969 from frappe/mergify/bp/version-15-hotfix/pr-51964 fix: create DN btn should not be shown if it cannot be created (backport #51964) --- .../doctype/sales_invoice/sales_invoice.js | 16 ++++++++++++---- .../doctype/sales_invoice/sales_invoice.py | 4 +++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index be876727314..2c96286d3bb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -118,11 +118,19 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( }); if (!is_delivered_by_supplier) { - this.frm.add_custom_button( - __("Delivery Note"), - this.frm.cscript["Make Delivery Note"], - __("Create") + const should_create_delivery_note = doc.items.some( + (item) => + item.qty - item.delivered_qty > 0 && + !item.dn_detail && + !item.delivered_by_supplier ); + if (should_create_delivery_note) { + this.frm.add_custom_button( + __("Delivery Note"), + this.frm.cscript["Make Delivery Note"], + __("Create") + ); + } } } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c202b70dc42..8ed23724dfb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2211,7 +2211,9 @@ def make_delivery_note(source_name, target_doc=None): "cost_center": "cost_center", }, "postprocess": update_item, - "condition": lambda doc: doc.delivered_by_supplier != 1 and not doc.dn_detail, + "condition": lambda doc: doc.delivered_by_supplier != 1 + and not doc.dn_detail + and doc.qty - doc.delivered_qty > 0, }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": { From f61305aa4560f585fc572bb42a2a8f093bca3ad1 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Wed, 21 Jan 2026 19:53:41 +0530 Subject: [PATCH 09/38] fix(project): add missing counter to project update naming series (cherry picked from commit 49e64f4e1c324a6e520e95151c9f8ecf240ec4fb) --- erpnext/projects/doctype/project/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index cc8f2434513..1f434a485b5 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -603,7 +603,7 @@ def send_project_update_email_to_users(project): "sent": 0, "date": today(), "time": nowtime(), - "naming_series": "UPDATE-.project.-.YY.MM.DD.-", + "naming_series": "UPDATE-.project.-.YY.MM.DD.-.####", } ).insert() From f7770c322530ba5b9834c6a2453a5d5f8a70df1d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 05:16:45 +0000 Subject: [PATCH 10/38] Merge pull request #51979 from frappe/mergify/bp/version-15-hotfix/pr-51966 fix(customer): add customer group filters (backport #51966) --- erpnext/selling/doctype/customer/customer.json | 3 ++- erpnext/selling/doctype/selling_settings/selling_settings.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 7c0676c672f..dd1910ed15a 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -181,6 +181,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Customer Group", + "link_filters": "[[\"Customer Group\", \"is_group\", \"=\", 0]]", "oldfieldname": "customer_group", "oldfieldtype": "Link", "options": "Customer Group", @@ -610,7 +611,7 @@ "link_fieldname": "party" } ], - "modified": "2025-11-25 09:35:56.772949", + "modified": "2026-01-21 17:23:42.151114", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 83ba1907671..d2f8945eb29 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -51,6 +51,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Default Customer Group", + "link_filters": "[[\"Customer Group\", \"is_group\", \"=\", 0]]", "options": "Customer Group" }, { @@ -231,7 +232,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-09-23 21:10:14.826653", + "modified": "2026-01-21 17:28:37.027837", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From 0a87fa53489ef01b984b8d517f56aa8cd9404c8e Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 22 Jan 2026 12:54:51 +0530 Subject: [PATCH 11/38] fix: autofill warehouse for packed items (cherry picked from commit 3f8a0a483355509360d524cae0debc1b2f8a8b27) --- erpnext/public/js/controllers/transaction.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0ae1828ee0d..2e782b49e66 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2726,10 +2726,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe set_warehouse() { this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse); + this.autofill_warehouse(this.frm.doc.packed_items, "warehouse", this.frm.doc.set_warehouse); } set_target_warehouse() { this.autofill_warehouse(this.frm.doc.items, "target_warehouse", this.frm.doc.set_target_warehouse); + this.autofill_warehouse( + this.frm.doc.packed_items, + "target_warehouse", + this.frm.doc.set_target_warehouse + ); } set_from_warehouse() { From f9fd0ffbae96f2191cdfffe72681f00dbb29966b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 22 Jan 2026 23:56:06 +0530 Subject: [PATCH 12/38] fix: negative stock for purchae return (cherry picked from commit d68a04ad169cd08f34219bbdc31fee765a3e3f76) # Conflicts: # erpnext/stock/serial_batch_bundle.py --- erpnext/stock/deprecated_serial_batch.py | 3 - .../purchase_receipt/test_purchase_receipt.py | 39 ++++++ .../serial_and_batch_bundle.py | 120 ++++++++++++++++-- erpnext/stock/serial_batch_bundle.py | 7 + 4 files changed, 155 insertions(+), 14 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index f6984c9edab..69443e3a608 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -78,7 +78,6 @@ class DeprecatedBatchNoValuation: for ledger in entries: self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value) self.available_qty[ledger.batch_no] += flt(ledger.batch_qty) - self.total_qty[ledger.batch_no] += flt(ledger.batch_qty) @deprecated def get_sle_for_batches(self): @@ -231,7 +230,6 @@ class DeprecatedBatchNoValuation: batch_data = query.run(as_dict=True) for d in batch_data: self.available_qty[d.batch_no] += flt(d.batch_qty) - self.total_qty[d.batch_no] += flt(d.batch_qty) for d in batch_data: if self.available_qty.get(d.batch_no): @@ -332,7 +330,6 @@ class DeprecatedBatchNoValuation: batch_data = query.run(as_dict=True) for d in batch_data: self.available_qty[d.batch_no] += flt(d.batch_qty) - self.total_qty[d.batch_no] += flt(d.batch_qty) if not self.last_sle: return diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 5570fc265b0..ee7b327adca 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4677,6 +4677,45 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]]) + def test_negative_stock_error_for_purchase_return(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = make_item( + "Test Negative Stock for Purchase Return Item", + {"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TNSFPRI.#####"}, + ).name + + pr = make_purchase_receipt( + item_code=item_code, + posting_date=add_days(today(), -3), + qty=10, + rate=100, + warehouse="_Test Warehouse - _TC", + ) + + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + + make_purchase_receipt( + item_code=item_code, + posting_date=add_days(today(), -4), + qty=10, + rate=100, + warehouse="_Test Warehouse - _TC", + ) + + make_stock_entry( + item_code=item_code, + qty=10, + source="_Test Warehouse - _TC", + target="_Test Warehouse 1 - _TC", + batch_no=batch_no, + use_serial_batch_fields=1, + ) + + return_pr = make_return_doc("Purchase Receipt", pr.name) + self.assertRaises(frappe.ValidationError, return_pr.submit) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier 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 b6caf53e470..fbbe0f4e8b4 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 @@ -576,14 +576,12 @@ class SerialandBatchBundle(Document): d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no))) precision = d.precision("qty") - for field in ["available_qty", "total_qty"]: - value = getattr(sn_obj, field) - available_qty = flt(value.get(d.batch_no), precision) - if self.docstatus == 1: - available_qty += flt(d.qty, precision) + available_qty = flt(sn_obj.available_qty.get(d.batch_no), precision) + if self.docstatus == 1: + available_qty += flt(d.qty, precision) - if not allow_negative_stock: - self.validate_negative_batch(d.batch_no, available_qty, field) + if not allow_negative_stock: + self.validate_negative_batch(d.batch_no, available_qty) d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) @@ -596,8 +594,8 @@ class SerialandBatchBundle(Document): } ) - def validate_negative_batch(self, batch_no, available_qty, field=None): - if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty, field=field): + def validate_negative_batch(self, batch_no, available_qty): + if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty): msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)} has negative stock of quantity {bold(available_qty)} in the @@ -605,7 +603,7 @@ class SerialandBatchBundle(Document): frappe.throw(_(msg), BatchNegativeStockError) - def is_stock_reco_for_valuation_adjustment(self, available_qty, field=None): + def is_stock_reco_for_valuation_adjustment(self, available_qty): if ( self.voucher_type == "Stock Reconciliation" and self.type_of_transaction == "Outward" @@ -613,7 +611,6 @@ class SerialandBatchBundle(Document): and ( abs(frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty")) == abs(available_qty) - or field == "total_qty" ) ): return True @@ -1346,6 +1343,7 @@ class SerialandBatchBundle(Document): def on_submit(self): self.validate_serial_nos_inventory() + self.validate_batch_quantity() def set_purchase_document_no(self): if self.flags.ignore_validate_serial_batch: @@ -1404,6 +1402,106 @@ class SerialandBatchBundle(Document): def on_cancel(self): self.validate_voucher_no_docstatus() + self.validate_batch_quantity() + + def validate_batch_quantity(self): + if not self.has_batch_no: + return + + if self.type_of_transaction != "Outward" or ( + self.voucher_type == "Stock Reconciliation" and self.type_of_transaction == "Outward" + ): + return + + batch_wise_available_qty = self.get_batchwise_available_qty() + precision = frappe.get_precision("Serial and Batch Entry", "qty") + + for d in self.entries: + available_qty = batch_wise_available_qty.get(d.batch_no, 0) + if flt(available_qty, precision) < 0: + frappe.throw( + _( + """ + The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry.""" + ).format( + bold(d.batch_no), + bold(self.item_code), + bold(self.warehouse), + bold(abs(flt(available_qty, precision))), + ), + title=_("Negative Stock Error"), + ) + + def get_batchwise_available_qty(self): + available_qty = self.get_available_qty_from_sabb() + available_qty_from_ledger = self.get_available_qty_from_stock_ledger() + + if not available_qty_from_ledger: + return available_qty + + for batch_no, qty in available_qty_from_ledger.items(): + if batch_no in available_qty: + available_qty[batch_no] += qty + else: + available_qty[batch_no] = qty + + return available_qty + + def get_available_qty_from_stock_ledger(self): + batches = [d.batch_no for d in self.entries if d.batch_no] + + sle = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(sle) + .select( + sle.batch_no, + Sum(sle.actual_qty).as_("available_qty"), + ) + .where( + (sle.item_code == self.item_code) + & (sle.warehouse == self.warehouse) + & (sle.is_cancelled == 0) + & (sle.batch_no.isin(batches)) + & (sle.docstatus == 1) + & (sle.serial_and_batch_bundle.isnull()) + & (sle.batch_no.isnotnull()) + ) + .for_update() + .groupby(sle.batch_no) + ) + + res = query.run(as_list=True) + + return frappe._dict(res) if res else frappe._dict() + + def get_available_qty_from_sabb(self): + batches = [d.batch_no for d in self.entries if d.batch_no] + + child = frappe.qb.DocType("Serial and Batch Entry") + + query = ( + frappe.qb.from_(child) + .select( + child.batch_no, + Sum(child.qty).as_("available_qty"), + ) + .where( + (child.item_code == self.item_code) + & (child.warehouse == self.warehouse) + & (child.is_cancelled == 0) + & (child.batch_no.isin(batches)) + & (child.docstatus == 1) + & (child.type_of_transaction.isin(["Inward", "Outward"])) + ) + .for_update() + .groupby(child.batch_no) + ) + query = query.where(child.voucher_type != "Pick List") + + res = query.run(as_list=True) + + return frappe._dict(res) if res else frappe._dict() def validate_voucher_no_docstatus(self): if self.voucher_type == "POS Invoice": diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index f2639998d3e..e35f1ea8b4e 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -803,15 +803,19 @@ class BatchNoValuation(DeprecatedBatchNoValuation): for ledger in entries: self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate) self.available_qty[ledger.batch_no] += flt(ledger.qty) +<<<<<<< HEAD entries = self.get_batch_wise_total_available_qty() for row in entries: self.total_qty[row.batch_no] += flt(row.total_qty) +======= +>>>>>>> d68a04ad16 (fix: negative stock for purchae return) self.calculate_avg_rate_from_deprecarated_ledgers() self.calculate_avg_rate_for_non_batchwise_valuation() self.set_stock_value_difference() +<<<<<<< HEAD def get_batch_wise_total_available_qty(self) -> list[dict]: # Get total qty of each batch no from Serial and Batch Bundle without checking time condition if not self.batchwise_valuation_batches: @@ -851,6 +855,9 @@ class BatchNoValuation(DeprecatedBatchNoValuation): return query.run(as_dict=True) def get_batch_no_ledgers(self) -> list[dict]: +======= + def get_batch_stock_before_date(self) -> list[dict]: +>>>>>>> d68a04ad16 (fix: negative stock for purchae return) # Get batch wise stock value difference from Serial and Batch Bundle considering time condition if not self.batchwise_valuation_batches: return [] From c8a52ec43c34200e6583fc33479a26cf86cac54e Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 23 Jan 2026 11:51:02 +0530 Subject: [PATCH 13/38] chore: fix conflicts Removed deprecated method for batch-wise total available quantity and adjusted stock value calculations. --- erpnext/stock/serial_batch_bundle.py | 49 ---------------------------- 1 file changed, 49 deletions(-) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index e35f1ea8b4e..4d7b3d4aaa4 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -803,61 +803,12 @@ class BatchNoValuation(DeprecatedBatchNoValuation): for ledger in entries: self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate) self.available_qty[ledger.batch_no] += flt(ledger.qty) -<<<<<<< HEAD - - entries = self.get_batch_wise_total_available_qty() - for row in entries: - self.total_qty[row.batch_no] += flt(row.total_qty) -======= ->>>>>>> d68a04ad16 (fix: negative stock for purchae return) self.calculate_avg_rate_from_deprecarated_ledgers() self.calculate_avg_rate_for_non_batchwise_valuation() self.set_stock_value_difference() -<<<<<<< HEAD - def get_batch_wise_total_available_qty(self) -> list[dict]: - # Get total qty of each batch no from Serial and Batch Bundle without checking time condition - if not self.batchwise_valuation_batches: - return [] - - parent = frappe.qb.DocType("Serial and Batch Bundle") - child = frappe.qb.DocType("Serial and Batch Entry") - - query = ( - frappe.qb.from_(parent) - .inner_join(child) - .on(parent.name == child.parent) - .select( - child.batch_no, - Sum(child.qty).as_("total_qty"), - ) - .where( - (parent.warehouse == self.sle.warehouse) - & (parent.item_code == self.sle.item_code) - & (child.batch_no.isin(self.batchwise_valuation_batches)) - & (parent.docstatus == 1) - & (parent.is_cancelled == 0) - & (parent.type_of_transaction.isin(["Inward", "Outward"])) - ) - .for_update() - .groupby(child.batch_no) - ) - - # Important to exclude the current voucher detail no / voucher no to calculate the correct stock value difference - if self.sle.voucher_detail_no: - query = query.where(parent.voucher_detail_no != self.sle.voucher_detail_no) - elif self.sle.voucher_no: - query = query.where(parent.voucher_no != self.sle.voucher_no) - - query = query.where(parent.voucher_type != "Pick List") - - return query.run(as_dict=True) - def get_batch_no_ledgers(self) -> list[dict]: -======= - def get_batch_stock_before_date(self) -> list[dict]: ->>>>>>> d68a04ad16 (fix: negative stock for purchae return) # Get batch wise stock value difference from Serial and Batch Bundle considering time condition if not self.batchwise_valuation_batches: return [] From 3489b65f1ae2e25adc75bfa2d2e9a2659e21f5ee Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 23 Jan 2026 12:27:37 +0530 Subject: [PATCH 14/38] chore: fix conflicts --- .../serial_and_batch_bundle.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 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 fbbe0f4e8b4..fe10c6aeeb9 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 @@ -1478,26 +1478,30 @@ class SerialandBatchBundle(Document): def get_available_qty_from_sabb(self): batches = [d.batch_no for d in self.entries if d.batch_no] + parent = frappe.qb.DocType("Serial and Batch Bundle") child = frappe.qb.DocType("Serial and Batch Entry") query = ( - frappe.qb.from_(child) + frappe.qb.from_(parent) + .inner_join(child) + .on(parent.name == child.parent) .select( child.batch_no, - Sum(child.qty).as_("available_qty"), + Sum(child.qty).as_("total_qty"), ) .where( - (child.item_code == self.item_code) - & (child.warehouse == self.warehouse) - & (child.is_cancelled == 0) + (parent.warehouse == self.warehouse) + & (parent.item_code == self.item_code) & (child.batch_no.isin(batches)) - & (child.docstatus == 1) - & (child.type_of_transaction.isin(["Inward", "Outward"])) + & (parent.docstatus == 1) + & (parent.is_cancelled == 0) + & (parent.type_of_transaction.isin(["Inward", "Outward"])) ) .for_update() .groupby(child.batch_no) ) - query = query.where(child.voucher_type != "Pick List") + + query = query.where(parent.voucher_type != "Pick List") res = query.run(as_list=True) From b5d84773545362715f61dbe17930a977fa81ee43 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 23 Jan 2026 19:35:47 +0530 Subject: [PATCH 15/38] fix: Bin reserved qty for production for extra material transfer (cherry picked from commit f5378b6573ab3d536b360e224ab63839cb51d9c2) # Conflicts: # erpnext/manufacturing/doctype/work_order/test_work_order.py --- .../doctype/work_order/test_work_order.py | 429 ++++++++++++++++++ .../doctype/work_order/work_order.py | 3 + 2 files changed, 432 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 4640f5192dd..f07e9ba013e 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -3186,6 +3186,435 @@ class TestWorkOrder(FrappeTestCase): allow_overproduction("overproduction_percentage_for_work_order", 0) +<<<<<<< HEAD +======= + def test_reserved_serial_batch(self): + raw_materials = [] + for item_code, properties in { + "Test Reserved FG Item": {"is_stock_item": 1}, + "Test Reserved Serial Item": {"has_serial_no": 1, "serial_no_series": "TSNN-RSI-.####"}, + "Test Reserved Batch Item": { + "has_batch_no": 1, + "batch_number_series": "BCH-RBI-.####", + "create_new_batch": 1, + }, + "Test Reserved Serial Batch Item": { + "has_serial_no": 1, + "serial_no_series": "TSNB-RSBI-.####", + "has_batch_no": 1, + "batch_number_series": "BCH-RSBI-.####", + "create_new_batch": 1, + }, + }.items(): + make_item(item_code, properties=properties) + if item_code != "Test Reserved FG Item": + raw_materials.append(item_code) + test_stock_entry.make_stock_entry( + item_code=item_code, + target="Stores - _TC", + qty=5, + basic_rate=100, + ) + + original_auto_reserve = frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch") + original_backflush = frappe.db.get_single_value( + "Manufacturing Settings", "backflush_raw_materials_based_on" + ) + frappe.db.set_single_value( + "Manufacturing Settings", + "backflush_raw_materials_based_on", + "Material Transferred for Manufacture", + ) + frappe.db.set_single_value("Stock Settings", "auto_reserve_serial_and_batch", 1) + + make_bom( + item="Test Reserved FG Item", + source_warehouse="Stores - _TC", + raw_materials=raw_materials, + ) + + wo = make_wo_order_test_record( + item="Test Reserved FG Item", + qty=5, + source_warehouse="Stores - _TC", + reserve_stock=1, + ) + + _reserved_item = get_reserved_entries(wo.name) + for key, value in _reserved_item.items(): + self.assertEqual(key[1], "Stores - _TC") + self.assertEqual(value.reserved_qty, 5) + if value.serial_nos: + self.assertEqual(len(value.serial_nos), 5) + + if value.batch_nos: + self.assertEqual(sum(value.batch_nos.values()), 5) + + # Transfer 5 qty + mt_stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 5)) + mt_stock_entry.submit() + + for row in mt_stock_entry.items: + value = _reserved_item[(row.item_code, row.s_warehouse)] + self.assertEqual(row.qty, value.reserved_qty) + if value.serial_nos: + serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle) + self.assertEqual(sorted(serial_nos), sorted(value.serial_nos)) + + if value.batch_nos: + self.assertTrue(row.batch_no in value.batch_nos) + + _before_reserved_item = get_reserved_entries(wo.name, mt_stock_entry.items[0].t_warehouse) + + # Manufacture 2 qty + fg_stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 2)) + fg_stock_entry.submit() + + for row in fg_stock_entry.items: + if not row.s_warehouse: + continue + + value = _before_reserved_item[(row.item_code, row.s_warehouse)] + if row.serial_no: + serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle) + for sn in serial_nos: + self.assertTrue(sn in value.serial_nos) + value.serial_nos.remove(sn) + + if row.batch_no: + self.assertTrue(row.batch_no in value.batch_nos) + value.batch_nos[row.batch_no] -= row.qty + if row.serial_no: + sns = get_serial_nos_from_bundle(row.serial_and_batch_bundle) + for sn in sns: + self.assertTrue(sn in value.serial_batches[row.batch_no]) + value.serial_batches[row.batch_no].remove(sn) + + # Manufacture 3 qty + fg_stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3)) + fg_stock_entry.submit() + + for row in fg_stock_entry.items: + if not row.s_warehouse: + continue + + value = _before_reserved_item[(row.item_code, row.s_warehouse)] + + if row.serial_no: + serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle) + self.assertEqual(sorted(serial_nos), sorted(value.serial_nos)) + + if row.batch_no: + self.assertTrue(row.batch_no in value.batch_nos) + self.assertEqual(value.batch_nos[row.batch_no], row.qty) + if row.serial_no: + sns = get_serial_nos_from_bundle(row.serial_and_batch_bundle) + self.assertEqual(sorted(sns), sorted(value.serial_batches[row.batch_no])) + + frappe.db.set_single_value( + "Manufacturing Settings", "backflush_raw_materials_based_on", original_backflush + ) + frappe.db.set_single_value("Stock Settings", "auto_reserve_serial_and_batch", original_auto_reserve) + + def test_phantom_bom_item_not_in_additional_cost(self): + """Test that phantom BOMs are not added to additional costs, + but regular non-stock items in the FG BOM are added.""" + + from erpnext.stock.doctype.item.test_item import make_item + + # Create items: + # - FG Item (stock item) + # - Phantom sub-assembly (non-stock item to be phantom) + # - Phantom RM (stock item - component of phantom BOM) + # - Packing Material (non-stock item - directly in FG BOM) + # - Regular RM (stock item - directly in FG BOM) + + fg_item = make_item( + "Test FG Item For Phantom Non Stock", + {"is_stock_item": 1, "valuation_rate": 100}, + ).name + + phantom_item = make_item( + "Test Phantom Sub Assembly Non Stock", + {"is_stock_item": 0, "valuation_rate": 0}, + ).name + + phantom_rm = make_item( + "Test Phantom RM Item", + {"is_stock_item": 1, "valuation_rate": 200}, + ).name + + packing_material = make_item( + "Test Packing Material Non Stock", + {"is_stock_item": 0, "valuation_rate": 150}, + ).name + + regular_rm = make_item( + "Test Regular RM Stock Item", + {"is_stock_item": 1, "valuation_rate": 100}, + ).name + + # Create price list entries for non-stock items + price_list = "_Test Price List India" + for item_code, rate in [ + (phantom_item, 500), + (phantom_rm, 200), + (packing_material, 150), + ]: + if not frappe.db.get_value("Item Price", {"item_code": item_code, "price_list": price_list}): + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": item_code, + "price_list_rate": rate, + "price_list": price_list, + } + ).insert(ignore_permissions=True) + + # Create Phantom BOM (for the phantom sub-assembly) + phantom_bom = frappe.get_doc( + { + "doctype": "BOM", + "item": phantom_item, + "is_default": 1, + "is_active": 1, + "is_phantom_bom": 1, # Mark as phantom BOM + "currency": "INR", + "quantity": 1, + "company": "_Test Company", + "rm_cost_as_per": "Price List", + "buying_price_list": price_list, + } + ) + phantom_bom.append( + "items", + { + "item_code": phantom_rm, + "qty": 1, + "rate": 200, + }, + ) + phantom_bom.insert() + phantom_bom.submit() + + # Create FG BOM with phantom item, packing material, and regular RM + fg_bom = frappe.get_doc( + { + "doctype": "BOM", + "item": fg_item, + "is_default": 1, + "is_active": 1, + "currency": "INR", + "quantity": 1, + "company": "_Test Company", + "rm_cost_as_per": "Price List", + "buying_price_list": price_list, + } + ) + + # Add phantom item (will be marked as is_phantom_item based on is_phantom_bom) + fg_bom.append( + "items", + { + "item_code": phantom_item, + "qty": 1, + "rate": 200, + "bom_no": phantom_bom.name, + }, + ) + + # Add packing material (non-stock, directly in FG BOM) + fg_bom.append( + "items", + { + "item_code": packing_material, + "qty": 1, + "rate": 150, + }, + ) + + # Add regular RM (stock item) + fg_bom.append( + "items", + { + "item_code": regular_rm, + "qty": 1, + "rate": 100, + }, + ) + + fg_bom.insert() + fg_bom.submit() + + # Ensure stock + test_stock_entry.make_stock_entry( + item_code=regular_rm, + target="_Test Warehouse - _TC", + qty=10, + basic_rate=100, + ) + + test_stock_entry.make_stock_entry( + item_code=phantom_rm, + target="_Test Warehouse - _TC", + qty=10, + basic_rate=200, + ) + + # Create work order + wo = make_wo_order_test_record( + production_item=fg_item, + bom_no=fg_bom.name, + qty=1, + source_warehouse="_Test Warehouse - _TC", + ) + + # Transfer materials + se_transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 1)) + se_transfer.insert() + se_transfer.submit() + + # Manufacture + se_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1)) + se_manufacture.insert() + + # Verify additional costs + self.assertTrue(se_manufacture.additional_costs, "Additional costs should not be empty") + total_additional_cost = sum(row.amount for row in se_manufacture.additional_costs) + + self.assertEqual( + total_additional_cost, + 150, # only packing material; phantom RM excluded + f"Additional cost should be 150 (packing material only), got {total_additional_cost}", + ) + + self.assertEqual( + se_manufacture.total_outgoing_value, + 300, # 100 (regular RM) + 200 (phantom RM) + f"Total outgoing value should be 300, got {se_manufacture.total_outgoing_value}", + ) + + self.assertEqual( + se_manufacture.total_incoming_value, + 450, # 300 (RM total) + 150 (packing material) + f"Total incoming value should be 450, got {se_manufacture.total_incoming_value}", + ) + + # Clean up + se_manufacture.submit() + se_manufacture.cancel() + se_transfer.cancel() + wo.reload() + wo.cancel() + fg_bom.cancel() + phantom_bom.cancel() + + def test_phantom_bom_explosion(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_tree_for_phantom_bom_tests + + expected = create_tree_for_phantom_bom_tests() + + wo = make_wo_order_test_record(item="Top Level Parent") + self.assertEqual([item.item_code for item in wo.required_items], expected) + + def test_reserved_qty_for_pp_with_extra_material_transfer(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + rm_item_code = make_item( + "_Test Reserved Qty PP Item", + { + "is_stock_item": 1, + }, + ).name + + fg_item_code = make_item( + "_Test Reserved Qty PP FG Item", + { + "is_stock_item": 1, + }, + ).name + + make_stock_entry_test_record( + item_code=rm_item_code, target="_Test Warehouse - _TC", qty=10, basic_rate=100 + ) + + make_bom( + item=fg_item_code, + raw_materials=[rm_item_code], + ) + + wo_order = make_wo_order_test_record( + item=fg_item_code, + qty=1, + source_warehouse="_Test Warehouse - _TC", + skip_transfer=0, + target_warehouse="_Test Warehouse - _TC", + ) + + bin1_at_completion = get_bin(rm_item_code, "_Test Warehouse - _TC") + self.assertEqual(bin1_at_completion.reserved_qty_for_production, 1) + + s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1)) + s.items[0].qty += 2 # extra material transfer + s.submit() + + bin1_at_completion = get_bin(rm_item_code, "_Test Warehouse - _TC") + + self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0) + + +def get_reserved_entries(voucher_no, warehouse=None): + doctype = frappe.qb.DocType("Stock Reservation Entry") + sabb = frappe.qb.DocType("Serial and Batch Entry") + + query = ( + frappe.qb.from_(doctype) + .left_join(sabb) + .on(doctype.name == sabb.parent) + .select( + doctype.name, + doctype.item_code, + doctype.warehouse, + doctype.reserved_qty, + sabb.serial_no, + sabb.batch_no, + sabb.qty, + sabb.delivered_qty, + ) + .where((doctype.voucher_no == voucher_no) & (doctype.docstatus == 1)) + ) + + if warehouse: + query = query.where(doctype.warehouse == warehouse) + + reservation_entries = query.run(as_dict=True) + + _reserved_item = frappe._dict({}) + for entry in reservation_entries: + key = (entry.item_code, entry.warehouse) + if key not in _reserved_item: + _reserved_item[key] = frappe._dict( + { + "reserved_qty": 0, + "serial_nos": [], + "batch_nos": defaultdict(int), + "serial_batches": defaultdict(list), + } + ) + + _reserved_item[key].reserved_qty += entry.qty + if entry.batch_no: + _reserved_item[key].batch_nos[entry.batch_no] += entry.qty + if entry.serial_no: + _reserved_item[key].serial_batches[entry.batch_no].append(entry.serial_no) + if entry.serial_no: + _reserved_item[key].serial_nos.append(entry.serial_no) + + return _reserved_item + +>>>>>>> f5378b6573 (fix: Bin reserved qty for production for extra material transfer) def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 7d8b4843828..f5a5e2693b9 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1786,6 +1786,9 @@ def get_reserved_qty_for_production( qty_field = wo_item.required_qty else: qty_field = Case() + qty_field = qty_field.when( + ((wo.skip_transfer == 0) & (wo_item.transferred_qty > wo_item.required_qty)), 0.0 + ) qty_field = qty_field.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) qty_field = qty_field.else_(wo_item.required_qty - wo_item.consumed_qty) From 9fce694936f38ba62399aba9ba28517462c0731f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 12:03:51 +0530 Subject: [PATCH 16/38] fix: Ensure paid_amount is always numeric before calling allocate_amount_to_references (backport #50935) (#52035) fix: Ensure paid_amount is always numeric before calling allocate_amount_to_references (#50935) fix: ensure paid_amount is not null in allocate_party_amount_against_ref_docs (cherry picked from commit 50b3396064b0d067b333d28ac0c6b2cb9416c306) Co-authored-by: El-Shafei H. --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 852fcc6807b..db2004b8c8f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1119,7 +1119,7 @@ frappe.ui.form.on("Payment Entry", { allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) { await frm.call("allocate_amount_to_references", { - paid_amount: paid_amount, + paid_amount: flt(paid_amount), paid_amount_change: paid_amount_change, allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false, }); From d89ac99e761816e62668c342152dce35ee4dca17 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 06:48:04 +0000 Subject: [PATCH 17/38] fix: update country_wise_tax.json for Algerian Taxes (backport #51878) (#52037) fix: update country_wise_tax.json for Algerian Taxes (#51878) * Algeria chart of accounts Algeria chart of accounts * Update Algeria Chart Of Account * Algeria chart of account * Algeria Chart of Account Algeria Chart of Account * Modify Algeria tax entries in country_wise_tax.json Updated tax rates and account names for Algeria. * Rename account for Algeria tax from VAT to TVA Rename account for Algeria tax from VAT to TVA (cherry picked from commit e810cd8440b2753d3be8b48c1ae6cba524ca75e9) Co-authored-by: HALFWARE --- .../setup/setup_wizard/data/country_wise_tax.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index e82daeb7c72..3616425771c 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -6,14 +6,14 @@ } }, "Algeria": { - "Algeria VAT 17%": { - "account_name": "VAT 17%", - "tax_rate": 17.00, + "Algeria TVA 19%": { + "account_name": "TVA 19%", + "tax_rate": 19.00, "default": 1 }, - "Algeria VAT 7%": { - "account_name": "VAT 7%", - "tax_rate": 7.00 + "Algeria TVA 9%": { + "account_name": "TVA 9%", + "tax_rate": 9.00 } }, From f349be0a00c13f06e480589b80b1bd10509d356c Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 12 Jan 2026 00:23:02 +0530 Subject: [PATCH 18/38] refactor: remove redundant onload function for bank mapping table (cherry picked from commit 7c7ba0154a3c9947e75ef7c8ba35403670bc287f) --- erpnext/accounts/doctype/bank/bank.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index 77af313dc16..9181b6576d3 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -3,9 +3,6 @@ frappe.provide("erpnext.integrations"); frappe.ui.form.on("Bank", { - onload: function (frm) { - add_fields_to_mapping_table(frm); - }, refresh: function (frm) { add_fields_to_mapping_table(frm); frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal); From d4195d31bf49e889b43bb82d1af266057e4cc607 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 12 Jan 2026 00:30:11 +0530 Subject: [PATCH 19/38] fix: handle undefined bank_transaction_mapping in quick entry (cherry picked from commit 8a1b8259bdf709fcfa72f71e7779cfd11c054adb) --- erpnext/accounts/doctype/bank/bank.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index 9181b6576d3..12cde75324d 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -34,11 +34,11 @@ let add_fields_to_mapping_table = function (frm) { }); }); - frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property( - "bank_transaction_field", - "options", - options - ); + const grid = frm.fields_dict.bank_transaction_mapping?.grid; + + if (grid) { + grid.update_docfield_property("bank_transaction_field", "options", options); + } }; erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { From e1c3125efafd03d5a4f8d2ca9436e2f305160fb9 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Sat, 24 Jan 2026 11:34:34 +0530 Subject: [PATCH 20/38] refactor: use console.error for error logging in Plaid integration (cherry picked from commit 9322095786bdd3762f47f8cb33f7aae8a5e3b951) --- erpnext/accounts/doctype/bank/bank.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index 12cde75324d..0a268aa88a2 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -113,7 +113,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { "There was an issue connecting to Plaid's authentication server. Check browser console for more information" ) ); - console.log(error); + console.error(error); } plaid_success(token, response) { From 624ec1930557a4de9ba9615e060ee5c2bad58363 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sat, 24 Jan 2026 13:42:04 +0530 Subject: [PATCH 21/38] chore: fix conflicts Remove test for reserved serial and batch items and clean up related code. --- .../doctype/work_order/test_work_order.py | 382 ------------------ 1 file changed, 382 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index f07e9ba013e..f1c9b706ca0 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -3186,337 +3186,6 @@ class TestWorkOrder(FrappeTestCase): allow_overproduction("overproduction_percentage_for_work_order", 0) -<<<<<<< HEAD -======= - def test_reserved_serial_batch(self): - raw_materials = [] - for item_code, properties in { - "Test Reserved FG Item": {"is_stock_item": 1}, - "Test Reserved Serial Item": {"has_serial_no": 1, "serial_no_series": "TSNN-RSI-.####"}, - "Test Reserved Batch Item": { - "has_batch_no": 1, - "batch_number_series": "BCH-RBI-.####", - "create_new_batch": 1, - }, - "Test Reserved Serial Batch Item": { - "has_serial_no": 1, - "serial_no_series": "TSNB-RSBI-.####", - "has_batch_no": 1, - "batch_number_series": "BCH-RSBI-.####", - "create_new_batch": 1, - }, - }.items(): - make_item(item_code, properties=properties) - if item_code != "Test Reserved FG Item": - raw_materials.append(item_code) - test_stock_entry.make_stock_entry( - item_code=item_code, - target="Stores - _TC", - qty=5, - basic_rate=100, - ) - - original_auto_reserve = frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch") - original_backflush = frappe.db.get_single_value( - "Manufacturing Settings", "backflush_raw_materials_based_on" - ) - frappe.db.set_single_value( - "Manufacturing Settings", - "backflush_raw_materials_based_on", - "Material Transferred for Manufacture", - ) - frappe.db.set_single_value("Stock Settings", "auto_reserve_serial_and_batch", 1) - - make_bom( - item="Test Reserved FG Item", - source_warehouse="Stores - _TC", - raw_materials=raw_materials, - ) - - wo = make_wo_order_test_record( - item="Test Reserved FG Item", - qty=5, - source_warehouse="Stores - _TC", - reserve_stock=1, - ) - - _reserved_item = get_reserved_entries(wo.name) - for key, value in _reserved_item.items(): - self.assertEqual(key[1], "Stores - _TC") - self.assertEqual(value.reserved_qty, 5) - if value.serial_nos: - self.assertEqual(len(value.serial_nos), 5) - - if value.batch_nos: - self.assertEqual(sum(value.batch_nos.values()), 5) - - # Transfer 5 qty - mt_stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 5)) - mt_stock_entry.submit() - - for row in mt_stock_entry.items: - value = _reserved_item[(row.item_code, row.s_warehouse)] - self.assertEqual(row.qty, value.reserved_qty) - if value.serial_nos: - serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle) - self.assertEqual(sorted(serial_nos), sorted(value.serial_nos)) - - if value.batch_nos: - self.assertTrue(row.batch_no in value.batch_nos) - - _before_reserved_item = get_reserved_entries(wo.name, mt_stock_entry.items[0].t_warehouse) - - # Manufacture 2 qty - fg_stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 2)) - fg_stock_entry.submit() - - for row in fg_stock_entry.items: - if not row.s_warehouse: - continue - - value = _before_reserved_item[(row.item_code, row.s_warehouse)] - if row.serial_no: - serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle) - for sn in serial_nos: - self.assertTrue(sn in value.serial_nos) - value.serial_nos.remove(sn) - - if row.batch_no: - self.assertTrue(row.batch_no in value.batch_nos) - value.batch_nos[row.batch_no] -= row.qty - if row.serial_no: - sns = get_serial_nos_from_bundle(row.serial_and_batch_bundle) - for sn in sns: - self.assertTrue(sn in value.serial_batches[row.batch_no]) - value.serial_batches[row.batch_no].remove(sn) - - # Manufacture 3 qty - fg_stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3)) - fg_stock_entry.submit() - - for row in fg_stock_entry.items: - if not row.s_warehouse: - continue - - value = _before_reserved_item[(row.item_code, row.s_warehouse)] - - if row.serial_no: - serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle) - self.assertEqual(sorted(serial_nos), sorted(value.serial_nos)) - - if row.batch_no: - self.assertTrue(row.batch_no in value.batch_nos) - self.assertEqual(value.batch_nos[row.batch_no], row.qty) - if row.serial_no: - sns = get_serial_nos_from_bundle(row.serial_and_batch_bundle) - self.assertEqual(sorted(sns), sorted(value.serial_batches[row.batch_no])) - - frappe.db.set_single_value( - "Manufacturing Settings", "backflush_raw_materials_based_on", original_backflush - ) - frappe.db.set_single_value("Stock Settings", "auto_reserve_serial_and_batch", original_auto_reserve) - - def test_phantom_bom_item_not_in_additional_cost(self): - """Test that phantom BOMs are not added to additional costs, - but regular non-stock items in the FG BOM are added.""" - - from erpnext.stock.doctype.item.test_item import make_item - - # Create items: - # - FG Item (stock item) - # - Phantom sub-assembly (non-stock item to be phantom) - # - Phantom RM (stock item - component of phantom BOM) - # - Packing Material (non-stock item - directly in FG BOM) - # - Regular RM (stock item - directly in FG BOM) - - fg_item = make_item( - "Test FG Item For Phantom Non Stock", - {"is_stock_item": 1, "valuation_rate": 100}, - ).name - - phantom_item = make_item( - "Test Phantom Sub Assembly Non Stock", - {"is_stock_item": 0, "valuation_rate": 0}, - ).name - - phantom_rm = make_item( - "Test Phantom RM Item", - {"is_stock_item": 1, "valuation_rate": 200}, - ).name - - packing_material = make_item( - "Test Packing Material Non Stock", - {"is_stock_item": 0, "valuation_rate": 150}, - ).name - - regular_rm = make_item( - "Test Regular RM Stock Item", - {"is_stock_item": 1, "valuation_rate": 100}, - ).name - - # Create price list entries for non-stock items - price_list = "_Test Price List India" - for item_code, rate in [ - (phantom_item, 500), - (phantom_rm, 200), - (packing_material, 150), - ]: - if not frappe.db.get_value("Item Price", {"item_code": item_code, "price_list": price_list}): - frappe.get_doc( - { - "doctype": "Item Price", - "item_code": item_code, - "price_list_rate": rate, - "price_list": price_list, - } - ).insert(ignore_permissions=True) - - # Create Phantom BOM (for the phantom sub-assembly) - phantom_bom = frappe.get_doc( - { - "doctype": "BOM", - "item": phantom_item, - "is_default": 1, - "is_active": 1, - "is_phantom_bom": 1, # Mark as phantom BOM - "currency": "INR", - "quantity": 1, - "company": "_Test Company", - "rm_cost_as_per": "Price List", - "buying_price_list": price_list, - } - ) - phantom_bom.append( - "items", - { - "item_code": phantom_rm, - "qty": 1, - "rate": 200, - }, - ) - phantom_bom.insert() - phantom_bom.submit() - - # Create FG BOM with phantom item, packing material, and regular RM - fg_bom = frappe.get_doc( - { - "doctype": "BOM", - "item": fg_item, - "is_default": 1, - "is_active": 1, - "currency": "INR", - "quantity": 1, - "company": "_Test Company", - "rm_cost_as_per": "Price List", - "buying_price_list": price_list, - } - ) - - # Add phantom item (will be marked as is_phantom_item based on is_phantom_bom) - fg_bom.append( - "items", - { - "item_code": phantom_item, - "qty": 1, - "rate": 200, - "bom_no": phantom_bom.name, - }, - ) - - # Add packing material (non-stock, directly in FG BOM) - fg_bom.append( - "items", - { - "item_code": packing_material, - "qty": 1, - "rate": 150, - }, - ) - - # Add regular RM (stock item) - fg_bom.append( - "items", - { - "item_code": regular_rm, - "qty": 1, - "rate": 100, - }, - ) - - fg_bom.insert() - fg_bom.submit() - - # Ensure stock - test_stock_entry.make_stock_entry( - item_code=regular_rm, - target="_Test Warehouse - _TC", - qty=10, - basic_rate=100, - ) - - test_stock_entry.make_stock_entry( - item_code=phantom_rm, - target="_Test Warehouse - _TC", - qty=10, - basic_rate=200, - ) - - # Create work order - wo = make_wo_order_test_record( - production_item=fg_item, - bom_no=fg_bom.name, - qty=1, - source_warehouse="_Test Warehouse - _TC", - ) - - # Transfer materials - se_transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 1)) - se_transfer.insert() - se_transfer.submit() - - # Manufacture - se_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1)) - se_manufacture.insert() - - # Verify additional costs - self.assertTrue(se_manufacture.additional_costs, "Additional costs should not be empty") - total_additional_cost = sum(row.amount for row in se_manufacture.additional_costs) - - self.assertEqual( - total_additional_cost, - 150, # only packing material; phantom RM excluded - f"Additional cost should be 150 (packing material only), got {total_additional_cost}", - ) - - self.assertEqual( - se_manufacture.total_outgoing_value, - 300, # 100 (regular RM) + 200 (phantom RM) - f"Total outgoing value should be 300, got {se_manufacture.total_outgoing_value}", - ) - - self.assertEqual( - se_manufacture.total_incoming_value, - 450, # 300 (RM total) + 150 (packing material) - f"Total incoming value should be 450, got {se_manufacture.total_incoming_value}", - ) - - # Clean up - se_manufacture.submit() - se_manufacture.cancel() - se_transfer.cancel() - wo.reload() - wo.cancel() - fg_bom.cancel() - phantom_bom.cancel() - - def test_phantom_bom_explosion(self): - from erpnext.manufacturing.doctype.bom.test_bom import create_tree_for_phantom_bom_tests - - expected = create_tree_for_phantom_bom_tests() - - wo = make_wo_order_test_record(item="Top Level Parent") - self.assertEqual([item.item_code for item in wo.required_items], expected) - def test_reserved_qty_for_pp_with_extra_material_transfer(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( make_stock_entry as make_stock_entry_test_record, @@ -3565,57 +3234,6 @@ class TestWorkOrder(FrappeTestCase): self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0) -def get_reserved_entries(voucher_no, warehouse=None): - doctype = frappe.qb.DocType("Stock Reservation Entry") - sabb = frappe.qb.DocType("Serial and Batch Entry") - - query = ( - frappe.qb.from_(doctype) - .left_join(sabb) - .on(doctype.name == sabb.parent) - .select( - doctype.name, - doctype.item_code, - doctype.warehouse, - doctype.reserved_qty, - sabb.serial_no, - sabb.batch_no, - sabb.qty, - sabb.delivered_qty, - ) - .where((doctype.voucher_no == voucher_no) & (doctype.docstatus == 1)) - ) - - if warehouse: - query = query.where(doctype.warehouse == warehouse) - - reservation_entries = query.run(as_dict=True) - - _reserved_item = frappe._dict({}) - for entry in reservation_entries: - key = (entry.item_code, entry.warehouse) - if key not in _reserved_item: - _reserved_item[key] = frappe._dict( - { - "reserved_qty": 0, - "serial_nos": [], - "batch_nos": defaultdict(int), - "serial_batches": defaultdict(list), - } - ) - - _reserved_item[key].reserved_qty += entry.qty - if entry.batch_no: - _reserved_item[key].batch_nos[entry.batch_no] += entry.qty - if entry.serial_no: - _reserved_item[key].serial_batches[entry.batch_no].append(entry.serial_no) - if entry.serial_no: - _reserved_item[key].serial_nos.append(entry.serial_no) - - return _reserved_item - ->>>>>>> f5378b6573 (fix: Bin reserved qty for production for extra material transfer) - def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( make_stock_entry as make_stock_entry_test_record, From a4b099e481a6fb3acf18518469b7323f3c9bef1e Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sat, 24 Jan 2026 13:50:33 +0530 Subject: [PATCH 22/38] chore: fix conflicts Removed subcontracting order validation methods from stock entry. --- erpnext/stock/doctype/stock_entry/stock_entry.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a4a699ab1de..d313c037e02 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -243,15 +243,8 @@ class StockEntry(StockController): self.reset_default_field_value("to_warehouse", "items", "t_warehouse") self.validate_same_source_target_warehouse_during_material_transfer() - -<<<<<<< HEAD -======= - self.validate_closed_subcontracting_order() - self.validate_subcontract_order() self.validate_raw_materials_exists() - super().validate_subcontracting_inward() - def validate_raw_materials_exists(self): if self.purpose not in ["Manufacture", "Repack", "Disassemble"]: return @@ -272,7 +265,6 @@ class StockEntry(StockController): title=_("Raw Materials Missing"), ) ->>>>>>> f003b3c378 (fix: validation to check at-least one raw material for manufacture entry) def set_serial_batch_for_disassembly(self): if self.purpose != "Disassemble": return From c351d6b1c0b5f782c09968b998dee4abcf613fee Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sat, 24 Jan 2026 13:51:54 +0530 Subject: [PATCH 23/38] chore: fix conflicts Removed old implementation of make_serialized_item function and updated its definition. --- .../doctype/stock_entry/test_stock_entry.py | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 076231193fa..2383fabaf89 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2232,44 +2232,6 @@ class TestStockEntry(FrappeTestCase): se.save() se.submit() - -<<<<<<< HEAD -def make_serialized_item(**args): -======= - warehouse = "_Test Warehouse - _TC" - retain_sample_item = make_item( - "Retain Sample Item", - properties={ - "is_stock_item": 1, - "retain_sample": 1, - "sample_quantity": 2, - "has_batch_no": 1, - "has_serial_no": 1, - "create_new_batch": 1, - "batch_number_series": "SAMPLE-RET-.#####", - "serial_no_series": "SAMPLE-RET-SN-.#####", - }, - ) - material_receipt = make_stock_entry( - item_code=retain_sample_item.item_code, target=warehouse, qty=10, purpose="Material Receipt" - ) - - source_sabb = frappe.get_doc( - "Serial and Batch Bundle", material_receipt.items[0].serial_and_batch_bundle - ) - batch = source_sabb.entries[0].batch_no - serial_nos = [entry.serial_no for entry in source_sabb.entries] - - sample_entry = frappe.get_doc( - move_sample_to_retention_warehouse(material_receipt.company, material_receipt.items) - ) - sample_entry.submit() - target_sabb = frappe.get_doc("Serial and Batch Bundle", sample_entry.items[0].serial_and_batch_bundle) - - self.assertEqual(sample_entry.items[0].transfer_qty, 2) - self.assertEqual(target_sabb.entries[0].batch_no, batch) - self.assertEqual([entry.serial_no for entry in target_sabb.entries], serial_nos[:2]) - def test_raw_material_missing_validation(self): original_value = frappe.db.get_single_value("Manufacturing Settings", "material_consumption") frappe.db.set_single_value("Manufacturing Settings", "material_consumption", 0) @@ -2293,8 +2255,7 @@ def make_serialized_item(**args): frappe.db.set_single_value("Manufacturing Settings", "material_consumption", original_value) -def make_serialized_item(self, **args): ->>>>>>> f003b3c378 (fix: validation to check at-least one raw material for manufacture entry) +def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) From 14de520ebb9307004faa34199f80ba756fa2e360 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 24 Jan 2026 14:15:56 +0530 Subject: [PATCH 24/38] fix: UOM of item not fetching in BOM (cherry picked from commit ba8eadda5296318308e90fb47efadd622676aa90) --- erpnext/manufacturing/doctype/bom/bom.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 313a9de4e4b..c48c56df3d5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -553,8 +553,6 @@ var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) { do_not_explode: d.do_not_explode, }, callback: function (r) { - d = locals[cdt][cdn]; - $.extend(d, r.message); refresh_field("items"); refresh_field("scrap_items"); From 5e6192249e5dc20c322f00f9d26e4353f5239d71 Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:41:30 +0100 Subject: [PATCH 25/38] fix: swedish_address_template (cherry picked from commit 334e8ada30422d336e191425328480ed554b36f6) --- erpnext/regional/address_template/templates/sweden.html | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 erpnext/regional/address_template/templates/sweden.html diff --git a/erpnext/regional/address_template/templates/sweden.html b/erpnext/regional/address_template/templates/sweden.html new file mode 100644 index 00000000000..0c2ed73f0ae --- /dev/null +++ b/erpnext/regional/address_template/templates/sweden.html @@ -0,0 +1,4 @@ +{{ address_line1 }}
+{% if address_line2 %}{{ address_line2 }}
{% endif -%} +{{ pincode }} {{ city | upper }}
+{{ country | upper }} \ No newline at end of file From 853facad96410205b2ebf9b26e68c9085e2e6395 Mon Sep 17 00:00:00 2001 From: Shankarv19bcr Date: Mon, 26 Jan 2026 14:37:55 +0530 Subject: [PATCH 26/38] fix: strip whitespace in customer_name (cherry picked from commit e5ba0e640191c541745a579dd474a6a20b9257db) --- erpnext/selling/doctype/customer/customer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 1c1ae08b280..7fb15943c9f 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -114,6 +114,7 @@ class Customer(TransactionBase): set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) def get_customer_name(self): + self.customer_name = self.customer_name.strip() if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import: count = frappe.db.sql( """select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer From 74bf61e0c152507b954ff8e07326d6dbcd771cdb Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Mon, 26 Jan 2026 21:17:06 +0530 Subject: [PATCH 27/38] fix(asset capitalization): update asset values using db_set --- erpnext/assets/doctype/asset/asset.py | 5 ++++- .../asset_capitalization/asset_capitalization.py | 15 ++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c9968c7a34d..7637192ba9b 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -669,7 +669,10 @@ class Asset(AccountsController): def get_status(self): """Returns status based on whether it is draft, submitted, scrapped or depreciated""" if self.docstatus == 0: - status = "Draft" + if self.is_composite_asset: + status = "Work In Progress" + else: + status = "Draft" elif self.docstatus == 1: status = "Submitted" diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 674cb3ffa3d..588894d7c10 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -611,14 +611,15 @@ class AssetCapitalization(StockController): asset_doc = frappe.get_doc("Asset", self.target_asset) if self.docstatus == 2: - asset_doc.gross_purchase_amount -= total_target_asset_value - asset_doc.purchase_amount -= total_target_asset_value + gross_purchase_amount = asset_doc.gross_purchase_amount - total_target_asset_value + purchase_amount = asset_doc.purchase_amount - total_target_asset_value + asset_doc.db_set("total_asset_cost", asset_doc.total_asset_cost - total_target_asset_value) else: - asset_doc.gross_purchase_amount += total_target_asset_value - asset_doc.purchase_amount += total_target_asset_value - asset_doc.set_status("Work In Progress") - asset_doc.flags.ignore_validate = True - asset_doc.save() + gross_purchase_amount = asset_doc.gross_purchase_amount + total_target_asset_value + purchase_amount = asset_doc.purchase_amount + total_target_asset_value + + asset_doc.db_set("gross_purchase_amount", gross_purchase_amount) + asset_doc.db_set("purchase_amount", purchase_amount) frappe.msgprint( _("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format( From 5cfd8d19308590ace440a61cf00b09f9452483de Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Mon, 26 Jan 2026 23:06:37 +0530 Subject: [PATCH 28/38] refactor: avoid multiple db_set --- .../asset_capitalization/asset_capitalization.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 588894d7c10..d234b162ba2 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -613,13 +613,19 @@ class AssetCapitalization(StockController): if self.docstatus == 2: gross_purchase_amount = asset_doc.gross_purchase_amount - total_target_asset_value purchase_amount = asset_doc.purchase_amount - total_target_asset_value - asset_doc.db_set("total_asset_cost", asset_doc.total_asset_cost - total_target_asset_value) + total_asset_cost = asset_doc.total_asset_cost - total_target_asset_value else: gross_purchase_amount = asset_doc.gross_purchase_amount + total_target_asset_value purchase_amount = asset_doc.purchase_amount + total_target_asset_value + total_asset_cost = asset_doc.total_asset_cost + total_target_asset_value - asset_doc.db_set("gross_purchase_amount", gross_purchase_amount) - asset_doc.db_set("purchase_amount", purchase_amount) + asset_doc.db_set( + { + "gross_purchase_amount": gross_purchase_amount, + "purchase_amount": purchase_amount, + "total_asset_cost": total_asset_cost, + } + ) frappe.msgprint( _("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format( From 72a9b58b14c310da75e7632ae71c987945edd66f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 05:52:23 +0000 Subject: [PATCH 29/38] fix(journal-entry): prevent submit failure due to double background queuing (backport #52083) (#52086) Co-authored-by: V Shankar fix(journal-entry): prevent submit failure due to double background queuing (#52083) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index e7c0832554f..2ed9881772c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -172,7 +172,7 @@ class JournalEntry(AccountsController): validate_docs_for_deferred_accounting([self.name], []) def submit(self): - if len(self.accounts) > 100: + if len(self.accounts) > 100 and not self.meta.queue_in_background: queue_submission(self, "_submit") else: return self._submit() From 934b5494f012b05e245c4a35816119322edcfec0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 06:32:59 +0000 Subject: [PATCH 30/38] fix(payment entry): update currency symbol (backport #51956) (#52093) Co-authored-by: NaviN <118178330+Navin-S-R@users.noreply.github.com> fix(payment entry): update currency symbol (#51956) --- .../accounts/doctype/payment_entry/payment_entry.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index db2004b8c8f..580af69c404 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -400,6 +400,16 @@ frappe.ui.form.on("Payment Entry", { ); frm.refresh_fields(); + + const party_currency = + frm.doc.payment_type === "Receive" ? "paid_from_account_currency" : "paid_to_account_currency"; + + var reference_grid = frm.fields_dict["references"].grid; + ["total_amount", "outstanding_amount", "allocated_amount"].forEach((fieldname) => { + reference_grid.update_docfield_property(fieldname, "options", party_currency); + }); + + reference_grid.refresh(); }, show_general_ledger: function (frm) { From 0c89cd55248a7d9bab6750375fb6ae7c9a4e339c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 27 Jan 2026 11:07:52 +0530 Subject: [PATCH 31/38] fix: show message if image is removed from item description (cherry picked from commit b49c679a508f70a3c4ade05f4c7e618e6546b7a8) # Conflicts: # erpnext/stock/doctype/item/item.py --- erpnext/stock/doctype/item/item.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index fdaaa23c5af..bbcab3ca922 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -227,9 +227,33 @@ class Item(Document): def validate_description(self): """Clean HTML description if set""" +<<<<<<< HEAD if cint(frappe.db.get_single_value("Stock Settings", "clean_description_html")): +======= + if ( + cint(frappe.get_single_value("Stock Settings", "clean_description_html")) + and self.description != self.item_name # perf: Avoid cleaning up a fallback + ): + old_desc = self.description +>>>>>>> b49c679a50 (fix: show message if image is removed from item description) self.description = clean_html(self.description) + if ( + old_desc + and self.description + and " Date: Tue, 27 Jan 2026 12:28:07 +0530 Subject: [PATCH 32/38] fix(stock): use purchase UOM in Supplier Quotation items --- erpnext/stock/get_item_details.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index b0ade4324fa..312b8e129f8 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -421,9 +421,10 @@ def get_basic_details(args, item, overwrite_warehouse=True): if not args.get("uom"): if args.get("doctype") in sales_doctypes: args.uom = item.sales_uom if item.sales_uom else item.stock_uom - elif (args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]) or ( - args.get("doctype") == "Material Request" and args.get("material_request_type") == "Purchase" - ): + elif ( + args.get("doctype") + in ["Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation"] + ) or (args.get("doctype") == "Material Request" and args.get("material_request_type") == "Purchase"): args.uom = item.purchase_uom if item.purchase_uom else item.stock_uom else: args.uom = item.stock_uom From 90d6bb34dce3478b2efbb3cbd910d18621f4f896 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 27 Jan 2026 14:38:19 +0530 Subject: [PATCH 33/38] chore: resolve conflicts --- erpnext/stock/doctype/item/item.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index bbcab3ca922..2f98aff00f2 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -227,15 +227,8 @@ class Item(Document): def validate_description(self): """Clean HTML description if set""" -<<<<<<< HEAD if cint(frappe.db.get_single_value("Stock Settings", "clean_description_html")): -======= - if ( - cint(frappe.get_single_value("Stock Settings", "clean_description_html")) - and self.description != self.item_name # perf: Avoid cleaning up a fallback - ): old_desc = self.description ->>>>>>> b49c679a50 (fix: show message if image is removed from item description) self.description = clean_html(self.description) if ( From 468ec805f101283124797063865644f113a7c718 Mon Sep 17 00:00:00 2001 From: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:45:33 +0530 Subject: [PATCH 34/38] fix: check the payment ledger entry has the dimension (#51823) * fix: check the payment ledger entry has the dimension * fix: add project in payment ledger entry (cherry picked from commit efa3973b77f500e334bd290c094003a1b0078c1d) --- .../doctype/payment_ledger_entry/payment_ledger_entry.json | 6 ++++++ .../doctype/payment_ledger_entry/payment_ledger_entry.py | 1 + .../payment_reconciliation/payment_reconciliation.py | 2 +- erpnext/accounts/utils.py | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json index 30a2da24bbb..80333239ad6 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -132,6 +132,12 @@ "fieldtype": "Link", "label": "Cost Center", "options": "Cost Center" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" }, { "fieldname": "due_date", diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py index c7cc97d7197..bcf3ddec01a 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py @@ -38,6 +38,7 @@ class PaymentLedgerEntry(Document): amount_in_account_currency: DF.Currency company: DF.Link | None cost_center: DF.Link | None + project: DF.Link | None delinked: DF.Check due_date: DF.Date | None finance_book: DF.Link | None diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index c81863f044f..9ec4e0a073a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -746,7 +746,7 @@ class PaymentReconciliation(Document): ple = qb.DocType("Payment Ledger Entry") for x in self.dimensions: dimension = x.fieldname - if self.get(dimension): + if self.get(dimension) and frappe.db.has_column("Payment Ledger Entry", dimension): self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension)) def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False): diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5a7c6ca9055..2a977dd2c03 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1868,6 +1868,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0): account=gle.account, party_type=gle.party_type, party=gle.party, + project=gle.project, cost_center=gle.cost_center, finance_book=gle.finance_book, due_date=gle.due_date, From 90dc22a57d51c54b4b2c1ccd7a0c33276cef3fc5 Mon Sep 17 00:00:00 2001 From: harrishragavan Date: Tue, 27 Jan 2026 09:28:24 +0530 Subject: [PATCH 35/38] fix(shipment): user contact validation to use full name (cherry picked from commit 3c6eb9a531c839733b2e43a118b76ac1e846587b) --- erpnext/stock/doctype/shipment/shipment.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js index 7f1e1c8d729..f22139c89f5 100644 --- a/erpnext/stock/doctype/shipment/shipment.js +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -261,9 +261,9 @@ frappe.ui.form.on("Shipment", { frappe.db.get_value( "User", { name: frappe.session.user }, - ["full_name", "last_name", "email", "phone", "mobile_no"], + ["full_name", "email", "phone", "mobile_no"], (r) => { - if (!(r.last_name && r.email && (r.phone || r.mobile_no))) { + if (!(r.full_name && r.email && (r.phone || r.mobile_no))) { if (delivery_type == "Delivery") { frm.set_value("delivery_company", ""); frm.set_value("delivery_contact", ""); @@ -272,9 +272,9 @@ frappe.ui.form.on("Shipment", { frm.set_value("pickup_contact", ""); } frappe.throw( - __("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") + + __("Full Name, Email or Phone/Mobile of the user are mandatory to continue.") + "
" + - __("Please first set Last Name, Email and Phone for the user") + + __("Please first set Full Name, Email and Phone for the user") + ` ${frappe.session.user}` ); } From a09b73e65d98253f1a6f370504cb6408bed87496 Mon Sep 17 00:00:00 2001 From: SowmyaArunachalam Date: Wed, 21 Jan 2026 16:11:04 +0530 Subject: [PATCH 36/38] fix(sales order): set project at item level from parent (cherry picked from commit 9e51701e2ad34d4410d5f68ebfdebe77e86141c0) --- erpnext/public/js/utils/sales_common.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index 7e2271dc38f..cd59fc5ff0f 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -488,7 +488,16 @@ erpnext.sales_common = { } } - project() { + project(doc, cdt, cdn) { + var item = frappe.get_doc(cdt, cdn); + if (item.project) { + $.each(this.frm.doc["items"] || [], function (i, other_item) { + if (!other_item.project) { + other_item.project = item.project; + refresh_field("project", other_item.name, other_item.parentfield); + } + }); + } let me = this; if (["Delivery Note", "Sales Invoice", "Sales Order"].includes(this.frm.doc.doctype)) { if (this.frm.doc.project) { From e12564daa6101ce083f18cd09e3aa908f4d854a3 Mon Sep 17 00:00:00 2001 From: SowmyaArunachalam Date: Tue, 27 Jan 2026 11:36:51 +0530 Subject: [PATCH 37/38] chore: use frappe.model.set_value (cherry picked from commit 3b27f49d795d8634f5925fe9804df3d79e54904c) --- erpnext/public/js/utils/sales_common.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index cd59fc5ff0f..1adbe402744 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -489,12 +489,16 @@ erpnext.sales_common = { } project(doc, cdt, cdn) { - var item = frappe.get_doc(cdt, cdn); + const item = frappe.get_doc(cdt, cdn); if (item.project) { $.each(this.frm.doc["items"] || [], function (i, other_item) { if (!other_item.project) { - other_item.project = item.project; - refresh_field("project", other_item.name, other_item.parentfield); + frappe.model.set_value( + other_item.doctype, + other_item.name, + "project", + item.project + ); } }); } From 7146c0385cdf13322c80ff8daf112dfc4856d295 Mon Sep 17 00:00:00 2001 From: SowmyaArunachalam Date: Tue, 27 Jan 2026 15:37:14 +0530 Subject: [PATCH 38/38] fix: handle parent level project change (cherry picked from commit 543b6e51c08f281bd733cd0d0ceb44a480f9cf35) --- erpnext/public/js/utils/sales_common.js | 34 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index 1adbe402744..c9b5ed7e6f2 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -489,18 +489,28 @@ erpnext.sales_common = { } project(doc, cdt, cdn) { - const item = frappe.get_doc(cdt, cdn); - if (item.project) { - $.each(this.frm.doc["items"] || [], function (i, other_item) { - if (!other_item.project) { - frappe.model.set_value( - other_item.doctype, - other_item.name, - "project", - item.project - ); - } - }); + if (!cdt || !cdn) { + if (this.frm.doc.project) { + $.each(this.frm.doc["items"] || [], function (i, item) { + if (!item.project) { + frappe.model.set_value(item.doctype, item.name, "project", doc.project); + } + }); + } + } else { + const item = frappe.get_doc(cdt, cdn); + if (item.project) { + $.each(this.frm.doc["items"] || [], function (i, other_item) { + if (!other_item.project) { + frappe.model.set_value( + other_item.doctype, + other_item.name, + "project", + item.project + ); + } + }); + } } let me = this; if (["Delivery Note", "Sales Invoice", "Sales Order"].includes(this.frm.doc.doctype)) {