From 3f2081b440f1a85160911232a20c92272f18d687 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Sun, 16 Nov 2025 08:36:30 +0530 Subject: [PATCH 01/39] fix: prevent pi status from changing on asset repair --- erpnext/assets/doctype/asset_repair/asset_repair.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 358945edf87..7198c7efac0 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -305,7 +305,6 @@ class AssetRepair(AccountsController): "cost_center": self.cost_center, "posting_date": self.completion_date, "against_voucher_type": "Purchase Invoice", - "against_voucher": self.purchase_invoice, "company": self.company, }, item=self, From 85c0c1696430d75af4f6d2c9cd527e96a699335c Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Wed, 12 Nov 2025 11:32:41 +0530 Subject: [PATCH 02/39] fix: validate sabb autocreation when disabled (cherry picked from commit 3ca19408811bbf26819c6ba65fc9522a8c80aa4a) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.py --- erpnext/stock/doctype/stock_entry/stock_entry.py | 9 +++++++++ erpnext/stock/serial_batch_bundle.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 76c57ae873d..8d72aae7506 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1135,8 +1135,17 @@ class StockEntry(StockController): "ignore_serial_nos": already_picked_serial_nos, "qty": row.transfer_qty * -1, } +<<<<<<< HEAD ).update_serial_and_batch_entries() elif not row.serial_and_batch_bundle: +======= + ).update_serial_and_batch_entries( + serial_nos=serial_nos.get(row.name), batch_nos=batch_nos.get(row.name) + ) + elif not row.serial_and_batch_bundle and frappe.get_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward" + ): +>>>>>>> 3ca1940881 (fix: validate sabb autocreation when disabled) bundle_doc = SerialBatchCreation( { "item_code": row.item_code, diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index db9cf422b5f..2ee85fc2c24 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -259,7 +259,7 @@ class SerialBatchBundle: and not self.sle.serial_and_batch_bundle and self.item_details.has_batch_no == 1 and ( - self.item_details.create_new_batch + (self.item_details.create_new_batch and self.sle.actual_qty > 0) or ( frappe.db.get_single_value( "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward" From 8ccb9a5ad2049bbc5effb98095e6cbbd41b14a75 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Thu, 13 Nov 2025 08:54:44 +0000 Subject: [PATCH 03/39] fix: add return status for purchase receipt (cherry picked from commit 3a0e1e8ef95603c98a9c4ca9e66694ac32cd257d) --- erpnext/controllers/status_updater.py | 1 + .../doctype/purchase_receipt/purchase_receipt.json | 4 ++-- .../stock/doctype/purchase_receipt/purchase_receipt.py | 10 +++++++++- .../doctype/purchase_receipt/purchase_receipt_list.js | 2 +- .../doctype/purchase_receipt/test_purchase_receipt.py | 3 ++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 15c2760332e..0e94cfd95de 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -93,6 +93,7 @@ status_map = { ["Draft", None], ["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"], ["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"], + ["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"], ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], [ "Completed", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 603f4a121d1..f220ed3736e 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -893,7 +893,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nPartly Billed\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", + "options": "\nDraft\nPartly Billed\nTo Bill\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed", "print_hide": 1, "print_width": "150px", "read_only": 1, @@ -1300,7 +1300,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2025-08-06 16:41:02.690658", + "modified": "2025-11-12 19:53:48.173096", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c4f878fe85d..53aad87e2da 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -117,7 +117,15 @@ class PurchaseReceipt(BuyingController): shipping_address_display: DF.SmallText | None shipping_rule: DF.Link | None status: DF.Literal[ - "", "Draft", "Partly Billed", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed" + "", + "Draft", + "Partly Billed", + "To Bill", + "Completed", + "Return", + "Return Issued", + "Cancelled", + "Closed", ] subcontracting_receipt: DF.Link | None supplied_items: DF.Table[PurchaseReceiptItemSupplied] diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js index d70b357d731..30562e23de8 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js @@ -11,7 +11,7 @@ frappe.listview_settings["Purchase Receipt"] = { "currency", ], get_indicator: function (doc) { - if (cint(doc.is_return) == 1) { + if (cint(doc.is_return) == 1 && doc.status == "Return") { return [__("Return"), "gray", "is_return,=,Yes"]; } else if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 383421f3c67..959c58b2380 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -455,6 +455,7 @@ class TestPurchaseReceipt(FrappeTestCase): # Check if Original PR updated self.assertEqual(pr.items[0].returned_qty, 2) self.assertEqual(pr.per_returned, 40) + self.assertEqual(returned.status, "Return") from erpnext.controllers.sales_and_purchase_return import make_return_doc @@ -2128,7 +2129,7 @@ class TestPurchaseReceipt(FrappeTestCase): return_pr.items[0].stock_qty = 0.0 return_pr.submit() - self.assertEqual(return_pr.status, "To Bill") + self.assertEqual(return_pr.status, "Return") pi = make_purchase_invoice(return_pr.name) pi.submit() From 36e9aae9d05770598ecb839b259a94e1fcad04d0 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 18 Nov 2025 18:04:29 +0530 Subject: [PATCH 04/39] chore: fix conflicts --- 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 8d72aae7506..0ead7461d67 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1135,17 +1135,9 @@ class StockEntry(StockController): "ignore_serial_nos": already_picked_serial_nos, "qty": row.transfer_qty * -1, } -<<<<<<< HEAD - ).update_serial_and_batch_entries() - elif not row.serial_and_batch_bundle: -======= - ).update_serial_and_batch_entries( - serial_nos=serial_nos.get(row.name), batch_nos=batch_nos.get(row.name) - ) elif not row.serial_and_batch_bundle and frappe.get_single_value( "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward" ): ->>>>>>> 3ca1940881 (fix: validate sabb autocreation when disabled) bundle_doc = SerialBatchCreation( { "item_code": row.item_code, From 876dec5077217788df16c0cc109b110e6bc8b809 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 18 Nov 2025 18:05:08 +0530 Subject: [PATCH 05/39] chore: fix conflicts --- erpnext/stock/doctype/stock_entry/stock_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 0ead7461d67..9664ed67498 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1135,6 +1135,7 @@ class StockEntry(StockController): "ignore_serial_nos": already_picked_serial_nos, "qty": row.transfer_qty * -1, } + ).update_serial_and_batch_entries() elif not row.serial_and_batch_bundle and frappe.get_single_value( "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward" ): From 5b1674018b9e58d60fe657b0494c4901dec7e01c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 19 Nov 2025 12:11:56 +0530 Subject: [PATCH 06/39] fix: redundant message on bom save (cherry picked from commit 074f07694fb04553c034ca84d0f7ec7797bbec7c) # Conflicts: # erpnext/manufacturing/doctype/bom/bom.py --- erpnext/manufacturing/doctype/bom/bom.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 22264fb0e92..af1eac402e6 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -465,7 +465,7 @@ class BOM(WebsiteGenerator): ) ) - def get_rm_rate(self, arg): + def get_rm_rate(self, arg, notify=True): """Get raw material rate as per selected method, if bom exists takes bom cost""" rate = 0 if not self.rm_cost_as_per: @@ -491,7 +491,7 @@ class BOM(WebsiteGenerator): ), alert=True, ) - else: + elif notify: frappe.msgprint( _("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]), alert=True, @@ -796,7 +796,13 @@ class BOM(WebsiteGenerator): "stock_uom": d.stock_uom, "conversion_factor": d.conversion_factor, "sourced_by_supplier": d.sourced_by_supplier, +<<<<<<< HEAD } +======= + "is_phantom_item": d.is_phantom_item, + }, + notify=False, +>>>>>>> 074f07694f (fix: redundant message on bom save) ) d.base_rate = flt(d.rate) * flt(self.conversion_rate) From 2550b44db88869b7cc23eb32d19e36d8bbf9cb30 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 19 Nov 2025 12:38:47 +0530 Subject: [PATCH 07/39] chore: resolve conflicts --- erpnext/manufacturing/doctype/bom/bom.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index af1eac402e6..89f90e1658c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -796,13 +796,8 @@ class BOM(WebsiteGenerator): "stock_uom": d.stock_uom, "conversion_factor": d.conversion_factor, "sourced_by_supplier": d.sourced_by_supplier, -<<<<<<< HEAD - } -======= - "is_phantom_item": d.is_phantom_item, }, notify=False, ->>>>>>> 074f07694f (fix: redundant message on bom save) ) d.base_rate = flt(d.rate) * flt(self.conversion_rate) From 015f946a1471e1c062ae27d1d30c7f23e6053950 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 19 Nov 2025 16:07:50 +0530 Subject: [PATCH 08/39] fix: add filter company and status to job card employee (cherry picked from commit 3ca3a6d9bba4878a73172cd5cf49976742007b0f) --- erpnext/manufacturing/doctype/job_card/job_card.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 9d3c646598b..69d0cc8fbd8 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -38,6 +38,15 @@ frappe.ui.form.on("Job Card", { return doc.status === "Complete" ? "green" : "orange"; } }); + + frm.set_query("employee", () => { + return { + filters: { + company: frm.doc.company, + status: "Active", + }, + }; + }); }, refresh: function (frm) { From 1d6e3e4e7dd39d9fad07eca6b205fbe88b358e78 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 19 Nov 2025 16:13:56 +0530 Subject: [PATCH 09/39] fix: show current company warehouse only in get material from bom MR (cherry picked from commit 3271eaaf0ebbb16553724a11620e9fbffc4cf9f2) --- erpnext/stock/doctype/material_request/material_request.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 243ba7adac8..f295f33853f 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -332,6 +332,9 @@ frappe.ui.form.on("Material Request", { label: __("For Warehouse"), options: "Warehouse", reqd: 1, + get_query: function () { + return { filters: { company: frm.doc.company } }; + }, }, { fieldname: "qty", fieldtype: "Float", label: __("Quantity"), reqd: 1, default: 1 }, { From c85ce55f275c4627b29e40508ab7805ff379a5c7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:30:39 +0000 Subject: [PATCH 10/39] Merge pull request #50631 from frappe/mergify/bp/version-15-hotfix/pr-50629 fix: process loss % can be negative (backport #50629) --- erpnext/manufacturing/doctype/bom/bom.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 04774d4f2a8..ab8586aaef1 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -548,12 +548,14 @@ { "fieldname": "process_loss_percentage", "fieldtype": "Percent", - "label": "% Process Loss" + "label": "% Process Loss", + "non_negative": 1 }, { "fieldname": "process_loss_qty", "fieldtype": "Float", "label": "Process Loss Qty", + "non_negative": 1, "read_only": 1 }, { @@ -639,7 +641,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2025-10-29 17:43:12.966753", + "modified": "2025-11-19 16:17:15.925156", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", From 673b893942ca789d7ea554120b75c2f444b28780 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Wed, 19 Nov 2025 01:23:26 +0530 Subject: [PATCH 11/39] fix: ignore reserved batches from total available batches --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 6 +++++- erpnext/stock/stock_ledger.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) 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 d9071c406a2..690e3c81c92 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 @@ -2297,7 +2297,11 @@ def get_auto_batch_nos(kwargs): stock_ledgers_batches = get_stock_ledgers_batches(kwargs) pos_invoice_batches = get_reserved_batches_for_pos(kwargs) - sre_reserved_batches = get_reserved_batches_for_sre(kwargs) + + sre_reserved_batches = frappe._dict() + if not kwargs.ignore_reserved_stock: + sre_reserved_batches = get_reserved_batches_for_sre(kwargs) + picked_batches = frappe._dict() if kwargs.get("is_pick_list"): picked_batches = get_picked_batches(kwargs) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f4956e1b600..8b61ca56983 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2313,6 +2313,7 @@ def validate_reserved_batch_nos(kwargs): "posting_date": kwargs.posting_date, "posting_time": kwargs.posting_time, "ignore_voucher_nos": kwargs.ignore_voucher_nos, + "ignore_reserved_stock": True, } ) ) From 55f2f1c515133a1fdf2152bd44e88f26c965267f Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Wed, 19 Nov 2025 01:24:07 +0530 Subject: [PATCH 12/39] test: add unit test for reserved stock validation --- .../test_stock_reservation_entry.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py index ddbf0b2dc25..942c7f482ae 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py @@ -678,6 +678,36 @@ class TestStockReservationEntry(FrappeTestCase): # Test - 1: ValidationError should be thrown as the inwarded stock is reserved. self.assertRaises(frappe.ValidationError, se.cancel) + @change_settings("Stock Settings", {"allow_negative_stock": 0, "enable_stock_reservation": 1}) + def test_reserved_stock_validation_for_batch_item(self): + item_properties = { + "is_stock_item": 1, + "valuation_rate": 100, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SRBV-.#####.", + } + sr_item = make_item(item_code="Test Reserve Item", properties=item_properties) + # inward 100 qty of stock + create_material_receipt(items={sr_item.name: sr_item}, warehouse=self.warehouse, qty=100) + + # reserve 80 qty from sales order + so = make_sales_order(item_code=sr_item.name, warehouse=self.warehouse, qty=80) + so.create_stock_reservation_entries() + + # create a material issue entry including the reserved qty 10 + se = make_stock_entry( + item_code=sr_item.name, + qty=30, + from_warehouse=self.warehouse, + rate=100, + purpose="Material Issue", + do_not_submit=True, + ) + + # validation for reserved stock should be thrown + self.assertRaises(frappe.ValidationError, se.submit) + def tearDown(self) -> None: cancel_all_stock_reservation_entries() return super().tearDown() From 89fcdbf56b5496d53ab432e1283df5992df6ec88 Mon Sep 17 00:00:00 2001 From: Karuppasamy B Date: Thu, 13 Nov 2025 01:16:48 +0530 Subject: [PATCH 13/39] fix(purchase_receipt): add internal_and_external_links field to show purchase invoice connection count (cherry picked from commit 6c1620ab8cb816733de4e613af67ff4374ed0f84) --- .../doctype/purchase_receipt/purchase_receipt_dashboard.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py index b1b0a962246..628b4628f79 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py @@ -18,6 +18,9 @@ def get_data(): "Purchase Order": ["items", "purchase_order"], "Project": ["items", "project"], }, + "internal_and_external_links": { + "Purchase Invoice": ["items", "purchase_invoice"], + }, "transactions": [ { "label": _("Related"), From 6b6e017e36e4367afe41204c2c3c4754719325f5 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Fri, 31 Oct 2025 14:00:37 +0530 Subject: [PATCH 14/39] feat: modify accounting dimension as multiselect field (cherry picked from commit 3fcd8d84acfa8aac0e4e01b860e153a1c3aa1f29) --- .../accounts_payable/accounts_payable.js | 15 +++++------- .../accounts_payable_summary.js | 15 +++++------- .../accounts_receivable.js | 15 +++++------- .../accounts_receivable_summary.js | 15 +++++------- .../report/trial_balance/trial_balance.js | 23 ++++++++++--------- 5 files changed, 36 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 7c3ced847ad..9af8c93a52e 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -26,16 +26,13 @@ frappe.query_reports["Accounts Payable"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: () => { - var company = frappe.query_report.get_filter_value("company"); - return { - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "party_account", diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index ac9d5bfbd01..18a85af95be 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -45,16 +45,13 @@ frappe.query_reports["Accounts Payable Summary"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: () => { - var company = frappe.query_report.get_filter_value("company"); - return { - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "party_type", diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index c5bfe4de0ff..b052d50838d 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -28,16 +28,13 @@ frappe.query_reports["Accounts Receivable"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: () => { - var company = frappe.query_report.get_filter_value("company"); - return { - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "party_type", diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index ae0bddaa766..1ac2b27ca71 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -45,16 +45,13 @@ frappe.query_reports["Accounts Receivable Summary"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: () => { - var company = frappe.query_report.get_filter_value("company"); - return { - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "party_type", diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index 2f701900cf7..8c08c35b3e1 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -47,22 +47,23 @@ frappe.query_reports["Trial Balance"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: function () { - var company = frappe.query_report.get_filter_value("company"); - return { - doctype: "Cost Center", - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "project", label: __("Project"), - fieldtype: "Link", + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Project", txt, { + company: frappe.query_report.get_filter_value("company"), + }); + }, options: "Project", }, { From 02a1f815da9f8df1ab21142087fe15a7f741446e Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Fri, 31 Oct 2025 19:16:35 +0530 Subject: [PATCH 15/39] feat(reports): preserve accounting dimension filters while navigating between reports (cherry picked from commit fcfcaa76c6a2cf190655c226ba773053aa88bb42) --- .../accounts_receivable.py | 7 ++----- .../report/trial_balance/trial_balance.py | 15 +++---------- erpnext/public/js/financial_statements.js | 21 +++++++++++++++++-- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 1fb93846735..9ca930bd829 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, get_dimension_with_children, ) +from erpnext.accounts.report.financial_statements import get_cost_centers_with_children from erpnext.accounts.utils import ( build_qb_match_conditions, get_advance_payment_doctypes, @@ -994,11 +995,7 @@ class ReceivablePayableReport: self.add_accounting_dimensions_filters() def get_cost_center_conditions(self): - lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) - cost_center_list = [ - center.name - for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)}) - ] + cost_center_list = get_cost_centers_with_children(self.filters.cost_center) self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list)) def add_common_filters(self): diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 3f75f1e2b4c..2ea76c82975 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.report.financial_statements import ( filter_accounts, filter_out_zero_value_rows, + get_cost_centers_with_children, set_gl_entries_by_account, ) from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency @@ -103,10 +104,6 @@ def get_data(filters): opening_balances = get_opening_balances(filters, ignore_is_opening) - # add filter inside list so that the query in financial_statements.py doesn't break - if filters.project: - filters.project = [filters.project] - set_gl_entries_by_account( filters.company, filters.from_date, @@ -270,18 +267,12 @@ def get_opening_balance( opening_balance = opening_balance.where(closing_balance.voucher_type != "Period Closing Voucher") if filters.cost_center: - lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"]) - cost_center = frappe.qb.DocType("Cost Center") opening_balance = opening_balance.where( - closing_balance.cost_center.isin( - frappe.qb.from_(cost_center) - .select("name") - .where((cost_center.lft >= lft) & (cost_center.rgt <= rgt)) - ) + closing_balance.cost_center.isin(get_cost_centers_with_children(filters.get("cost_center"))) ) if filters.project: - opening_balance = opening_balance.where(closing_balance.project == filters.project) + opening_balance = opening_balance.where(closing_balance.project.isin(filters.project)) if frappe.db.count("Finance Book"): if filters.get("include_default_book_entries"): diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index f9eddcf2889..e6f257d39b8 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -79,18 +79,35 @@ erpnext.financial_statements = { }, open_general_ledger: function (data) { if (!data.account && !data.accounts) return; - let project = $.grep(frappe.query_report.filters, function (e) { + let filters = frappe.query_report.filters; + + let project = $.grep(filters, function (e) { return e.df.fieldname == "project"; }); + let cost_center = $.grep(filters, function (e) { + return e.df.fieldname == "cost_center"; + }); + frappe.route_options = { account: data.account || data.accounts, company: frappe.query_report.get_filter_value("company"), from_date: data.from_date || data.year_start_date, to_date: data.to_date || data.year_end_date, - project: project && project.length > 0 ? project[0].$input.val() : "", + project: project && project.length > 0 ? project[0].get_value() : "", + cost_center: cost_center && cost_center.length > 0 ? cost_center[0].get_value() : "", }; + filters.forEach((f) => { + if (f.df.fieldtype == "MultiSelectList") { + if (f.df.fieldname in frappe.route_options) return; + let value = f.get_value(); + if (value && value.length > 0) { + frappe.route_options[f.df.fieldname] = value; + } + } + }); + let report = "General Ledger"; if (["Payable", "Receivable"].includes(data.account_type)) { From 0bc98b609fcf70a8037038671a8ebd357caffada Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 20 Nov 2025 13:00:24 +0530 Subject: [PATCH 16/39] fix: validation for SABB deletion (cherry picked from commit dd4bef070651333f9ce33edb2b0b334a45a34b5e) --- .../serial_and_batch_bundle.py | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) 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 690e3c81c92..3e8df4dd8e5 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 @@ -1347,7 +1347,36 @@ class SerialandBatchBundle(Document): if self.voucher_type == "POS Invoice": return - if frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1: + child_doctype = self.voucher_type + " Item" + mapper = { + "Asset Capitalization": "Asset Capitalization Stock Item", + "Asset Repair": "Asset Repair Consumed Item", + "Stock Entry": "Stock Entry Detail", + }.get(self.voucher_type) + + if mapper: + child_doctype = mapper + + if self.voucher_type == "Delivery Note" and not frappe.db.exists( + "Delivery Note Item", self.voucher_detail_no + ): + child_doctype = "Packed Item" + + elif self.voucher_type == "Sales Invoice" and not frappe.db.exists( + "Sales Invoice Item", self.voucher_detail_no + ): + child_doctype = "Packed Item" + + elif self.voucher_type == "Subcontracting Receipt" and not frappe.db.exists( + "Subcontracting Receipt Item", self.voucher_detail_no + ): + child_doctype = "Subcontracting Receipt Supplied Item" + + if ( + frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1 + and self.voucher_detail_no + and frappe.db.exists(child_doctype, self.voucher_detail_no) + ): msg = f"""The {self.voucher_type} {bold(self.voucher_no)} is in submitted state, please cancel it first""" frappe.throw(_(msg)) From 25cd230471060d2672ea754cc3ec933d8eceeb1c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 20 Nov 2025 14:57:33 +0530 Subject: [PATCH 17/39] fix: serial batch selector shown only once (cherry picked from commit aa6f09e9a9f9466680235a1246b7852466b6369b) --- erpnext/stock/doctype/stock_entry/stock_entry.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 61c270bd170..6a6bb4c95b4 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -951,12 +951,10 @@ frappe.ui.form.on("Stock Entry Detail", { no_batch_serial_number_value = true; } - if ( - no_batch_serial_number_value && - !frappe.flags.hide_serial_batch_dialog && - !frappe.flags.dialog_set - ) { - frappe.flags.dialog_set = true; + if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog) { + if (!frappe.flags.dialog_set) { + frappe.flags.dialog_set = true; + } erpnext.stock.select_batch_and_serial_no(frm, d); } else { frappe.flags.dialog_set = false; From aa94c91c12689e04d56c24e7a55d567f65609951 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 20 Nov 2025 15:42:27 +0530 Subject: [PATCH 18/39] fix: remove disabled warehouse in get_warehouses_based_on_account (cherry picked from commit ff2d9bf4cb49d384083a2c0dadf1d7571246703b) --- erpnext/stock/doctype/warehouse/warehouse.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 4600c2bbaae..a1991dd9c07 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -221,7 +221,9 @@ def get_child_warehouses(warehouse): def get_warehouses_based_on_account(account, company=None): warehouses = [] - for d in frappe.get_all("Warehouse", fields=["name", "is_group"], filters={"account": account}): + for d in frappe.get_all( + "Warehouse", fields=["name", "is_group"], filters={"account": account, "disabled": 0} + ): if d.is_group: warehouses.extend(get_child_warehouses(d.name)) else: From a24733791d3070a89d06c29a8e39751623e5fe4b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 20 Nov 2025 15:34:39 +0530 Subject: [PATCH 19/39] fix: unhide zero val checkbox (cherry picked from commit 20e0313a8c4d1d94a19cc9e82d9f04ad31a059f2) # Conflicts: # erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json --- .../doctype/stock_reconciliation/stock_reconciliation.py | 1 + .../stock_reconciliation_item.json | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 937b130cf7b..cc4b08b50f4 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -979,6 +979,7 @@ class StockReconciliation(StockController): is_customer_item = frappe.db.get_value("Item", d.item_code, "is_customer_provided_item") if is_customer_item and d.valuation_rate: d.valuation_rate = 0.0 + d.allow_zero_valuation_rate = 1 changed_any_values = True if changed_any_values: diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index 4d3a87c70aa..47e59d7e9e8 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -185,12 +185,10 @@ }, { "default": "0", - "depends_on": "allow_zero_valuation_rate", "fieldname": "allow_zero_valuation_rate", "fieldtype": "Check", "label": "Allow Zero Valuation Rate", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "depends_on": "barcode", @@ -256,7 +254,11 @@ ], "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2025-03-12 16:34:51.326821", +======= + "modified": "2025-11-20 15:27:13.868179", +>>>>>>> 20e0313a8c (fix: unhide zero val checkbox) "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", From e3b2cc24b2ce3440c7126c197f0ba82400821b63 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 20 Nov 2025 16:12:41 +0530 Subject: [PATCH 20/39] chore: fix conflicts --- .../stock_reconciliation_item.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index 47e59d7e9e8..e61591694e0 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -254,11 +254,7 @@ ], "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2025-03-12 16:34:51.326821", -======= "modified": "2025-11-20 15:27:13.868179", ->>>>>>> 20e0313a8c (fix: unhide zero val checkbox) "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", @@ -269,4 +265,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From 4ba4da090dace52f0f1d49a30cad9dc0b36c625c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 20 Nov 2025 11:49:55 +0530 Subject: [PATCH 21/39] fix(product bundle): fields reset if doc is new (cherry picked from commit 7faee7edc2c713024d12fa659a5e1da9646a35c8) --- erpnext/stock/doctype/packed_item/packed_item.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 5a4f3e7722d..65bec2d7ea3 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -108,7 +108,12 @@ def get_indexed_packed_items_table(doc): """ indexed_table = {} for packed_item in doc.get("packed_items"): - key = (packed_item.parent_item, packed_item.item_code, packed_item.parent_detail_docname) + key = ( + packed_item.parent_item, + packed_item.item_code, + packed_item.idx if doc.is_new() else packed_item.parent_detail_docname, + ) + indexed_table[key] = packed_item return indexed_table @@ -169,7 +174,11 @@ def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, re exists, pi_row = False, {} # check if row already exists in packed items table - key = (main_item_row.item_code, packing_item.item_code, main_item_row.name) + key = ( + main_item_row.item_code, + packing_item.item_code, + main_item_row.idx if doc.is_new() else main_item_row.name, + ) if packed_items_table.get(key): pi_row, exists = packed_items_table.get(key), True From b1d40de87eaeb8e579bab893db619c045bcfaa80 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Fri, 21 Nov 2025 06:18:43 +0530 Subject: [PATCH 22/39] fix(customer): link contact and addresses if created from lead/opportunity/prospect (cherry picked from commit 310099f4cd7e1b14a8e81bf0cdcd3b747f49007a) --- erpnext/selling/doctype/customer/customer.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 9bebfa1e086..466009c772a 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -232,7 +232,7 @@ class Customer(TransactionBase): self.update_lead_status() if self.flags.is_new_doc: - self.link_lead_address_and_contact() + self.link_address_and_contact() self.copy_communication() self.update_customer_groups() @@ -272,15 +272,23 @@ class Customer(TransactionBase): if self.lead_name: frappe.db.set_value("Lead", self.lead_name, "status", "Converted") - def link_lead_address_and_contact(self): - if self.lead_name: - # assign lead address and contact to customer (if already not set) + def link_address_and_contact(self): + linked_documents = { + "Lead": self.lead_name, + "Opportunity": self.opportunity_name, + "Prospect": self.prospect_name, + } + for doctype, docname in linked_documents.items(): + # assign lead, opportunity and prospect address and contact to customer (if already not set) + if not docname: + continue + linked_contacts_and_addresses = frappe.get_all( "Dynamic Link", filters=[ ["parenttype", "in", ["Contact", "Address"]], - ["link_doctype", "=", "Lead"], - ["link_name", "=", self.lead_name], + ["link_doctype", "=", doctype], + ["link_name", "=", docname], ], fields=["parent as name", "parenttype as doctype"], ) From 2809c46a6e175f095fe98e9445b25eb3c46e1bfd Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 20 Nov 2025 17:40:28 +0530 Subject: [PATCH 23/39] fix: pick list status doesn't update when DN created from it and PL was created from SO (cherry picked from commit f7b3253683b0e532236762a8b855fa51ce82feff) --- erpnext/stock/doctype/pick_list/pick_list.py | 33 ++++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 330d01edb3d..043795501e2 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -644,10 +644,12 @@ class PickList(TransactionBase): def update_bundle_picked_qty(self): product_bundles = self._get_product_bundles() - product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values()) + product_bundle_qty_map = self._get_product_bundle_qty_map( + next(iter(product_bundles.values())).get("item_code") + ) - for so_row, item_code in product_bundles.items(): - picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code]) + for so_row, value in product_bundles.items(): + picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[value.item_code]) item_table = "Sales Order Item" already_picked = frappe.db.get_value(item_table, so_row, "picked_qty", for_update=True) frappe.db.set_value( @@ -770,15 +772,23 @@ class PickList(TransactionBase): if not item.product_bundle_item: continue - product_bundles[item.sales_order_item] = frappe.db.get_value( - "Sales Order Item", - item.sales_order_item, - "item_code", + product_bundles[item.sales_order_item] = frappe._dict( + { + "item_code": frappe.db.get_value( + "Sales Order Item", + item.sales_order_item, + "item_code", + ), + "pick_list_item": item.name, + } ) return product_bundles def _get_product_bundle_qty_map(self, bundles: list[str]) -> dict[str, dict[str, float]]: # bundle_item_code: Dict[component, qty] + if isinstance(bundles, str): + bundles = [bundles] + product_bundle_qty_map = {} for bundle_item_code in bundles: bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0}) @@ -1386,17 +1396,20 @@ def add_product_bundles_to_delivery_note( When mapping pick list items, the bundle item itself isn't part of the locations. Dynamically fetch and add parent bundle item into DN.""" product_bundles = pick_list._get_product_bundles() - product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values()) + product_bundle_qty_map = pick_list._get_product_bundle_qty_map( + next(iter(product_bundles.values())).get("item_code") + ) - for so_row, item_code in product_bundles.items(): + for so_row, value in product_bundles.items(): sales_order_item = frappe.get_doc("Sales Order Item", so_row) if sales_order and sales_order_item.parent != sales_order: continue dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper) dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle( - so_row, product_bundle_qty_map[item_code] + so_row, product_bundle_qty_map[value.item_code] ) + dn_bundle_item.pick_list_item = value.pick_list_item dn_bundle_item.against_pick_list = pick_list.name update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note) From 45bc218acbc63214f985131d70263a08b55bbc87 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 21 Nov 2025 10:51:19 +0530 Subject: [PATCH 24/39] fix: tests (cherry picked from commit d26f8aa62937364917ced7a51dfb7e1bdfd5e20e) --- erpnext/stock/doctype/pick_list/pick_list.py | 22 +++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 043795501e2..e2cfb8eeca8 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -7,7 +7,7 @@ from itertools import groupby import frappe from frappe import _, bold -from frappe.model.mapper import get_mapped_doc, map_child_doc +from frappe.model.mapper import map_child_doc from frappe.query_builder import Case from frappe.query_builder.custom import GROUP_CONCAT from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum @@ -644,9 +644,7 @@ class PickList(TransactionBase): def update_bundle_picked_qty(self): product_bundles = self._get_product_bundles() - product_bundle_qty_map = self._get_product_bundle_qty_map( - next(iter(product_bundles.values())).get("item_code") - ) + product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values()) for so_row, value in product_bundles.items(): picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[value.item_code]) @@ -784,15 +782,11 @@ class PickList(TransactionBase): ) return product_bundles - def _get_product_bundle_qty_map(self, bundles: list[str]) -> dict[str, dict[str, float]]: - # bundle_item_code: Dict[component, qty] - if isinstance(bundles, str): - bundles = [bundles] - + def _get_product_bundle_qty_map(self, bundles) -> dict[str, dict[str, float]]: product_bundle_qty_map = {} - for bundle_item_code in bundles: - bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0}) - product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items} + for data in bundles: + bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": data.item_code, "disabled": 0}) + product_bundle_qty_map[data.item_code] = {item.item_code: item.qty for item in bundle.items} return product_bundle_qty_map def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int: @@ -1396,9 +1390,7 @@ def add_product_bundles_to_delivery_note( When mapping pick list items, the bundle item itself isn't part of the locations. Dynamically fetch and add parent bundle item into DN.""" product_bundles = pick_list._get_product_bundles() - product_bundle_qty_map = pick_list._get_product_bundle_qty_map( - next(iter(product_bundles.values())).get("item_code") - ) + product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values()) for so_row, value in product_bundles.items(): sales_order_item = frappe.get_doc("Sales Order Item", so_row) From f62e5e69b855edf5da59f19ac13053a95b10ed70 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 21 Nov 2025 11:58:33 +0530 Subject: [PATCH 25/39] fix: pricing rule was ignoring time validity (cherry picked from commit ffae7c4175ded3c34f16332f9ed4bc41826ab6d2) --- erpnext/accounts/doctype/pricing_rule/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index fde3b9e48dd..2d841b75519 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -243,10 +243,13 @@ def get_other_conditions(conditions, values, args): if group_condition: conditions += " and " + group_condition - if args.get("transaction_date"): + date = args.get("transaction_date") or frappe.get_value( + args.get("doctype"), args.get("name"), "posting_date" + ) + if date: conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01') and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')""" - values["transaction_date"] = args.get("transaction_date") + values["transaction_date"] = date if args.get("doctype") in [ "Quotation", From 7ed3c6d18a51e8151d571f0168447b5e594f41a2 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Tue, 11 Nov 2025 10:26:31 +0000 Subject: [PATCH 26/39] fix: use current_tax_amount value for base_total_taxes_and_charges (cherry picked from commit 5a3fcbedb55ca46376657f8b62e3036e2390c1a4) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 4196b81c9ba..bfa28630d6f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1800,7 +1800,7 @@ class PaymentEntry(AccountsController): else: self.total_taxes_and_charges += current_tax_amount - self.base_total_taxes_and_charges += tax.base_tax_amount + self.base_total_taxes_and_charges += current_tax_amount if self.get("taxes"): self.paid_amount_after_tax = self.get("taxes")[-1].base_total From 2e9a0cb01c22fe682cade44ada4d4ca2cf6981b2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 23 Nov 2025 19:16:39 +0530 Subject: [PATCH 27/39] fix: unknown column error (cherry picked from commit 3b7d7aed4c8091033bdea02fcb806c0b4e0b9d18) --- erpnext/accounts/doctype/pricing_rule/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 2d841b75519..c6875b53ed8 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -244,7 +244,7 @@ def get_other_conditions(conditions, values, args): conditions += " and " + group_condition date = args.get("transaction_date") or frappe.get_value( - args.get("doctype"), args.get("name"), "posting_date" + args.get("doctype"), args.get("name"), "posting_date", ignore=True ) if date: conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01') From 2678694c5f2185f3a4f55394e9f491b3b86de0ad Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Wed, 19 Nov 2025 14:19:26 +0000 Subject: [PATCH 28/39] fix(manufacturing): apply precision for bom amount and rm_cost_per_qty (cherry picked from commit 57f9353d90be7bdc393c647dee1c0b5d9b07a095) --- erpnext/manufacturing/doctype/bom/bom.py | 4 +++- .../doctype/subcontracting_order/subcontracting_order.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 89f90e1658c..72d238cfba2 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -801,7 +801,9 @@ class BOM(WebsiteGenerator): ) d.base_rate = flt(d.rate) * flt(self.conversion_rate) - d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")) + d.amount = flt( + flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")), d.precision("amount") + ) d.base_amount = d.amount * flt(self.conversion_rate) d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt( self.quantity, self.precision("quantity") diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 2f9a04e7e93..4f87e695dfc 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -187,7 +187,7 @@ class SubcontractingOrder(SubcontractingController): for item in self.get("items"): bom = frappe.get_doc("BOM", item.bom) rm_cost = sum(flt(rm_item.amount) for rm_item in bom.items) - item.rm_cost_per_qty = rm_cost / flt(bom.quantity) + item.rm_cost_per_qty = flt(rm_cost / flt(bom.quantity), item.precision("rm_cost_per_qty")) def calculate_items_qty_and_amount(self): total_qty = total = 0 From 5b60fbbd309c7b4b6511439d6d6656f6a08870f5 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Fri, 21 Nov 2025 06:43:20 +0000 Subject: [PATCH 29/39] fix: apply precision for scrap items amount (cherry picked from commit 9194e6350aa92e88e10638718c1121c55ea45300) --- erpnext/manufacturing/doctype/bom/bom.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 72d238cfba2..a6fad4ee1aa 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -826,7 +826,10 @@ class BOM(WebsiteGenerator): d.base_rate = flt(d.rate, d.precision("rate")) * flt( self.conversion_rate, self.precision("conversion_rate") ) - d.amount = flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")) + d.amount = flt( + flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")), + d.precision("amount"), + ) d.base_amount = flt(d.amount, d.precision("amount")) * flt( self.conversion_rate, self.precision("conversion_rate") ) From 6cffba9a71de2256249ef48866ad2baf609b2209 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 07:17:43 +0000 Subject: [PATCH 30/39] Merge pull request #50713 from frappe/mergify/bp/version-15-hotfix/pr-50712 --- .../manufacturing/workspace/manufacturing/manufacturing.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 320db68e037..602dfcc04b0 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -318,7 +318,7 @@ "type": "Link" } ], - "modified": "2024-10-21 14:13:38.777556", + "modified": "2025-11-24 11:11:28.343568", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -336,7 +336,7 @@ "doc_view": "List", "label": "Learn Manufacturing", "type": "URL", - "url": "https://school.frappe.io/lms/courses/manufacturing?utm_source=in_app" + "url": "https://school.frappe.io/lms/courses/production-planning-and-execution" }, { "color": "Grey", From 475eada727780bc7d3fcab7a4bc7ca3ffe0e1ffa Mon Sep 17 00:00:00 2001 From: "El-Shafei H." Date: Mon, 24 Nov 2025 09:51:42 +0300 Subject: [PATCH 31/39] fix: add missing translate function (cherry picked from commit 56def01240a050f654419309e1b015327676075f) --- erpnext/assets/doctype/asset/asset.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index f28860d325b..d2a86acc837 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -202,7 +202,7 @@ frappe.ui.form.on("Asset", { callback: function (r) { if (!r.message) { $(".primary-action").prop("hidden", true); - $(".form-message").text("Capitalize this asset to confirm"); + $(".form-message").text(__("Capitalize this asset to confirm")); frm.add_custom_button(__("Capitalize Asset"), function () { frm.trigger("create_asset_capitalization"); From 1995291194078c725c8de645e38b76b905bb9ebc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:18:18 +0000 Subject: [PATCH 32/39] fix: add validation for FG Items as per BOM qty (backport #50579) (#50715) Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com> Co-authored-by: Mihir Kandoi Co-authored-by: Kavin <78342682+kavin-114@users.noreply.github.com> fix: add validation for FG Items as per BOM qty (#50579) --- .../buying_settings/buying_settings.json | 11 +- .../buying_settings/buying_settings.py | 1 + .../controllers/subcontracting_controller.py | 4 +- .../subcontracting_receipt.py | 65 ++++++++++- .../test_subcontracting_receipt.py | 108 +++++++++++++++++- 5 files changed, 180 insertions(+), 9 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 170a68fb394..f9c3da285e1 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -36,6 +36,7 @@ "backflush_raw_materials_of_subcontract_based_on", "column_break_11", "over_transfer_allowance", + "validate_consumed_qty", "section_break_xcug", "auto_create_subcontracting_order", "column_break_izrr", @@ -270,6 +271,14 @@ "label": "Fixed Outgoing Email Account", "link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]", "options": "Email Account" + }, + { + "default": "0", + "depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"Material Transferred for Subcontract\"", + "description": "Raw materials consumed qty will be validated based on FG BOM required qty", + "fieldname": "validate_consumed_qty", + "fieldtype": "Check", + "label": "Validate Consumed Qty (as per BOM)" } ], "grid_page_length": 50, @@ -278,7 +287,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-08-20 22:13:38.506889", + "modified": "2025-11-20 12:59:09.925862", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index 8b83418f6f8..3634f8a9069 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -44,6 +44,7 @@ class BuyingSettings(Document): supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"] supplier_group: DF.Link | None use_transaction_date_exchange_rate: DF.Check + validate_consumed_qty: DF.Check # end: auto-generated types def validate(self): diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 36341a090dc..bbb5429303b 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -505,7 +505,7 @@ class SubcontractingController(StockController): if item.get("serial_and_batch_bundle"): frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True) - def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): + def _get_materials_from_bom(self, item_code, bom_no, exploded_item=0): doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] @@ -849,7 +849,7 @@ class SubcontractingController(StockController): if self.doctype == self.subcontract_data.order_doctype or ( self.backflush_based_on == "BOM" or self.is_return ): - for bom_item in self.__get_materials_from_bom( + for bom_item in self._get_materials_from_bom( row.item_code, row.bom, row.get("include_exploded_items") ): qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 520d7bb0e16..15e0259722f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -1,6 +1,8 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from collections import defaultdict + import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc @@ -17,6 +19,10 @@ from erpnext.stock.get_item_details import get_default_cost_center, get_default_ from erpnext.stock.stock_ledger import get_valuation_rate +class BOMQuantityError(frappe.ValidationError): + pass + + class SubcontractingReceipt(SubcontractingController): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -156,6 +162,7 @@ class SubcontractingReceipt(SubcontractingController): def on_submit(self): self.validate_closed_subcontracting_order() self.validate_available_qty_for_consumption() + self.validate_bom_required_qty() self.update_status_updater_args() self.update_prevdoc_status() self.set_subcontracting_order_status(update_bin=False) @@ -512,12 +519,60 @@ class SubcontractingReceipt(SubcontractingController): item.available_qty_for_consumption and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0 ): - msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)} - must be less than or equal to Available Qty For Consumption - {flt(item.available_qty_for_consumption, precision)} - in Consumed Items Table.""" + msg = _( + """Row {0}: Consumed Qty {1} {2} must be less than or equal to Available Qty For Consumption + {3} {4} in Consumed Items Table.""" + ).format( + item.idx, + flt(item.consumed_qty, precision), + item.stock_uom, + flt(item.available_qty_for_consumption, precision), + item.stock_uom, + ) - frappe.throw(_(msg)) + frappe.throw(msg) + + def validate_bom_required_qty(self): + if ( + frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") + == "Material Transferred for Subcontract" + ) and not (frappe.db.get_single_value("Buying Settings", "validate_consumed_qty")): + return + + rm_consumed_dict = self.get_rm_wise_consumed_qty() + + for row in self.items: + precision = row.precision("qty") + for bom_item in self._get_materials_from_bom( + row.item_code, row.bom, row.get("include_exploded_items") + ): + required_qty = flt( + bom_item.qty_consumed_per_unit * row.qty * row.conversion_factor, precision + ) + consumed_qty = rm_consumed_dict.get(bom_item.rm_item_code, 0) + diff = flt(consumed_qty, precision) - flt(required_qty, precision) + + if diff < 0: + msg = _( + """Additional {0} {1} of item {2} required as per BOM to complete this transaction""" + ).format( + frappe.bold(abs(diff)), + frappe.bold(bom_item.stock_uom), + frappe.bold(bom_item.rm_item_code), + ) + + frappe.throw( + msg, + exc=BOMQuantityError, + ) + + def get_rm_wise_consumed_qty(self): + rm_dict = defaultdict(float) + + for row in self.supplied_items: + rm_dict[row.rm_item_code] += row.consumed_qty + + return rm_dict def update_status_updater_args(self): if cint(self.is_return): diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index b9d062af5b2..443ca8cb569 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -38,6 +38,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( make_subcontracting_receipt, ) +from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import ( + BOMQuantityError, +) class TestSubcontractingReceipt(FrappeTestCase): @@ -174,7 +177,7 @@ class TestSubcontractingReceipt(FrappeTestCase): def test_subcontracting_over_receipt(self): """ Behaviour: Raise multiple SCRs against one SCO that in total - receive more than the required qty in the SCO. + receive more than the required qty in the SCO. Expected Result: Error Raised for Over Receipt against SCO. """ from erpnext.controllers.subcontracting_controller import ( @@ -1785,6 +1788,109 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertEqual(scr.items[0].rm_cost_per_qty, 300) self.assertEqual(scr.items[0].service_cost_per_qty, 100) + def test_bom_required_qty_validation_based_on_bom(self): + set_backflush_based_on("BOM") + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) + + fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BRQV-.####", + } + ).name + + make_bom(item=fg_item, raw_materials=[rm_item1], rm_qty=2) + se = make_stock_entry( + item_code=rm_item1, + qty=1, + target="_Test Warehouse 1 - _TC", + rate=300, + ) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 1, + }, + ] + + sco = get_subcontracting_order(service_items=service_items) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.reload() + + self.assertEqual(scr.supplied_items[0].batch_no, batch_no) + self.assertEqual(scr.supplied_items[0].consumed_qty, 1) + self.assertEqual(scr.supplied_items[0].required_qty, 2) + + self.assertRaises(BOMQuantityError, scr.submit) + + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0) + + def test_bom_required_qty_validation_based_on_transfer(self): + from erpnext.controllers.subcontracting_controller import ( + make_rm_stock_entry as make_subcontract_transfer_entry, + ) + + set_backflush_based_on("Material Transferred for Subcontract") + frappe.db.set_single_value("Buying Settings", "validate_consumed_qty", 1) + + item_code = "_Test Subcontracted Validation FG Item 1" + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + } + ).name + + make_subcontracted_item(item_code=item_code, raw_materials=[rm_item1]) + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": item_code, + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order( + service_items=service_items, + include_exploded_items=0, + ) + + # inward raw material stock + make_stock_entry(target="_Test Warehouse - _TC", item_code=rm_item1, qty=10, basic_rate=100) + + rm_items = [ + { + "item_code": item_code, + "rm_item_code": sco.supplied_items[0].rm_item_code, + "qty": sco.supplied_items[0].required_qty - 5, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + ] + + # transfer partial raw materials + ste = frappe.get_doc(make_subcontract_transfer_entry(sco.name, rm_items)) + ste.to_warehouse = "_Test Warehouse 1 - _TC" + ste.save() + ste.submit() + + scr = make_subcontracting_receipt(sco.name) + scr.save() + + self.assertRaises(BOMQuantityError, scr.submit) + def make_return_subcontracting_receipt(**args): args = frappe._dict(args) From e8e09cf8eafe1205585531b28b7f32a27dc1899c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 25 Nov 2025 10:18:04 +0530 Subject: [PATCH 33/39] fix: incorrect query filter when selecting primary customer adr (#50727) (cherry picked from commit c2b8b97d7dcbdfd3cd8a070ce842f339c67ebd43) # Conflicts: # erpnext/selling/doctype/customer/customer.json --- erpnext/buying/doctype/supplier/supplier.js | 8 +++--- erpnext/buying/doctype/supplier/supplier.py | 22 ++++++++++------ erpnext/selling/doctype/customer/customer.js | 9 ++++--- .../selling/doctype/customer/customer.json | 4 +++ erpnext/selling/doctype/customer/customer.py | 26 ++++++++++++------- 5 files changed, 46 insertions(+), 23 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 93fa566dc74..a27244d1528 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -41,18 +41,20 @@ frappe.ui.form.on("Supplier", { frm.set_query("supplier_primary_contact", function (doc) { return { - query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact", + query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary", filters: { supplier: doc.name, + type: "Contact", }, }; }); frm.set_query("supplier_primary_address", function (doc) { return { + query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary", filters: { - link_doctype: "Supplier", - link_name: doc.name, + supplier: doc.name, + type: "Address", }, }; }); diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 3b72953c563..07a2d31166b 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -215,19 +215,25 @@ class Supplier(TransactionBase): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters): +def get_supplier_primary(doctype, txt, searchfield, start, page_len, filters): supplier = filters.get("supplier") - contact = frappe.qb.DocType("Contact") + type = filters.get("type") + type_doctype = frappe.qb.DocType(type) dynamic_link = frappe.qb.DocType("Dynamic Link") - return ( - frappe.qb.from_(contact) + query = ( + frappe.qb.from_(type_doctype) .join(dynamic_link) - .on(contact.name == dynamic_link.parent) - .select(contact.name, contact.email_id) + .on(type_doctype.name == dynamic_link.parent) + .select(type_doctype.name) .where( (dynamic_link.link_name == supplier) & (dynamic_link.link_doctype == "Supplier") - & (contact.name.like(f"%{txt}%")) + & (type_doctype.name.like(f"%{txt}%")) ) - ).run(as_dict=False) + ) + + if type == "Contact": + query = query.select(type_doctype.email_id) + + return query.run() diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 598452276cc..f5a2009e551 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -55,17 +55,20 @@ frappe.ui.form.on("Customer", { frm.set_query("customer_primary_contact", function (doc) { return { - query: "erpnext.selling.doctype.customer.customer.get_customer_primary_contact", + query: "erpnext.selling.doctype.customer.customer.get_customer_primary", filters: { customer: doc.name, + type: "Contact", }, }; }); + frm.set_query("customer_primary_address", function (doc) { return { + query: "erpnext.selling.doctype.customer.customer.get_customer_primary", filters: { - link_doctype: "Customer", - link_name: doc.name, + customer: doc.name, + type: "Address", }, }; }); diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index a04b9c414cf..dff5984feec 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -610,7 +610,11 @@ "link_fieldname": "party" } ], +<<<<<<< HEAD "modified": "2025-03-05 10:01:47.885574", +======= + "modified": "2025-11-25 09:35:56.772949", +>>>>>>> c2b8b97d7d (fix: incorrect query filter when selecting primary customer adr (#50727)) "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 466009c772a..449b56de3b4 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -800,21 +800,29 @@ def make_address(args, is_primary_address=1, is_shipping_address=1): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters): +def get_customer_primary(doctype, txt, searchfield, start, page_len, filters): customer = filters.get("customer") - - con = qb.DocType("Contact") + type = filters.get("type") + type_doctype = qb.DocType(type) dlink = qb.DocType("Dynamic Link") - return ( - qb.from_(con) + query = ( + qb.from_(type_doctype) .join(dlink) - .on(con.name == dlink.parent) - .select(con.name, con.email_id) - .where((dlink.link_name == customer) & (con.name.like(f"%{txt}%"))) - .run() + .on(type_doctype.name == dlink.parent) + .select(type_doctype.name) + .where( + (dlink.link_name == customer) + & (type_doctype.name.like(f"%{txt}%")) + & (dlink.link_doctype == "Customer") + ) ) + if type == "Contact": + query = query.select(type_doctype.email_id) + + return query.run() + def parse_full_name(full_name: str) -> tuple[str, str | None, str | None]: """Parse full name into first name, middle name and last name""" From 2b7d58602d3a85edab89e63d450494c9463b5040 Mon Sep 17 00:00:00 2001 From: Logesh Periyasamy Date: Tue, 25 Nov 2025 10:21:17 +0530 Subject: [PATCH 34/39] feat(accounting-dimension): add dynamic triggers for custom accounting dimensions (#50621) * feat: add dynamic triggers for custom accounting dimensions * feat: add accounting dimension trigger call in setup event * chore: ignore cur_frm semgrep rules * chore: move function to transaction.js (cherry picked from commit 5e58e344b2d57456b7c83d2445babf0f0e38e98d) --- .../purchase_invoice/purchase_invoice.js | 1 + .../doctype/sales_invoice/sales_invoice.js | 1 + .../doctype/purchase_order/purchase_order.js | 1 + erpnext/public/js/controllers/transaction.js | 17 +++++++++++++++++ .../selling/doctype/sales_order/sales_order.js | 3 +++ .../doctype/delivery_note/delivery_note.js | 1 + .../purchase_receipt/purchase_receipt.js | 1 + 7 files changed, 25 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ad367f3c9cb..9a831598328 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller(); erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController { setup(doc) { + this.setup_accounting_dimension_triggers(); this.setup_posting_date_time_check(); super.setup(doc); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 193014d340d..abb5d76b9d8 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -14,6 +14,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( erpnext.selling.SellingController ) { setup(doc) { + this.setup_accounting_dimension_triggers(); this.setup_posting_date_time_check(); super.setup(doc); this.frm.make_methods = { diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index d82a5e18c23..85095e66a57 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -303,6 +303,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( erpnext.buying.BuyingController ) { setup() { + this.setup_accounting_dimension_triggers(); this.frm.custom_make_buttons = { "Purchase Receipt": "Purchase Receipt", "Purchase Invoice": "Purchase Invoice", diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 7b63d78c09b..3abcde36072 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2749,6 +2749,23 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe ]); } } + + setup_accounting_dimension_triggers() { + frappe.call({ + method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", + callback: function (r) { + if (r.message && r.message[0]) { + let dimensions = r.message[0].map((d) => d.fieldname); + dimensions.forEach((dim) => { + // nosemgrep: frappe-semgrep-rules.rules.frappe-cur-frm-usage + cur_frm.cscript[dim] = function (doc, cdt, cdn) { + erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", dim); + }; + }); + } + }, + }); + } }; erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close, show_dialog) { diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 3e0425c1201..ce20966a25e 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -574,6 +574,9 @@ frappe.ui.form.on("Sales Order Item", { }); erpnext.selling.SalesOrderController = class SalesOrderController extends erpnext.selling.SellingController { + setup() { + this.setup_accounting_dimension_triggers(); + } onload(doc, dt, dn) { super.onload(doc, dt, dn); } diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 527d672cc6a..b9c98bbb8ba 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -140,6 +140,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends ( erpnext.selling.SellingController ) { setup(doc) { + this.setup_accounting_dimension_triggers(); this.setup_posting_date_time_check(); super.setup(doc); this.frm.make_methods = { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index db065a80c92..29026969404 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -195,6 +195,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend erpnext.buying.BuyingController ) { setup(doc) { + this.setup_accounting_dimension_triggers(); this.setup_posting_date_time_check(); super.setup(doc); } From 841f5c24ad9b8592b2074cfe32b313c77ca5d51d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 25 Nov 2025 10:51:39 +0530 Subject: [PATCH 35/39] chore: resolve conflicts --- erpnext/selling/doctype/customer/customer.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index dff5984feec..7c0676c672f 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -610,11 +610,7 @@ "link_fieldname": "party" } ], -<<<<<<< HEAD - "modified": "2025-03-05 10:01:47.885574", -======= "modified": "2025-11-25 09:35:56.772949", ->>>>>>> c2b8b97d7d (fix: incorrect query filter when selecting primary customer adr (#50727)) "modified_by": "Administrator", "module": "Selling", "name": "Customer", @@ -700,4 +696,4 @@ "states": [], "title_field": "customer_name", "track_changes": 1 -} \ No newline at end of file +} From 56f03aee027486897a4ed4e327473bf957895112 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Mon, 17 Nov 2025 13:42:43 +0530 Subject: [PATCH 36/39] fix(ledger-summary-report): show party group and territory (cherry picked from commit 231479a6e2e6b8ca97d4c58036c6ac4755c83c63) # Conflicts: # erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py --- .../customer_ledger_summary.py | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 6f5fe349dd2..5b669ba5826 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -69,12 +69,18 @@ class PartyLedgerSummaryReport: party_type = self.filters.party_type doctype = qb.DocType(party_type) + + party_details_fields = [ + doctype.name.as_("party"), + f"{scrub(party_type)}_name", + f"{scrub(party_type)}_group", + ] + + if party_type == "Customer": + party_details_fields.append(doctype.territory) + conditions = self.get_party_conditions(doctype) - query = ( - qb.from_(doctype) - .select(doctype.name.as_("party"), f"{scrub(party_type)}_name") - .where(Criterion.all(conditions)) - ) + query = qb.from_(doctype).select(*party_details_fields).where(Criterion.all(conditions)) from frappe.desk.reportview import build_match_conditions @@ -153,6 +159,31 @@ class PartyLedgerSummaryReport: credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note" + if self.filters.party_type == "Customer": + columns += [ + { + "label": _("Customer Group"), + "fieldname": "customer_group", + "fieldtype": "Link", + "options": "Customer Group", + }, + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + }, + ] + else: + columns += [ + { + "label": _("Supplier Group"), + "fieldname": "supplier_group", + "fieldtype": "Link", + "options": "Supplier Group", + } + ] + columns += [ { "label": _("Opening Balance"), @@ -213,6 +244,7 @@ class PartyLedgerSummaryReport: }, ] +<<<<<<< HEAD # Hidden columns for handling 'User Permissions' if self.filters.party_type == "Customer": columns += [ @@ -242,6 +274,9 @@ class PartyLedgerSummaryReport: } ] +======= + columns.append({"label": _("Dr/Cr"), "fieldname": "dr_or_cr", "fieldtype": "Data", "width": 100}) +>>>>>>> 231479a6e2 (fix(ledger-summary-report): show party group and territory) return columns def get_data(self): From 56cf5382f0285c27482f9d67306d1dac2766c407 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Tue, 18 Nov 2025 13:42:45 +0530 Subject: [PATCH 37/39] test: add party_group, territory in json (cherry picked from commit 8f91919933e5c7ada6e10152e98d9fbcd57c9b63) # Conflicts: # erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py --- .../test_customer_ledger_summary.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py index ca9c62dac6c..2c91717eabc 100644 --- a/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py @@ -151,6 +151,96 @@ class TestCustomerLedgerSummary(FrappeTestCase, AccountsTestMixin): with self.subTest(field=field): self.assertEqual(report[0].get(field), expected_after_cr_and_payment.get(field)) +<<<<<<< HEAD +======= + def test_customer_ledger_ignore_cr_dr_filter(self): + si = create_sales_invoice() + + cr_note = make_return_doc(si.doctype, si.name) + cr_note.submit() + + pr = frappe.get_doc("Payment Reconciliation") + pr.company = si.company + pr.party_type = "Customer" + pr.party = si.customer + pr.receivable_payable_account = si.debit_to + + pr.get_unreconciled_entries() + + invoices = [invoice.as_dict() for invoice in pr.invoices if invoice.invoice_number == si.name] + payments = [payment.as_dict() for payment in pr.payments if payment.reference_name == cr_note.name] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + system_generated_journal = frappe.db.get_all( + "Journal Entry", + filters={ + "docstatus": 1, + "reference_type": si.doctype, + "reference_name": si.name, + "voucher_type": "Credit Note", + "is_system_generated": True, + }, + fields=["name"], + ) + self.assertEqual(len(system_generated_journal), 1) + expected = { + "party": "_Test Customer", + "customer_name": "_Test Customer", + "customer_group": "_Test Customer Group", + "territory": "_Test Territory", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 100.0, + "paid_amount": 0.0, + "return_amount": 100.0, + "closing_balance": 0.0, + "currency": "INR", + "dr_or_cr": "", + } + # Without ignore_cr_dr_notes + columns, data = execute( + frappe._dict( + { + "company": si.company, + "from_date": si.posting_date, + "to_date": si.posting_date, + "ignore_cr_dr_notes": False, + } + ) + ) + self.assertEqual(len(data), 1) + self.assertDictEqual(expected, data[0]) + + # With ignore_cr_dr_notes + expected = { + "party": "_Test Customer", + "customer_name": "_Test Customer", + "customer_group": "_Test Customer Group", + "territory": "_Test Territory", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 100.0, + "paid_amount": 0.0, + "return_amount": 100.0, + "closing_balance": 0.0, + "currency": "INR", + "dr_or_cr": "", + } + columns, data = execute( + frappe._dict( + { + "company": si.company, + "from_date": si.posting_date, + "to_date": si.posting_date, + "ignore_cr_dr_notes": True, + } + ) + ) + self.assertEqual(len(data), 1) + self.assertEqual(expected, data[0]) + +>>>>>>> 8f91919933 (test: add party_group, territory in json) def test_journal_voucher_against_return_invoice(self): filters = {"company": self.company, "from_date": today(), "to_date": today()} From 38124a7616aac1a9c8b2ae37b45250fea3cd9234 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 25 Nov 2025 12:01:37 +0530 Subject: [PATCH 38/39] chore: resolve conflicts --- .../customer_ledger_summary.py | 33 ------- .../test_customer_ledger_summary.py | 90 ------------------- 2 files changed, 123 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 5b669ba5826..e56081280ef 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -244,39 +244,6 @@ class PartyLedgerSummaryReport: }, ] -<<<<<<< HEAD - # Hidden columns for handling 'User Permissions' - if self.filters.party_type == "Customer": - columns += [ - { - "label": _("Territory"), - "fieldname": "territory", - "fieldtype": "Link", - "options": "Territory", - "hidden": 1, - }, - { - "label": _("Customer Group"), - "fieldname": "customer_group", - "fieldtype": "Link", - "options": "Customer Group", - "hidden": 1, - }, - ] - else: - columns += [ - { - "label": _("Supplier Group"), - "fieldname": "supplier_group", - "fieldtype": "Link", - "options": "Supplier Group", - "hidden": 1, - } - ] - -======= - columns.append({"label": _("Dr/Cr"), "fieldname": "dr_or_cr", "fieldtype": "Data", "width": 100}) ->>>>>>> 231479a6e2 (fix(ledger-summary-report): show party group and territory) return columns def get_data(self): diff --git a/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py index 2c91717eabc..ca9c62dac6c 100644 --- a/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py @@ -151,96 +151,6 @@ class TestCustomerLedgerSummary(FrappeTestCase, AccountsTestMixin): with self.subTest(field=field): self.assertEqual(report[0].get(field), expected_after_cr_and_payment.get(field)) -<<<<<<< HEAD -======= - def test_customer_ledger_ignore_cr_dr_filter(self): - si = create_sales_invoice() - - cr_note = make_return_doc(si.doctype, si.name) - cr_note.submit() - - pr = frappe.get_doc("Payment Reconciliation") - pr.company = si.company - pr.party_type = "Customer" - pr.party = si.customer - pr.receivable_payable_account = si.debit_to - - pr.get_unreconciled_entries() - - invoices = [invoice.as_dict() for invoice in pr.invoices if invoice.invoice_number == si.name] - payments = [payment.as_dict() for payment in pr.payments if payment.reference_name == cr_note.name] - pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) - pr.reconcile() - - system_generated_journal = frappe.db.get_all( - "Journal Entry", - filters={ - "docstatus": 1, - "reference_type": si.doctype, - "reference_name": si.name, - "voucher_type": "Credit Note", - "is_system_generated": True, - }, - fields=["name"], - ) - self.assertEqual(len(system_generated_journal), 1) - expected = { - "party": "_Test Customer", - "customer_name": "_Test Customer", - "customer_group": "_Test Customer Group", - "territory": "_Test Territory", - "party_name": "_Test Customer", - "opening_balance": 0, - "invoiced_amount": 100.0, - "paid_amount": 0.0, - "return_amount": 100.0, - "closing_balance": 0.0, - "currency": "INR", - "dr_or_cr": "", - } - # Without ignore_cr_dr_notes - columns, data = execute( - frappe._dict( - { - "company": si.company, - "from_date": si.posting_date, - "to_date": si.posting_date, - "ignore_cr_dr_notes": False, - } - ) - ) - self.assertEqual(len(data), 1) - self.assertDictEqual(expected, data[0]) - - # With ignore_cr_dr_notes - expected = { - "party": "_Test Customer", - "customer_name": "_Test Customer", - "customer_group": "_Test Customer Group", - "territory": "_Test Territory", - "party_name": "_Test Customer", - "opening_balance": 0, - "invoiced_amount": 100.0, - "paid_amount": 0.0, - "return_amount": 100.0, - "closing_balance": 0.0, - "currency": "INR", - "dr_or_cr": "", - } - columns, data = execute( - frappe._dict( - { - "company": si.company, - "from_date": si.posting_date, - "to_date": si.posting_date, - "ignore_cr_dr_notes": True, - } - ) - ) - self.assertEqual(len(data), 1) - self.assertEqual(expected, data[0]) - ->>>>>>> 8f91919933 (test: add party_group, territory in json) def test_journal_voucher_against_return_invoice(self): filters = {"company": self.company, "from_date": today(), "to_date": today()} From 488d635dc94b8bdbea01929469a1ac512d8be23a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:49:53 +0530 Subject: [PATCH 39/39] chore: switched frankfurter domain from frankfurter.app to frankfurter.dev (backport #50734) (#50740) Co-authored-by: diptanilsaha --- .../currency_exchange_settings.js | 2 +- .../currency_exchange_settings.json | 9 +++++---- .../currency_exchange_settings.py | 10 +++++----- erpnext/patches.txt | 3 ++- ...ate_currency_exchange_settings_for_frankfurter.py | 12 ++++++++++++ .../currency_exchange/test_currency_exchange.py | 6 +++--- erpnext/setup/install.py | 2 +- 7 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 erpnext/patches/v16_0/update_currency_exchange_settings_for_frankfurter.py diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js index ad68352c2a4..c3531420ce1 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js @@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", { to: "{to_currency}", }; add_param(frm, r.message, params, result); - } else if (frm.doc.service_provider == "frankfurter.app") { + } else if (frm.doc.service_provider == "frankfurter.dev") { let result = ["rates", "{to_currency}"]; let params = { base: "{from_currency}", diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index bd90b8add80..614f4e6d3e5 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -78,7 +78,7 @@ "fieldname": "service_provider", "fieldtype": "Select", "label": "Service Provider", - "options": "frankfurter.app\nexchangerate.host\nCustom", + "options": "frankfurter.dev\nexchangerate.host\nCustom", "reqd": 1 }, { @@ -104,7 +104,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-03-18 08:32:26.895076", + "modified": "2025-11-25 13:03:41.896424", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", @@ -141,8 +141,9 @@ "write": 1 } ], - "sort_field": "modified", + "row_format": "Dynamic", + "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py index 160e791978e..3a12ab30403 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py @@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document): disabled: DF.Check req_params: DF.Table[CurrencyExchangeSettingsDetails] result_key: DF.Table[CurrencyExchangeSettingsResult] - service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"] + service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"] url: DF.Data | None use_http: DF.Check # end: auto-generated types @@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document): self.append("req_params", {"key": "date", "value": "{transaction_date}"}) self.append("req_params", {"key": "from", "value": "{from_currency}"}) self.append("req_params", {"key": "to", "value": "{to_currency}"}) - elif self.service_provider == "frankfurter.app": + elif self.service_provider == "frankfurter.dev": self.set("result_key", []) self.set("req_params", []) @@ -105,11 +105,11 @@ class CurrencyExchangeSettings(Document): @frappe.whitelist() def get_api_endpoint(service_provider: str | None = None, use_http: bool = False): - if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]: + if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev"]: if service_provider == "exchangerate.host": api = "api.exchangerate.host/convert" - elif service_provider == "frankfurter.app": - api = "api.frankfurter.app/{transaction_date}" + elif service_provider == "frankfurter.dev": + api = "api.frankfurter.dev/v1/{transaction_date}" protocol = "https://" if use_http: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 33c1e6c79df..7a79332e316 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -425,4 +425,5 @@ erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter erpnext.patches.v15_0.set_asset_status_if_not_already_set erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1) -execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1) \ No newline at end of file +execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1) +erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter diff --git a/erpnext/patches/v16_0/update_currency_exchange_settings_for_frankfurter.py b/erpnext/patches/v16_0/update_currency_exchange_settings_for_frankfurter.py new file mode 100644 index 00000000000..68157b1a4ad --- /dev/null +++ b/erpnext/patches/v16_0/update_currency_exchange_settings_for_frankfurter.py @@ -0,0 +1,12 @@ +import frappe + + +def execute(): + settings = frappe.get_doc("Currency Exchange Settings") + if settings.service_provider != "frankfurter.app": + return + + settings.service_provider = "frankfurter.dev" + settings.set_parameters_and_result() + settings.flags.ignore_validate = True + settings.save() diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index d28b1e65b7b..86fae223b45 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -68,9 +68,9 @@ def patched_requests_get(*args, **kwargs): if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"): if test_exchange_values.get(kwargs["params"]["date"]): return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200) - elif args[0].startswith("https://api.frankfurter.app") and kwargs.get("params"): + elif args[0].startswith("https://api.frankfurter.dev") and kwargs.get("params"): if kwargs["params"].get("base") and kwargs["params"].get("symbols"): - date = args[0].replace("https://api.frankfurter.app/", "") + date = args[0].replace("https://api.frankfurter.dev/v1/", "") if test_exchange_values.get(date): return PatchResponse( {"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200 @@ -149,7 +149,7 @@ class TestCurrencyExchange(unittest.TestCase): self.assertEqual(flt(exchange_rate, 3), 65.1) settings = frappe.get_single("Currency Exchange Settings") - settings.service_provider = "frankfurter.app" + settings.service_provider = "frankfurter.dev" settings.save() def test_exchange_rate_strict(self, mock_get): diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index b826c52f20e..0bcab1ccb59 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -93,7 +93,7 @@ def setup_currency_exchange(): ces.set("result_key", []) ces.set("req_params", []) - ces.api_endpoint = "https://api.frankfurter.app/{transaction_date}" + ces.api_endpoint = "https://api.frankfurter.dev/v1/{transaction_date}" ces.append("result_key", {"key": "rates"}) ces.append("result_key", {"key": "{to_currency}"}) ces.append("req_params", {"key": "base", "value": "{from_currency}"})