From 088bbac543a228319f5ee4dd1247c7b6e48eb794 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:34:13 +0100 Subject: [PATCH 01/43] fix: use dummy translations for custom field labels (#49875) (cherry picked from commit 9a989a84fb59c7c9a0da6bd4d8f567667e0fc1bc) # Conflicts: # erpnext/setup/install.py # erpnext/setup/setup_wizard/operations/install_fixtures.py --- erpnext/setup/install.py | 11 +++++++---- .../setup_wizard/operations/install_fixtures.py | 6 +++++- erpnext/setup/utils.py | 12 ++++++++++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index b826c52f20e..fb09598a450 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -4,7 +4,6 @@ import click import frappe -from frappe import _ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to from frappe.utils import cint @@ -12,6 +11,7 @@ from frappe.utils import cint import erpnext from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules from erpnext.setup.doctype.incoterm.incoterm import create_incoterms +from erpnext.setup.utils import identity as _ from .default_success_action import get_default_success_action @@ -193,22 +193,25 @@ def add_company_to_session_defaults(): def add_standard_navbar_items(): navbar_settings = frappe.get_single("Navbar Settings") +<<<<<<< HEAD +======= +>>>>>>> 9a989a84fb (fix: use dummy translations for custom field labels (#49875)) erpnext_navbar_items = [ { - "item_label": "Documentation", + "item_label": _("Documentation"), "item_type": "Route", "route": "https://docs.erpnext.com/", "is_standard": 1, }, { - "item_label": "User Forum", + "item_label": _("User Forum"), "item_type": "Route", "route": "https://discuss.frappe.io", "is_standard": 1, }, { - "item_label": "Frappe School", + "item_label": _("Frappe School"), "item_type": "Route", "route": "https://frappe.io/school?utm_source=in_app", "is_standard": 1, diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 0f3356ffa50..5c5d47730eb 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -16,6 +16,10 @@ from frappe.utils import cstr, getdate from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates +<<<<<<< HEAD +======= +from erpnext.setup.utils import identity as _ +>>>>>>> 9a989a84fb (fix: use dummy translations for custom field labels (#49875)) def read_lines(filename: str) -> list[str]: @@ -550,7 +554,7 @@ def create_bank_account(args, demo=False): return doc except RootNotEditable: - frappe.throw(_("Bank account cannot be named as {0}").format(args.get("bank_account"))) + frappe.throw(frappe._("Bank account cannot be named as {0}").format(args.get("bank_account"))) except frappe.DuplicateEntryError: # bank account same as a CoA entry pass diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index eae5c9f6c74..b7436a140f8 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -234,3 +234,15 @@ def welcome_email(): site_name = get_default_company() or "ERPNext" title = _("Welcome to {0}").format(site_name) return title + + +def identity(x, *args, **kwargs): + """Used for redefining the translation function to return the string as is. + + We want to create english records but still mark the strings as translatable. + E.g. when the respective DocTypes have 'Translate Link Fields' enabled or + we're creating custom fields. + + Use like this: `from erpnext.setup.utils import identity as _` + """ + return x From e29a384f902e8e5a8bc563ce1752fc3cce452d1b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:39:40 +0100 Subject: [PATCH 02/43] chore: resolve conflicts --- erpnext/setup/install.py | 5 ----- erpnext/setup/setup_wizard/operations/install_fixtures.py | 4 ---- 2 files changed, 9 deletions(-) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index fb09598a450..c7082122a15 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -6,7 +6,6 @@ import click import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to -from frappe.utils import cint import erpnext from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules @@ -193,10 +192,6 @@ def add_company_to_session_defaults(): def add_standard_navbar_items(): navbar_settings = frappe.get_single("Navbar Settings") -<<<<<<< HEAD - -======= ->>>>>>> 9a989a84fb (fix: use dummy translations for custom field labels (#49875)) erpnext_navbar_items = [ { "item_label": _("Documentation"), diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 5c5d47730eb..758a05f0736 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -7,7 +7,6 @@ import os from pathlib import Path import frappe -from frappe import _ from frappe.desk.doctype.global_search_settings.global_search_settings import ( update_global_search_doctypes, ) @@ -16,10 +15,7 @@ from frappe.utils import cstr, getdate from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates -<<<<<<< HEAD -======= from erpnext.setup.utils import identity as _ ->>>>>>> 9a989a84fb (fix: use dummy translations for custom field labels (#49875)) def read_lines(filename: str) -> list[str]: From 19dc26ea16cbb963fb6353c4c16d2aebb1be26fa Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:51:05 +0100 Subject: [PATCH 03/43] revert: changes to install_fixtures I think this would be too breaking. Custom apps might expect the translated data to exist. --- erpnext/setup/setup_wizard/operations/install_fixtures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 758a05f0736..0f3356ffa50 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -7,6 +7,7 @@ import os from pathlib import Path import frappe +from frappe import _ from frappe.desk.doctype.global_search_settings.global_search_settings import ( update_global_search_doctypes, ) @@ -15,7 +16,6 @@ from frappe.utils import cstr, getdate from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates -from erpnext.setup.utils import identity as _ def read_lines(filename: str) -> list[str]: @@ -550,7 +550,7 @@ def create_bank_account(args, demo=False): return doc except RootNotEditable: - frappe.throw(frappe._("Bank account cannot be named as {0}").format(args.get("bank_account"))) + frappe.throw(_("Bank account cannot be named as {0}").format(args.get("bank_account"))) except frappe.DuplicateEntryError: # bank account same as a CoA entry pass From ec3a226a83c3ec2245d668f5b8877596e83fa319 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:54:34 +0100 Subject: [PATCH 04/43] fix: mark navbar item as translatable --- erpnext/setup/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index c7082122a15..2dd05b1a7b8 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -212,7 +212,7 @@ def add_standard_navbar_items(): "is_standard": 1, }, { - "item_label": "Report an Issue", + "item_label": _("Report an Issue"), "item_type": "Route", "route": "https://github.com/frappe/erpnext/issues", "is_standard": 1, From a8ed2815a47601ae2ba507afe5c979e3b622592b Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Wed, 10 Dec 2025 08:13:22 +0530 Subject: [PATCH 05/43] fix(share balance): use currency field instead of int for rate and amount (cherry picked from commit 2fe5fad884864ed978ef846ce5e27aee3b602b80) # Conflicts: # erpnext/accounts/doctype/share_balance/share_balance.json --- .../doctype/share_balance/share_balance.json | 41 ++++++++++++++++++- .../doctype/share_balance/share_balance.py | 4 +- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/share_balance/share_balance.json b/erpnext/accounts/doctype/share_balance/share_balance.json index 04d7bb75bf8..dd3e66b1fbf 100644 --- a/erpnext/accounts/doctype/share_balance/share_balance.json +++ b/erpnext/accounts/doctype/share_balance/share_balance.json @@ -74,6 +74,7 @@ "unique": 0 }, { +<<<<<<< HEAD "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, @@ -103,6 +104,15 @@ "set_only_once": 0, "unique": 0 }, +======= + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "read_only": 1, + "reqd": 1 + }, +>>>>>>> 2fe5fad884 (fix(share balance): use currency field instead of int for rate and amount) { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -193,6 +203,7 @@ "unique": 0 }, { +<<<<<<< HEAD "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, @@ -222,6 +233,15 @@ "set_only_once": 0, "unique": 0 }, +======= + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "read_only": 1, + "reqd": 1 + }, +>>>>>>> 2fe5fad884 (fix(share balance): use currency field instead of int for rate and amount) { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -313,6 +333,7 @@ "set_only_once": 0, "unique": 0 } +<<<<<<< HEAD ], "has_web_view": 0, "hide_heading": 0, @@ -339,4 +360,22 @@ "sort_order": "DESC", "track_changes": 1, "track_seen": 0 -} \ No newline at end of file +} +======= + ], + "istable": 1, + "links": [], + "modified": "2025-12-10 08:06:40.611761", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Share Balance", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} +>>>>>>> 2fe5fad884 (fix(share balance): use currency field instead of int for rate and amount) diff --git a/erpnext/accounts/doctype/share_balance/share_balance.py b/erpnext/accounts/doctype/share_balance/share_balance.py index 7a77ce9b871..2eff8a9a9be 100644 --- a/erpnext/accounts/doctype/share_balance/share_balance.py +++ b/erpnext/accounts/doctype/share_balance/share_balance.py @@ -14,7 +14,7 @@ class ShareBalance(Document): if TYPE_CHECKING: from frappe.types import DF - amount: DF.Int + amount: DF.Currency current_state: DF.Literal["", "Issued", "Purchased"] from_no: DF.Int is_company: DF.Check @@ -22,7 +22,7 @@ class ShareBalance(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data - rate: DF.Int + rate: DF.Currency share_type: DF.Link to_no: DF.Int # end: auto-generated types From 4ee4a57f722499c3b72765b6e4dde807bd930f83 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Sat, 6 Dec 2025 10:49:15 +0530 Subject: [PATCH 06/43] fix: precision issue on job card submission (cherry picked from commit 80730908c901be4a460c95b63b39d6f60e744819) --- erpnext/manufacturing/doctype/work_order/work_order.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 5e4dcf22431..fe2115c7022 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -979,14 +979,14 @@ class WorkOrder(Document): for d in self.get("operations"): precision = d.precision("completed_qty") - qty = flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision) + qty = flt(flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision), precision) if not qty: d.status = "Pending" - elif flt(qty) < flt(self.qty): + elif qty < flt(self.qty, precision): d.status = "Work in Progress" - elif flt(qty) == flt(self.qty): + elif qty == flt(self.qty, precision): d.status = "Completed" - elif flt(qty) <= max_allowed_qty_for_wo: + elif qty <= flt(max_allowed_qty_for_wo, precision): d.status = "Completed" else: frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'")) From 8fd3e8e22e112c4704981e8ea9cca58e783c9b53 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Wed, 10 Dec 2025 09:59:04 +0530 Subject: [PATCH 07/43] chore: resolve conflict --- .../doctype/share_balance/share_balance.json | 45 ++----------------- 1 file changed, 3 insertions(+), 42 deletions(-) diff --git a/erpnext/accounts/doctype/share_balance/share_balance.json b/erpnext/accounts/doctype/share_balance/share_balance.json index dd3e66b1fbf..4cbe2deffe5 100644 --- a/erpnext/accounts/doctype/share_balance/share_balance.json +++ b/erpnext/accounts/doctype/share_balance/share_balance.json @@ -74,14 +74,13 @@ "unique": 0 }, { -<<<<<<< HEAD "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "fieldname": "rate", - "fieldtype": "Int", + "fieldtype": "Currency", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -103,16 +102,7 @@ "search_index": 0, "set_only_once": 0, "unique": 0 - }, -======= - "fieldname": "rate", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Rate", - "read_only": 1, - "reqd": 1 }, ->>>>>>> 2fe5fad884 (fix(share balance): use currency field instead of int for rate and amount) { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -203,14 +193,13 @@ "unique": 0 }, { -<<<<<<< HEAD "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "fieldname": "amount", - "fieldtype": "Int", + "fieldtype": "Currency", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -232,16 +221,7 @@ "search_index": 0, "set_only_once": 0, "unique": 0 - }, -======= - "fieldname": "amount", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Amount", - "read_only": 1, - "reqd": 1 }, ->>>>>>> 2fe5fad884 (fix(share balance): use currency field instead of int for rate and amount) { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -333,7 +313,6 @@ "set_only_once": 0, "unique": 0 } -<<<<<<< HEAD ], "has_web_view": 0, "hide_heading": 0, @@ -345,7 +324,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-01-10 18:32:36.201124", + "modified": "2025-12-10 08:06:40.611761", "modified_by": "Administrator", "module": "Accounts", "name": "Share Balance", @@ -361,21 +340,3 @@ "track_changes": 1, "track_seen": 0 } -======= - ], - "istable": 1, - "links": [], - "modified": "2025-12-10 08:06:40.611761", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Share Balance", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [], - "track_changes": 1 -} ->>>>>>> 2fe5fad884 (fix(share balance): use currency field instead of int for rate and amount) From 46ca347578732b2be08ba43e7c6ec5f1ebd590b0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 06:19:50 +0000 Subject: [PATCH 08/43] perf: move all hourly/daily jobs to maintenance queue (backport #47504) (#51005) perf: move all hourly/daily jobs to maintenance queue (#47504) None of them need to strictly happen at 00:00 or *:00, so moving them all to maintenance queue which executes with same frequency but spaced out. (cherry picked from commit a50251401fab047462c2b6b27f0a9441a5b80c15) Co-authored-by: Ankush Menat --- erpnext/hooks.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 4514249824b..4451fc9a601 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -412,29 +412,29 @@ scheduler_events = { "0/15 * * * *": [ "erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs", ], - "0/30 * * * *": [ - "erpnext.utilities.doctype.video.video.update_youtube_data", - ], + "0/30 * * * *": [], # Hourly but offset by 30 minutes "30 * * * *": [ "erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs", ], # Daily but offset by 45 minutes - "45 0 * * *": [ - "erpnext.stock.reorder_item.reorder_item", - ], + "45 0 * * *": [], }, "hourly": [ - "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", - "erpnext.projects.doctype.project.project.project_status_update_reminder", "erpnext.projects.doctype.project.project.hourly_reminder", - "erpnext.projects.doctype.project.project.collect_project_status", ], - "hourly_long": [ + "hourly_long": [], + "hourly_maintenance": [ "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", "erpnext.utilities.bulk_transaction.retry", + "erpnext.projects.doctype.project.project.collect_project_status", + "erpnext.projects.doctype.project.project.project_status_update_reminder", + "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", + "erpnext.utilities.doctype.video.video.update_youtube_data", ], - "daily": [ + "daily": [], + "daily_long": [], + "daily_maintenance": [ "erpnext.support.doctype.issue.issue.auto_close_tickets", "erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity", "erpnext.controllers.accounts_controller.update_invoice_status", @@ -458,17 +458,16 @@ scheduler_events = { "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily", "erpnext.accounts.utils.run_ledger_health_checks", "erpnext.assets.doctype.asset_maintenance_log.asset_maintenance_log.update_asset_maintenance_log_status", - ], - "weekly": [ - "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly", - ], - "daily_long": [ + "erpnext.stock.reorder_item.reorder_item", "erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process", "erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms", "erpnext.crm.utils.open_leads_opportunities_based_on_todays_event", "erpnext.assets.doctype.asset.depreciation.post_depreciation_entries", ], + "weekly": [ + "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly", + ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly", From 918f8ca79b66bb291ade3782bc009c4474f73586 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Fri, 5 Dec 2025 16:34:28 +0530 Subject: [PATCH 09/43] fix(stock): remove total bar in chart view (cherry picked from commit 198eb372e35c59ba7958c7eb894894ca3e14a9f2) --- .../report/delivery_note_trends/delivery_note_trends.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py index 0ec4e1ce957..a456bad72d7 100644 --- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py +++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py @@ -20,6 +20,9 @@ def execute(filters=None): def get_chart_data(data, filters): + def wrap_in_quotes(label): + return f"'{label}'" + if not data: return [] @@ -36,6 +39,9 @@ def get_chart_data(data, filters): data = data[:10] for row in data: + if row[0] == wrap_in_quotes(_("Total")): + continue + labels.append(row[0]) datapoints.append(row[-1]) From f3c70a66b5daa8eab56cf169b896d771e67a606c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 11 Dec 2025 10:12:42 +0530 Subject: [PATCH 10/43] fix: Serial/Batches not fetching when creating Material Transfer from Purchase Receipt (cherry picked from commit d16c50486a7191ce6fa128fada3c4e87d4649404) # Conflicts: # erpnext/stock/doctype/purchase_receipt/purchase_receipt.py --- .../purchase_receipt/purchase_receipt.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 53aad87e2da..47e89269b35 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -19,6 +19,15 @@ from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.accounts_controller import merge_taxes from erpnext.controllers.buying_controller import BuyingController from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction +<<<<<<< HEAD +======= +from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation +from erpnext.stock.serial_batch_bundle import ( + SerialBatchCreation, + get_batches_from_bundle, + get_serial_nos_from_bundle, +) +>>>>>>> d16c50486a (fix: Serial/Batches not fetching when creating Material Transfer from Purchase Receipt) form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -1411,6 +1420,35 @@ def make_stock_entry(source_name, target_doc=None): target.purpose = "Material Transfer" target.set_missing_values() + def update_item(source_doc, target_doc, source_parent): + if source_doc.serial_and_batch_bundle: + serial_nos = get_serial_nos_from_bundle(source_doc.serial_and_batch_bundle) + if serial_nos: + serial_nos = "\n".join(serial_nos) + + batches = get_batches_from_bundle(source_doc.serial_and_batch_bundle) + if batches: + if len(batches) == 1: + target_doc.use_serial_batch_fields = 1 + target_doc.batch_no = next(iter(batches)) + elif not serial_nos: + cls_obj = SerialBatchCreation( + { + "type_of_transaction": "Outward", + "serial_and_batch_bundle": source_doc.serial_and_batch_bundle, + "item_code": source_doc.item_code, + "warehouse": source_doc.warehouse, + } + ) + + cls_obj.duplicate_package() + + target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle + + if serial_nos: + target_doc.use_serial_batch_fields = 1 + target_doc.serial_no = serial_nos + doclist = get_mapped_doc( "Purchase Receipt", source_name, @@ -1425,6 +1463,7 @@ def make_stock_entry(source_name, target_doc=None): "parent": "reference_purchase_receipt", "batch_no": "batch_no", }, + "postprocess": update_item, }, }, target_doc, From c8565c47a2c2fce59baa6844f73b8205290b2c85 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 11 Dec 2025 11:36:31 +0530 Subject: [PATCH 11/43] chore: fix conflicts --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 47e89269b35..7a4101e9dfb 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -19,15 +19,11 @@ from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.accounts_controller import merge_taxes from erpnext.controllers.buying_controller import BuyingController from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction -<<<<<<< HEAD -======= -from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation from erpnext.stock.serial_batch_bundle import ( SerialBatchCreation, get_batches_from_bundle, get_serial_nos_from_bundle, ) ->>>>>>> d16c50486a (fix: Serial/Batches not fetching when creating Material Transfer from Purchase Receipt) form_grid_templates = {"items": "templates/form_grid/item_grid.html"} From df820aece678ab6d18a54eb491a72066c6002d5d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 11 Dec 2025 13:43:58 +0530 Subject: [PATCH 12/43] fix: putaway rule not applying on serial nos (cherry picked from commit 6bb0bdcdcac7bf0d65a7640ab3c2aff650312957) --- .../doctype/putaway_rule/putaway_rule.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 53af3e12896..a335d19afa5 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -11,6 +11,7 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import cint, cstr, floor, flt, nowdate +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.utils import get_stock_balance @@ -119,13 +120,19 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): item = frappe._dict(item) source_warehouse = item.get("s_warehouse") + serial_nos = [] + if item.get("serial_no"): + serial_nos = get_serial_nos(item.get("serial_no")) + item.conversion_factor = flt(item.conversion_factor) or 1.0 pending_qty, item_code = flt(item.qty), item.item_code pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) uom_must_be_whole_number = frappe.db.get_value("UOM", item.uom, "must_be_whole_number") if not pending_qty or not item_code: - updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table) + updated_table = add_row( + item, pending_qty, source_warehouse or item.warehouse, updated_table, serial_nos=serial_nos + ) continue at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) @@ -141,7 +148,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) else: - updated_table = add_row(item, pending_qty, warehouse, updated_table) + updated_table = add_row(item, pending_qty, warehouse, updated_table, serial_nos=serial_nos) continue # maintain item/item-warehouse wise rules, to handle if item is entered twice @@ -167,7 +174,9 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): if not qty_to_allocate: break - updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, rule.name) + updated_table = add_row( + item, qty_to_allocate, rule.warehouse, updated_table, rule.name, serial_nos=serial_nos + ) pending_stock_qty -= stock_qty_to_allocate pending_qty -= qty_to_allocate @@ -265,7 +274,7 @@ def get_ordered_putaway_rules(item_code, company, source_warehouse=None): return False, vacant_rules -def add_row(item, to_allocate, warehouse, updated_table, rule=None): +def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None): new_updated_table_row = copy.deepcopy(item) new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1 new_updated_table_row.name = None @@ -283,6 +292,9 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None): if rule: new_updated_table_row.putaway_rule = rule + if serial_nos: + new_updated_table_row.serial_no = get_serial_nos_to_allocate(serial_nos, to_allocate) + new_updated_table_row.serial_and_batch_bundle = "" updated_table.append(new_updated_table_row) @@ -311,3 +323,12 @@ def show_unassigned_items_message(items_not_accomodated): """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + + +def get_serial_nos_to_allocate(serial_nos, to_allocate): + if serial_nos: + allocated_serial_nos = serial_nos[0 : cint(to_allocate)] + serial_nos[:] = serial_nos[cint(to_allocate) :] # pop out allocated serial nos and modify list + return "\n".join(allocated_serial_nos) if allocated_serial_nos else "" + else: + return "" From 8d32ba9a2eeadb855df9d77cb028b0ef304763b3 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Thu, 11 Dec 2025 14:59:05 +0530 Subject: [PATCH 13/43] fix(currency exchange settings): added backward compatibility for frankfurter api (cherry picked from commit 5c2bb66028f40ab4177ff17986fc87b5300fefd7) # Conflicts: # erpnext/patches.txt --- .../currency_exchange_settings.js | 2 +- .../currency_exchange_settings.py | 6 ++++-- erpnext/patches.txt | 11 +++++++++++ ...date_currency_exchange_settings_for_frankfurter.py | 7 ++++++- 4 files changed, 22 insertions(+), 4 deletions(-) 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 c3531420ce1..40f0938ee1c 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.dev") { + } else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) { let result = ["rates", "{to_currency}"]; let params = { base: "{from_currency}", 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 3a12ab30403..28e4158cc8d 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py @@ -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.dev": + elif self.service_provider in ("frankfurter.dev", "frankfurter.app"): self.set("result_key", []) self.set("req_params", []) @@ -105,9 +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.dev"]: + if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]: 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}" diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7a79332e316..b58742e414c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -424,6 +424,17 @@ erpnext.patches.v15_0.update_uae_zero_rated_fetch 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 +<<<<<<< HEAD execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1) execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1) erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter +======= +erpnext.patches.v16_0.update_serial_batch_entries +erpnext.patches.v16_0.set_company_wise_warehouses +erpnext.patches.v16_0.set_valuation_method_on_companies +erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_to_table +erpnext.patches.v16_0.migrate_budget_records_to_new_structure +erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11 +erpnext.patches.v16_0.migrate_account_freezing_settings_to_company +erpnext.patches.v16_0.populate_budget_distribution_total +>>>>>>> 5c2bb66028 (fix(currency exchange settings): added backward compatibility for frankfurter api) 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 index 68157b1a4ad..9d3f78cc09b 100644 --- a/erpnext/patches/v16_0/update_currency_exchange_settings_for_frankfurter.py +++ b/erpnext/patches/v16_0/update_currency_exchange_settings_for_frankfurter.py @@ -2,8 +2,13 @@ import frappe def execute(): + settings_meta = frappe.get_meta("Currency Exchange Settings") settings = frappe.get_doc("Currency Exchange Settings") - if settings.service_provider != "frankfurter.app": + + if ( + "frankfurter.dev" not in settings_meta.get_options("service_provider").split("\n") + or settings.service_provider != "frankfurter.app" + ): return settings.service_provider = "frankfurter.dev" From 113da4f51204e7d69e876a0f6faf7d732819a1d1 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Thu, 11 Dec 2025 15:59:57 +0530 Subject: [PATCH 14/43] chore: resolve conflict --- erpnext/patches.txt | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b58742e414c..1574c540100 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -424,17 +424,6 @@ erpnext.patches.v15_0.update_uae_zero_rated_fetch 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 -<<<<<<< HEAD execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1) execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1) -erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter -======= -erpnext.patches.v16_0.update_serial_batch_entries -erpnext.patches.v16_0.set_company_wise_warehouses -erpnext.patches.v16_0.set_valuation_method_on_companies -erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_to_table -erpnext.patches.v16_0.migrate_budget_records_to_new_structure erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11 -erpnext.patches.v16_0.migrate_account_freezing_settings_to_company -erpnext.patches.v16_0.populate_budget_distribution_total ->>>>>>> 5c2bb66028 (fix(currency exchange settings): added backward compatibility for frankfurter api) From 2d198e698abeaf79352c106f62c6b53d4bdc02c4 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Thu, 11 Dec 2025 17:48:37 +0530 Subject: [PATCH 15/43] fix: ensure fresh `grand_total_diff` is used for each calculation (cherry picked from commit b3fdef8d19ebb919d496b2f1e6a55706bad34f74) --- erpnext/controllers/taxes_and_totals.py | 5 ++++- erpnext/public/js/controllers/taxes_and_totals.js | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index d6c4b99588c..f059f2908bd 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -378,6 +378,9 @@ class calculate_taxes_and_totals: self._calculate() def calculate_taxes(self): + # reset value from earlier calculations + self.grand_total_diff = 0 + doc = self.doc if not doc.get("taxes"): return @@ -587,7 +590,7 @@ class calculate_taxes_and_totals: self.grand_total_diff = 0 def calculate_totals(self): - grand_total_diff = getattr(self, "grand_total_diff", 0) + grand_total_diff = self.grand_total_diff if self.doc.get("taxes"): self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index d8c81b726ee..de0a7ab51bc 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -343,6 +343,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } calculate_taxes() { + // reset value from earlier calculations + this.grand_total_diff = 0; + const doc = this.frm.doc; if (!doc.taxes?.length) return; @@ -578,6 +581,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) { me.grand_total_diff = diff; + } else { + me.grand_total_diff = 0; } } } @@ -587,7 +592,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency const me = this; const tax_count = this.frm.doc.taxes?.length; - const grand_total_diff = this.grand_total_diff || 0; + const grand_total_diff = this.grand_total_diff; this.frm.doc.grand_total = flt(tax_count ? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff From 5bfdc010f3392d8df3e7b3841218df8456a84e62 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:37:59 +0530 Subject: [PATCH 16/43] fix: re-calculate outstanding / write-off amount during submission (cherry picked from commit 09c9ac1b6636a850349c5710c1c07077f873cf3e) --- erpnext/controllers/taxes_and_totals.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index f059f2908bd..571f9ad4b14 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -853,12 +853,11 @@ class calculate_taxes_and_totals: ) ) - if self.doc.docstatus.is_draft(): - if self.doc.get("write_off_outstanding_amount_automatically"): - self.doc.write_off_amount = 0 + if self.doc.get("write_off_outstanding_amount_automatically"): + self.doc.write_off_amount = 0 - self.calculate_outstanding_amount() - self.calculate_write_off_amount() + self.calculate_outstanding_amount() + self.calculate_write_off_amount() def is_internal_invoice(self): """ From cdc04292f2aecd74e84e4c9465da768a2a80f293 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 11 Dec 2025 16:32:04 +0530 Subject: [PATCH 17/43] fix(manufacturing): add validation for disassemble qty (cherry picked from commit 86d6facab34d84ff5063e970467dd2ba78d82098) --- erpnext/manufacturing/doctype/work_order/work_order.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 5fe7c607b6c..9e0679a7cd1 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -403,6 +403,10 @@ frappe.ui.form.on("Work Order", { erpnext.work_order .show_prompt_for_qty_input(frm, "Disassemble") .then((data) => { + if (flt(data.qty) <= 0) { + frappe.msgprint(__("Disassemble Qty cannot be less than or equal to 0.")); + return; + } return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", { work_order_id: frm.doc.name, purpose: "Disassemble", From 279cf6fe007528f624f8f1e5ef7d169124584756 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 11 Dec 2025 16:35:02 +0530 Subject: [PATCH 18/43] fix(manufacturing): get items for disassembly order (cherry picked from commit 99148a2abae57f21681a8848622d56a1f92b20f8) # Conflicts: # erpnext/manufacturing/doctype/work_order/work_order.py --- .../manufacturing/doctype/work_order/work_order.py | 5 +++++ erpnext/stock/doctype/stock_entry/stock_entry.py | 14 +++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index fe2115c7022..0fc7473b7e6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1509,7 +1509,12 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None): stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse stock_entry.set_stock_entry_type() +<<<<<<< HEAD stock_entry.get_items(qty, work_order.production_item) +======= + stock_entry.is_additional_transfer_entry = is_additional_transfer_entry + stock_entry.get_items() +>>>>>>> 99148a2aba (fix(manufacturing): get items for disassembly order) if purpose != "Disassemble": stock_entry.set_serial_no_batch_for_finished_good() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 942a9f8133c..e3e7c461ef7 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1905,7 +1905,7 @@ class StockEntry(StockController): }, ) - def get_items_for_disassembly(self, disassemble_qty, production_item): + def get_items_for_disassembly(self): """Get items for Disassembly Order""" if not self.work_order: @@ -1918,7 +1918,7 @@ class StockEntry(StockController): items_dict = get_bom_items_as_dict( self.bom_no, self.company, - disassemble_qty, + self.fg_completed_qty, fetch_exploded=self.use_multi_level_bom, fetch_qty_in_stock_uom=False, ) @@ -1935,8 +1935,8 @@ class StockEntry(StockController): child_row.qty = bom_items.get("qty", child_row.qty) child_row.amount = bom_items.get("amount", child_row.amount) - if row.item_code == production_item: - child_row.qty = disassemble_qty + if row.is_finished_item: + child_row.qty = self.fg_completed_qty child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else "" child_row.t_warehouse = row.s_warehouse @@ -1972,12 +1972,12 @@ class StockEntry(StockController): ) @frappe.whitelist() - def get_items(self, qty=None, production_item=None): + def get_items(self): self.set("items", []) self.validate_work_order() - if self.purpose == "Disassemble" and qty is not None: - return self.get_items_for_disassembly(qty, production_item) + if self.purpose == "Disassemble": + return self.get_items_for_disassembly() if not self.posting_date or not self.posting_time: frappe.throw(_("Posting date and posting time is mandatory")) From 1693e3ef3fdd03d51454791915c0601d3a7a13e6 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 12 Dec 2025 11:53:18 +0530 Subject: [PATCH 19/43] chore: fix conflicts --- erpnext/manufacturing/doctype/work_order/work_order.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 0fc7473b7e6..20f8a4ab553 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1509,12 +1509,7 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None): stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse stock_entry.set_stock_entry_type() -<<<<<<< HEAD - stock_entry.get_items(qty, work_order.production_item) -======= - stock_entry.is_additional_transfer_entry = is_additional_transfer_entry stock_entry.get_items() ->>>>>>> 99148a2aba (fix(manufacturing): get items for disassembly order) if purpose != "Disassemble": stock_entry.set_serial_no_batch_for_finished_good() From 99b69c121e9cc28ab146228bbc5f1e8aed8d13e1 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Thu, 11 Dec 2025 15:07:46 +0530 Subject: [PATCH 20/43] fix(trial_balance): remove hardcoded precision for currency values (cherry picked from commit a8af04f6fc12d740435c547f480fdc734fed9d33) --- erpnext/accounts/report/trial_balance/trial_balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 2ea76c82975..13bf6f531d1 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -399,7 +399,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency): } for key in value_fields: - row[key] = flt(d.get(key, 0.0), 3) + row[key] = flt(d.get(key, 0.0)) if abs(row[key]) >= get_zero_cutoff(company_currency): # ignore zero values From b340d7d4f42cff1e5ae1bf27bf401f9455596196 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 11 Dec 2025 12:57:08 +0530 Subject: [PATCH 21/43] fix(accounts): handle drop ship in company linked address validation (cherry picked from commit 2ec119e561cdfdb9f0d1f987348fa62171697992) --- erpnext/controllers/accounts_controller.py | 27 +++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a087236529c..1f6b6ca17c9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -312,13 +312,30 @@ class AccountsController(TransactionBase): def validate_company_linked_addresses(self): address_fields = [] - if self.doctype in ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice"): + sales_doctypes = ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice") + purchase_doctypes = ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation") + + if self.doctype in sales_doctypes: address_fields = ["dispatch_address_name", "company_address"] - elif self.doctype in ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation"): + elif self.doctype in purchase_doctypes: address_fields = ["billing_address", "shipping_address"] + if not address_fields: + return + + # Determine if drop ship applies + is_drop_ship = self.doctype in { + "Purchase Order", + "Sales Order", + "Sales Invoice", + } and self.is_drop_ship(self.items) + for field in address_fields: address = self.get(field) + + if (field in ["dispatch_address_name", "shipping_address"]) and is_drop_ship: + continue + if address and not frappe.db.exists( "Dynamic Link", { @@ -329,11 +346,15 @@ class AccountsController(TransactionBase): }, ): frappe.throw( - _("{0} does not belong to the {1}.").format( + _("{0} does not belong to the Company {1}.").format( _(self.meta.get_label(field)), bold(self.company) ) ) + @staticmethod + def is_drop_ship(items): + return any(item.delivered_by_supplier for item in items) + def set_default_letter_head(self): if hasattr(self, "letter_head") and not self.letter_head: self.letter_head = frappe.db.get_value("Company", self.company, "default_letter_head") From 2263f9a4777c641fc3f17813b01063caf6aacd23 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 11 Dec 2025 13:01:53 +0530 Subject: [PATCH 22/43] test(accounts): add validation test for dispatch address with drop ship enabled (cherry picked from commit f6a96e5563340236c5672e9b11c38c105f410630) --- .../controllers/tests/test_accounts_controller.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index dde95899b7f..a120da6f852 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -2434,6 +2434,7 @@ class TestAccountsController(FrappeTestCase): def test_company_linked_address(self): from erpnext.crm.doctype.prospect.test_prospect import make_address + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order company_address = make_address( address_title="Company", address_type="Shipping", address_line1="100", city="Mumbai" @@ -2462,3 +2463,16 @@ class TestAccountsController(FrappeTestCase): po.billing_address = company_address.name po.reload() po.save() + + si = make_sales_order(do_not_save=1, do_not_submit=1) + si.dispatch_address_name = supplier_billing.name + self.assertRaises(frappe.ValidationError, si.save) + si.items[0].delivered_by_supplier = 1 + si.items[0].supplier = "_Test Supplier" + si.save() + + po = create_purchase_order(do_not_save=True) + po.shipping_address = customer_shipping.name + self.assertRaises(frappe.ValidationError, po.save) + po.items[0].delivered_by_supplier = 1 + po.save() From ebbecdba23ff3e0f9b7cc710b23616ee75c0dab3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 12 Dec 2025 12:21:10 +0530 Subject: [PATCH 23/43] fix: incorrect invoice qty (cherry picked from commit 96cdb7d54f051986c424fa9b71af1f781e1404f4) --- .../purchase_receipt/purchase_receipt.py | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 7a4101e9dfb..fc3df50de80 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -8,7 +8,7 @@ import frappe from frappe import _, throw from frappe.desk.notifications import clear_doctype_notifications from frappe.model.mapper import get_mapped_doc -from frappe.query_builder.functions import CombineDatetime +from frappe.query_builder.functions import Abs, CombineDatetime, Sum from frappe.utils import cint, flt, get_datetime, getdate, nowdate from pypika import functions as fn @@ -1372,21 +1372,26 @@ def get_invoiced_qty_map(purchase_receipt): def get_returned_qty_map(purchase_receipt): - """returns a map: {so_detail: returned_qty}""" - returned_qty_map = frappe._dict( - frappe.db.sql( - """select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty - from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr - where pr.name = pr_item.parent - and pr.docstatus = 1 - and pr.is_return = 1 - and pr.return_against = %s - """, - purchase_receipt, - ) - ) + """returns a map: {pr_detail: returned_qty}""" - return returned_qty_map + pr = frappe.qb.DocType("Purchase Receipt") + pr_item = frappe.qb.DocType("Purchase Receipt Item") + + query = ( + frappe.qb.from_(pr) + .inner_join(pr_item) + .on(pr.name == pr_item.parent) + .select(pr_item.purchase_receipt_item, Sum(Abs(pr_item.qty)).as_("qty")) + .where( + (pr.docstatus == 1) + & (pr.is_return == 1) + & (pr.return_against == purchase_receipt) + & (pr_item.purchase_receipt_item.isnotnull()) + ) + .groupby(pr_item.purchase_receipt_item) + ).run(as_list=1) + + return frappe._dict(query) if query else frappe._dict() @frappe.whitelist() From a2b6e4a1c587ce2f7e017f39944899f76e3e2f7d Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Fri, 12 Dec 2025 12:52:04 +0530 Subject: [PATCH 24/43] fix: validate budget after cost center allocation (cherry picked from commit f9be8a46fbb64fe78f75939466dca7c2c0f5ae3d) --- erpnext/accounts/general_ledger.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index f965da928ac..13201f4bcbc 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -199,19 +199,20 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_r for d in gl_map: cost_center = d.get("cost_center") + cost_center_allocation = get_cost_center_allocation_data( + gl_map[0]["company"], gl_map[0]["posting_date"], cost_center + ) + + if not cost_center_allocation: + new_gl_map.append(d) + continue + # Validate budget against main cost center if not from_repost: validate_expense_against_budget( d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision) ) - cost_center_allocation = get_cost_center_allocation_data( - gl_map[0]["company"], gl_map[0]["posting_date"], cost_center - ) - if not cost_center_allocation: - new_gl_map.append(d) - continue - if d.account == round_off_account: d.cost_center = cost_center_allocation[0][0] new_gl_map.append(d) @@ -414,7 +415,11 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False): gle.flags.notify_update = False gle.submit() - if not from_repost and gle.voucher_type != "Period Closing Voucher": + if ( + not from_repost + and gle.voucher_type != "Period Closing Voucher" + and (gle.is_cancelled == 0 or gle.voucher_type == "Journal Entry") + ): validate_expense_against_budget(args) From d09857294cb4ab6c670d45f60d44f053febb7515 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 12 Dec 2025 17:36:23 +0530 Subject: [PATCH 25/43] fix: stock ageing report (cherry picked from commit cb84ffd972a07cd2f5dc7fd6861dc089bb205b77) --- erpnext/stock/report/stock_ageing/stock_ageing.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 0edce832aec..46f655c8b11 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -273,6 +273,7 @@ class FIFOSlots: else: serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle) or [] + serial_nos = self.uppercase_serial_nos(serial_nos) if d.actual_qty > 0: self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos) else: @@ -289,6 +290,10 @@ class FIFOSlots: return self.item_details + def uppercase_serial_nos(self, serial_nos): + "Convert serial nos to uppercase for uniformity." + return [sn.upper() for sn in serial_nos] + def __init_key_stores(self, row: dict) -> tuple: "Initialise keys and FIFO Queue." From dab8ac7b1d36446785310de5c15a88275241c583 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 14 Dec 2025 12:10:26 +0530 Subject: [PATCH 26/43] fix: Short circuit guest perm checks --- erpnext/support/doctype/issue/issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 3ec23cf1481..b6023b47e76 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -199,7 +199,7 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord customer = contact_doc.get_link_for("Customer") ignore_permissions = False - if is_website_user(): + if is_website_user() and user != "Guest": if not filters: filters = {} From ae90ee3f1721f784d11c0c736baa2ad7ce8a9cb8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 14:30:11 +0530 Subject: [PATCH 27/43] Merge pull request #51087 from frappe/mergify/bp/version-15-hotfix/pr-51063 fix(transaction-deletion): Add virtual doctypes to the list of ignored doctypes (backport #51063) --- .../transaction_deletion_record.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index ae4b1db1232..9fa32c9d5ec 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -347,8 +347,9 @@ class TransactionDeletionRecord(Document): self.db_set("error_log", None) def get_doctypes_to_be_ignored_list(self): - singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name") - doctypes_to_be_ignored_list = singles + doctypes_to_be_ignored_list = frappe.get_all( + "DocType", or_filters=[["issingle", "=", 1], ["is_virtual", "=", 1]], pluck="name" + ) for doctype in self.doctypes_to_be_ignored: doctypes_to_be_ignored_list.append(doctype.doctype_name) From 9e8bb9b235f6723b28fe0568915ee52ed2e52262 Mon Sep 17 00:00:00 2001 From: Anjali Patel Date: Wed, 13 Aug 2025 09:33:54 +0000 Subject: [PATCH 28/43] fix: prevent self in "Reports To" dropdown (UI-level check) Ensures employee cannot select themselves in the "Reports To" field via UI. This complements server-side validation by improving UX. (cherry picked from commit 608d38a172ef20f2b44c6a2b0d5032406d1f5947) --- erpnext/setup/doctype/employee/employee.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index 2e45d5511b3..d245935d715 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -11,7 +11,13 @@ erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.fo }; }; this.frm.fields_dict.reports_to.get_query = function (doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.employee_query" }; + // return { query: "erpnext.controllers.queries.employee_query" }; + return { + filters: [ + ["status", "=", "Active"], // only active employees + ["name", "!=", doc.name] // exclude self + ] + }; }; } From e1dc80b6d859efc955f88083d0c7accdd3e0b38c Mon Sep 17 00:00:00 2001 From: Anjali Patel Date: Wed, 13 Aug 2025 13:37:48 +0000 Subject: [PATCH 29/43] fix: add missing query key in 'Reports To' field filter (cherry picked from commit cbfb14a6547258a21ea2c73c043b7069eec79a18) --- erpnext/setup/doctype/employee/employee.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index d245935d715..ee6a9d31201 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -11,12 +11,12 @@ erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.fo }; }; this.frm.fields_dict.reports_to.get_query = function (doc, cdt, cdn) { - // return { query: "erpnext.controllers.queries.employee_query" }; return { + query: "erpnext.controllers.queries.employee_query", filters: [ - ["status", "=", "Active"], // only active employees - ["name", "!=", doc.name] // exclude self - ] + ["status", "=", "Active"], + ["name", "!=", doc.name], + ], }; }; } From 0d5e45bb7c45dce5b8368ec63c5721ba4bcadb42 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Thu, 27 Nov 2025 19:15:46 +0530 Subject: [PATCH 30/43] fix: only show net gl balance as opening in general ledger (cherry picked from commit b7c7e0746e759a39a69f1662a0e65aa750922d65) --- .../report/general_ledger/general_ledger.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 100dcd46c85..d28311d8876 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -482,7 +482,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot immutable_ledger = frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger") - def update_value_in_dict(data, key, gle): + def update_value_in_dict(data, key, gle, show_net_values=False): data[key].debit += gle.debit data[key].credit += gle.credit @@ -493,10 +493,14 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot data[key].debit_in_transaction_currency += gle.debit_in_transaction_currency data[key].credit_in_transaction_currency += gle.credit_in_transaction_currency - if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in ( - "Receivable", - "Payable", - ): + if ( + filters.get("show_net_values_in_party_account") + and account_type_map.get(data[key].account) + in ( + "Receivable", + "Payable", + ) + ) or show_net_values: net_value = data[key].debit - data[key].credit net_value_in_account_currency = ( data[key].debit_in_account_currency - data[key].credit_in_account_currency @@ -526,11 +530,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries): if not group_by_voucher_consolidated: - update_value_in_dict(gle_map[group_by_value].totals, "opening", gle) - update_value_in_dict(gle_map[group_by_value].totals, "closing", gle) + update_value_in_dict(gle_map[group_by_value].totals, "opening", gle, True) + update_value_in_dict(gle_map[group_by_value].totals, "closing", gle, True) - update_value_in_dict(totals, "opening", gle) - update_value_in_dict(totals, "closing", gle) + update_value_in_dict(totals, "opening", gle, True) + update_value_in_dict(totals, "closing", gle, True) elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries): if not group_by_voucher_consolidated: From 98eeff8775ec9ca4f6ae1d040cfe37343ffaa1da Mon Sep 17 00:00:00 2001 From: Venkatesh <47534423+venkat102@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:07:06 +0530 Subject: [PATCH 31/43] fix: validate available stock with multiple dimensions (backport #50937) (#50983) * fix: validate available stock with multiple dimensions * test: validate negative stock with multiple inventory dimensions * chore: reset document_wise_inventory_dimensions --- .../test_inventory_dimension.py | 57 ++++++++++++++++++- .../stock_ledger_entry/stock_ledger_entry.py | 32 ++++++----- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 0136bcfcb9d..bfa1d8821ad 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -504,6 +504,61 @@ class TestInventoryDimension(FrappeTestCase): self.assertEqual(site_name, "Site 1") + def test_validate_negative_stock_with_multiple_dimension(self): + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0) + item_code = "Test Negative Multi Inventory Dimension Item" + create_item(item_code) + + inv_dimension_1 = create_inventory_dimension( + apply_to_all_doctypes=1, + dimension_name="Inv Site", + reference_document="Inv Site", + document_type="Inv Site", + validate_negative_stock=1, + ) + inv_dimension_1.db_set("validate_negative_stock", 1) + + inv_dimension_2 = create_inventory_dimension( + apply_to_all_doctypes=1, + dimension_name="Rack", + reference_document="Rack", + document_type="Rack", + validate_negative_stock=1, + ) + inv_dimension_2.db_set("validate_negative_stock", 1) + frappe.local.inventory_dimensions = {} + frappe.local.document_wise_inventory_dimensions = {} + + pr_doc = make_purchase_receipt(item_code=item_code, qty=30, do_not_submit=True) + pr_doc.items[0].inv_site = "Site 1" + pr_doc.items[0].rack = "Rack 1" + pr_doc.save() + pr_doc.submit() + + pr_doc = make_purchase_receipt(item_code=item_code, qty=15, do_not_submit=True) + pr_doc.items[0].inv_site = "Site 1" + pr_doc.items[0].rack = "Rack 2" + pr_doc.save() + pr_doc.submit() + + pr_doc = make_purchase_receipt(item_code=item_code, qty=30, do_not_submit=True) + pr_doc.items[0].inv_site = "Site 2" + pr_doc.items[0].rack = "Rack 1" + pr_doc.save() + pr_doc.submit() + + pr_doc = make_purchase_receipt(item_code=item_code, qty=25, do_not_submit=True) + pr_doc.items[0].inv_site = "Site 2" + pr_doc.items[0].rack = "Rack 2" + pr_doc.save() + pr_doc.submit() + + dn_doc = create_delivery_note(item_code=item_code, qty=35, do_not_submit=True) + dn_doc.items[0].inv_site = "Site 2" + dn_doc.items[0].rack = "Rack 1" + dn_doc.save() + self.assertRaises(InventoryDimensionNegativeStockError, dn_doc.submit) + def get_voucher_sl_entries(voucher_no, fields): return frappe.get_all( @@ -593,7 +648,7 @@ def prepare_test_data(): } ).insert(ignore_permissions=True) - for rack in ["Rack 1"]: + for rack in ["Rack 1", "Rack 2"]: if not frappe.db.exists("Rack", rack): frappe.get_doc({"doctype": "Rack", "rack_name": rack}).insert(ignore_permissions=True) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index ddef027d0b8..5344636dfaa 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -113,17 +113,15 @@ class StockLedgerEntry(Document): return flt_precision = cint(frappe.db.get_default("float_precision")) or 2 - for dimension, values in dimensions.items(): - dimension_value = values.get("value") - available_qty = self.get_available_qty_after_prev_transaction(dimension, dimension_value) + available_qty = self.get_available_qty_after_prev_transaction(dimensions) - diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction - if diff < 0 and abs(diff) > 0.0001: - self.throw_validation_error(diff, dimension, dimension_value) + diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction + if diff < 0 and abs(diff) > 0.0001: + self.throw_validation_error(diff, dimensions) - def get_available_qty_after_prev_transaction(self, dimension, dimension_value): + def get_available_qty_after_prev_transaction(self, dimensions): sle = frappe.qb.DocType("Stock Ledger Entry") - available_qty = ( + available_qty_query = ( frappe.qb.from_(sle) .select(Sum(sle.actual_qty)) .where( @@ -132,21 +130,27 @@ class StockLedgerEntry(Document): & (sle.posting_datetime < self.posting_datetime) & (sle.company == self.company) & (sle.is_cancelled == 0) - & (sle[dimension] == dimension_value) ) - ).run() + ) + + for dimension, values in dimensions.items(): + dimension_value = values.get("value") + available_qty_query = available_qty_query.where(sle[dimension] == dimension_value) + + available_qty = available_qty_query.run() return available_qty[0][0] or 0 - def throw_validation_error(self, diff, dimension, dimension_value): + def throw_validation_error(self, diff, dimensions): msg = _( - "{0} units of {1} are required in {2} with the inventory dimension: {3} ({4}) on {5} {6} for {7} to complete the transaction." + "{0} units of {1} are required in {2} with the inventory dimension: {3} on {4} {5} for {6} to complete the transaction." ).format( abs(diff), frappe.get_desk_link("Item", self.item_code), frappe.get_desk_link("Warehouse", self.warehouse), - frappe.bold(dimension), - frappe.bold(dimension_value), + frappe.bold( + ", ".join([f"{dimension}: {values.get('value')}" for dimension, values in dimensions.items()]) + ), self.posting_date, self.posting_time, frappe.get_desk_link(self.voucher_type, self.voucher_no), From fa04e368d3e32452f45b49863bb40d7a2136daaa Mon Sep 17 00:00:00 2001 From: Imesha Sudasingha Date: Mon, 15 Dec 2025 20:23:06 +0530 Subject: [PATCH 32/43] fix: preserve user-entered exchange rates in ERR journal entries The JE creation was overriding exchange_rate=1 with the system rate. Set ignore_exchange_rate flag to preserve user values. --- .../exchange_rate_revaluation/exchange_rate_revaluation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index c08bd3878d5..2bb7aef8b99 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -485,6 +485,9 @@ class ExchangeRateRevaluation(Document): journal_entry.posting_date = self.posting_date journal_entry.multi_currency = 1 + # Prevent JE from overriding user-entered exchange rates (e.g., rate of 1) + journal_entry.flags.ignore_exchange_rate = True + journal_entry_accounts = [] for d in accounts: if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")): From a61890ec2b5dfbe77b286d5c32c6e560b9eca075 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:04:16 +0100 Subject: [PATCH 33/43] feat: introduce extended bank transaction fields (backport #50021) (#51112) Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> Co-authored-by: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com> --- .../bank_transaction/bank_transaction.json | 27 +++- .../bank_transaction/bank_transaction.py | 38 +++++ .../test_bank_transaction_fees.py | 133 ++++++++++++++++++ 3 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 erpnext/accounts/doctype/bank_transaction/test_bank_transaction_fees.py diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json index 599a0604755..99622314532 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json @@ -38,7 +38,10 @@ "column_break_3czf", "bank_party_name", "bank_party_account_number", - "bank_party_iban" + "bank_party_iban", + "extended_bank_statement_section", + "included_fee", + "excluded_fee" ], "fields": [ { @@ -233,12 +236,32 @@ { "fieldname": "column_break_oufv", "fieldtype": "Column Break" + }, + { + "fieldname": "extended_bank_statement_section", + "fieldtype": "Section Break", + "label": "Extended Bank Statement" + }, + { + "fieldname": "included_fee", + "fieldtype": "Currency", + "label": "Included Fee", + "non_negative": 1, + "options": "currency" + }, + { + "description": "On save, the Excluded Fee will be converted to an Included Fee.", + "fieldname": "excluded_fee", + "fieldtype": "Currency", + "label": "Excluded Fee", + "non_negative": 1, + "options": "currency" } ], "grid_page_length": 50, "is_submittable": 1, "links": [], - "modified": "2025-10-23 17:32:58.514807", + "modified": "2025-12-07 20:49:18.600757", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Transaction", diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 16c84ac2a60..f9f1b54406b 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -32,6 +32,8 @@ class BankTransaction(Document): date: DF.Date | None deposit: DF.Currency description: DF.SmallText | None + excluded_fee: DF.Currency + included_fee: DF.Currency naming_series: DF.Literal["ACC-BTN-.YYYY.-"] party: DF.DynamicLink | None party_type: DF.Link | None @@ -45,9 +47,11 @@ class BankTransaction(Document): # end: auto-generated types def before_validate(self): + self.handle_excluded_fee() self.update_allocated_amount() def validate(self): + self.validate_included_fee() self.validate_duplicate_references() self.validate_currency() @@ -307,6 +311,40 @@ class BankTransaction(Document): self.party_type, self.party = result + def validate_included_fee(self): + """ + The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is + therefore outside of the deposit value and can be larger than the deposit itself. + """ + + if self.included_fee and self.withdrawal: + if self.included_fee > self.withdrawal: + frappe.throw(_("Included fee is bigger than the withdrawal itself.")) + + def handle_excluded_fee(self): + # Include the excluded fee on validate to handle all further processing the same + excluded_fee = flt(self.excluded_fee) + if excluded_fee <= 0: + return + + # Suppress a negative deposit (aka withdrawal), likely not intendend + if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0: + frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from.")) + + # Enforce directionality + if flt(self.deposit) > 0 and flt(self.withdrawal) > 0: + frappe.throw( + _("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.") + ) + + if flt(self.deposit) > 0: + self.deposit = flt(self.deposit) - excluded_fee + # A fee applied to deposit and withdrawal equal 0 become a withdrawal + elif flt(self.withdrawal) >= 0: + self.withdrawal = flt(self.withdrawal) + excluded_fee + self.included_fee = flt(self.included_fee) + excluded_fee + self.excluded_fee = 0 + @frappe.whitelist() def get_doctypes_for_bank_reconciliation(): diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction_fees.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction_fees.py new file mode 100644 index 00000000000..13f4f9cfd79 --- /dev/null +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction_fees.py @@ -0,0 +1,133 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBankTransactionFees(FrappeTestCase): + def test_included_fee_throws(self): + """A fee that's part of a withdrawal cannot be bigger than the + withdrawal itself.""" + bt = frappe.new_doc("Bank Transaction") + bt.withdrawal = 100 + bt.included_fee = 101 + + self.assertRaises(frappe.ValidationError, bt.validate_included_fee) + + def test_included_fee_allows_equal(self): + """A fee that's part of a withdrawal may be equal to the withdrawal + amount (only the fee was deducted from the account).""" + bt = frappe.new_doc("Bank Transaction") + bt.withdrawal = 100 + bt.included_fee = 100 + + bt.validate_included_fee() + + def test_included_fee_allows_for_deposit(self): + """For deposits, a fee may be recorded separately without limiting the + received amount.""" + bt = frappe.new_doc("Bank Transaction") + bt.deposit = 10 + bt.included_fee = 999 + + bt.validate_included_fee() + + def test_excluded_fee_noop_when_zero(self): + """When there is no excluded fee to apply, the amounts should remain + unchanged.""" + bt = frappe.new_doc("Bank Transaction") + bt.deposit = 100 + bt.withdrawal = 0 + bt.included_fee = 5 + bt.excluded_fee = 0 + + bt.handle_excluded_fee() + + self.assertEqual(bt.deposit, 100) + self.assertEqual(bt.withdrawal, 0) + self.assertEqual(bt.included_fee, 5) + self.assertEqual(bt.excluded_fee, 0) + + def test_excluded_fee_throws_when_exceeds_deposit(self): + """A fee deducted from an incoming payment must not exceed the incoming + amount (else it would be a withdrawal, a conversion we don't support).""" + bt = frappe.new_doc("Bank Transaction") + bt.deposit = 10 + bt.excluded_fee = 11 + + self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee) + + def test_excluded_fee_throws_when_both_deposit_and_withdrawal_are_set(self): + """A transaction must be either incoming or outgoing when applying a + fee, not both.""" + bt = frappe.new_doc("Bank Transaction") + bt.deposit = 10 + bt.withdrawal = 10 + bt.excluded_fee = 1 + + self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee) + + def test_excluded_fee_deducts_from_deposit(self): + """When a fee is deducted from an incoming payment, the net received + amount decreases and the fee is tracked as included.""" + bt = frappe.new_doc("Bank Transaction") + bt.deposit = 100 + bt.withdrawal = 0 + bt.included_fee = 2 + bt.excluded_fee = 5 + + bt.handle_excluded_fee() + + self.assertEqual(bt.deposit, 95) + self.assertEqual(bt.withdrawal, 0) + self.assertEqual(bt.included_fee, 7) + self.assertEqual(bt.excluded_fee, 0) + + def test_excluded_fee_can_reduce_an_incoming_payment_to_zero(self): + """A separately-deducted fee may reduce an incoming payment to zero, + while still tracking the fee.""" + bt = frappe.new_doc("Bank Transaction") + bt.deposit = 5 + bt.withdrawal = 0 + bt.included_fee = 0 + bt.excluded_fee = 5 + + bt.handle_excluded_fee() + + self.assertEqual(bt.deposit, 0) + self.assertEqual(bt.withdrawal, 0) + self.assertEqual(bt.included_fee, 5) + self.assertEqual(bt.excluded_fee, 0) + + def test_excluded_fee_increases_outgoing_payment(self): + """When a separately-deducted fee is provided for an outgoing payment, + the total money leaving increases and the fee is tracked.""" + bt = frappe.new_doc("Bank Transaction") + bt.deposit = 0 + bt.withdrawal = 100 + bt.included_fee = 2 + bt.excluded_fee = 5 + + bt.handle_excluded_fee() + + self.assertEqual(bt.deposit, 0) + self.assertEqual(bt.withdrawal, 105) + self.assertEqual(bt.included_fee, 7) + self.assertEqual(bt.excluded_fee, 0) + + def test_excluded_fee_turns_zero_amount_into_withdrawal(self): + """If only an excluded fee is provided, it should be treated as an + outgoing payment and the fee is then tracked as included.""" + bt = frappe.new_doc("Bank Transaction") + bt.deposit = 0 + bt.withdrawal = 0 + bt.included_fee = 0 + bt.excluded_fee = 5 + + bt.handle_excluded_fee() + + self.assertEqual(bt.deposit, 0) + self.assertEqual(bt.withdrawal, 5) + self.assertEqual(bt.included_fee, 5) + self.assertEqual(bt.excluded_fee, 0) From 41659a875b23a8dc65c6cead2a9e091a6a4f4453 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Fri, 12 Dec 2025 14:25:08 +0530 Subject: [PATCH 34/43] refactor: standardize cost_center updation across transactions (cherry picked from commit c28f6f1856daf22d295525e2311f2dc966fb4b73) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.js --- .../doctype/purchase_invoice/purchase_invoice.js | 11 ----------- .../accounts/doctype/sales_invoice/sales_invoice.js | 4 ---- erpnext/public/js/controllers/transaction.js | 8 ++------ erpnext/stock/doctype/delivery_note/delivery_note.js | 4 ---- 4 files changed, 2 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 2e7e0314aba..52f68180267 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -575,17 +575,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function }; }; -cur_frm.cscript.cost_center = function (doc, cdt, cdn) { - var d = locals[cdt][cdn]; - if (d.cost_center) { - var cl = doc.items || []; - for (var i = 0; i < cl.length; i++) { - if (!cl[i].cost_center) cl[i].cost_center = d.cost_center; - } - } - refresh_field("items"); -}; - cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) { return { filters: [["Project", "status", "not in", "Completed, Cancelled"]], diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index abb5d76b9d8..9711c4637bc 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -648,10 +648,6 @@ cur_frm.cscript.expense_account = function (doc, cdt, cdn) { erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "expense_account"); }; -cur_frm.cscript.cost_center = function (doc, cdt, cdn) { - erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center"); -}; - cur_frm.set_query("debit_to", function (doc) { return { filters: { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index f61de798390..1b23042769e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1093,12 +1093,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.frm.refresh_field("payment_schedule"); } - cost_center(doc) { - this.frm.doc.items.forEach((item) => { - item.cost_center = doc.cost_center; - }); - - this.frm.refresh_field("items"); + cost_center(doc, cdt, cdn) { + erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center"); } due_date(doc, cdt, cdn) { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index b9c98bbb8ba..cdb219180ad 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -130,10 +130,6 @@ frappe.ui.form.on("Delivery Note Item", { var d = locals[dt][dn]; frm.update_in_all_rows("items", "expense_account", d.expense_account); }, - cost_center: function (frm, dt, dn) { - var d = locals[dt][dn]; - frm.update_in_all_rows("items", "cost_center", d.cost_center); - }, }); erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends ( From 5d5dff9103c45ebb8f157d168184477163ffba42 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Tue, 16 Dec 2025 13:10:38 +0530 Subject: [PATCH 35/43] fix: prevent dispatch address copying on drop ship --- erpnext/selling/doctype/sales_order/sales_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a6f1cf4b666..8be426fa1a5 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1408,7 +1408,6 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t { "Sales Order": { "doctype": "Purchase Order", - "field_map": {"dispatch_address_name": "dispatch_address"}, "field_no_map": [ "address_display", "contact_display", @@ -1417,6 +1416,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t "contact_person", "taxes_and_charges", "shipping_address", + "dispatch_address", ], "validation": {"docstatus": ["=", 1]}, }, @@ -1549,7 +1549,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): { "Sales Order": { "doctype": "Purchase Order", - "field_map": {"dispatch_address_name": "dispatch_address"}, "field_no_map": [ "address_display", "contact_display", @@ -1558,6 +1557,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "contact_person", "taxes_and_charges", "shipping_address", + "dispatch_address", ], "validation": {"docstatus": ["=", 1]}, }, From 16c8b74d52fd48e99f063ee4e42ea7934f646b24 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 16 Dec 2025 14:05:28 +0530 Subject: [PATCH 36/43] fix: ensure type on method parameter (cherry picked from commit c055e86e51b9188829c13f5be682ebf52633c92f) --- erpnext/accounts/doctype/payment_request/payment_request.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 5737956fe86..752085e5f99 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -545,6 +545,9 @@ def make_payment_request(**args): if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST: frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt))) + if args.dn and not isinstance(args.dn, str): + frappe.throw(_("Invalid parameter. 'dn' should be of type str")) + ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn) if not args.get("company"): args.company = ref_doc.company From c01e40da3cc5f8e7441dce86ba96a917348d27f8 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Thu, 11 Dec 2025 16:17:26 +0530 Subject: [PATCH 37/43] fix(payment entry): fetch gain loss account from company boot (cherry picked from commit 8e54be78086bb210be4302913852f5ca2d85de37) --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 7 +++---- erpnext/startup/boot.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 7ca374e6575..31f0235d147 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1302,15 +1302,14 @@ frappe.ui.form.on("Payment Entry", { let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss); if (!row) { - const response = await get_company_defaults(frm.doc.company); - + const company_defaults = frappe.get_doc(":Company", frm.doc.company); const account = - response.message?.[account_fieldname] || + company_defaults?.[account_fieldname] || (await prompt_for_missing_account(frm, account_fieldname)); row = frm.add_child("deductions"); row.account = account; - row.cost_center = response.message?.cost_center; + row.cost_center = company_defaults?.cost_center; row.is_exchange_gain_loss = 1; } diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index b17b6b49b6b..83816f6cc09 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -50,7 +50,7 @@ def boot_session(bootinfo): bootinfo.docs += frappe.db.sql( """select name, default_currency, cost_center, default_selling_terms, default_buying_terms, - default_letter_head, default_bank_account, enable_perpetual_inventory, country from `tabCompany`""", + default_letter_head, default_bank_account, enable_perpetual_inventory, country, exchange_gain_loss_account from `tabCompany`""", as_dict=1, update={"doctype": ":Company"}, ) From 3a9888aad93d030af773b09b35be313232d8109f Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 16 Dec 2025 17:37:39 +0530 Subject: [PATCH 38/43] perf: sabb validate serial no (#51132) --- .../doctype/serial_and_batch_bundle/serial_and_batch_bundle.py | 2 ++ 1 file changed, 2 insertions(+) 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 d53382b7047..e20363cec3a 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 @@ -2008,6 +2008,8 @@ def get_available_serial_nos(kwargs): filters["name"] = ("in", time_based_serial_nos) elif ignore_serial_nos: filters["name"] = ("not in", ignore_serial_nos) + elif kwargs.get("serial_nos"): + filters["name"] = ("in", kwargs.get("serial_nos")) if kwargs.get("batches"): batches = get_non_expired_batches(kwargs.get("batches")) From 2c9c6c379829fc889fe1e2bb3c1dbe98d9309aad Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:11:44 +0530 Subject: [PATCH 39/43] fix(subcontract): ignore BOM qty validation for alternative items (backport #51122) (#51135) fix(subcontract): ignore BOM qty validation for alternative items (#51122) (cherry picked from commit 2f19244660b0d26a0c3915c825e776f1eb377a33) Co-authored-by: Kavin <78342682+kavin-114@users.noreply.github.com> --- .../subcontracting_receipt/subcontracting_receipt.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 15e0259722f..cd20986dc5a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -543,6 +543,12 @@ class SubcontractingReceipt(SubcontractingController): for row in self.items: precision = row.precision("qty") + + # if allow alternative item, ignore the validation as per BOM required qty + is_allow_alternative_item = frappe.db.get_value("BOM", row.bom, "allow_alternative_item") + if is_allow_alternative_item: + continue + for bom_item in self._get_materials_from_bom( row.item_code, row.bom, row.get("include_exploded_items") ): From 325fc619dc2e03cb68d0f77534c5b31eddab2d48 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 16 Dec 2025 17:47:23 +0530 Subject: [PATCH 40/43] fix: delayed tasks summary chart color (cherry picked from commit 38affb056245b7e7b71e29791b7d0ece89a0fd79) --- .../delayed_tasks_summary.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py index fe47cf7541e..b93a43239b4 100644 --- a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py +++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py @@ -75,13 +75,27 @@ def get_chart_data(data): delay = delay + 1 else: on_track = on_track + 1 + + labels = [] + datasets = [] + colors = [] + + if on_track: + labels.append(_("On Track")) + datasets.append(on_track) + colors.append("#84D5BA") + if delay: + labels.append(_("Delayed")) + datasets.append(delay) + colors.append("#CB4B5F") + charts = { "data": { - "labels": [_("On Track"), _("Delayed")], - "datasets": [{"name": _("Delayed"), "values": [on_track, delay]}], + "labels": labels, + "datasets": [{"name": _("Delayed"), "values": datasets}], }, "type": "percentage", - "colors": ["#84D5BA", "#CB4B5F"], + "colors": colors, } return charts From 6a6398a3928b83fa0da6e85fecd387e9f24bf86e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:22:15 +0530 Subject: [PATCH 41/43] fix: add validation for transferred qty and handle MR transfer status for in-transit entry. (backport #50683) (#51134) fix: add validation for transferred qty and handle MR transfer status for in-transit entry. (#50683) * fix: add validation for transferred qty * fix: modify if statement * test: add unit test for mr transfer status in-transit entry (cherry picked from commit 890316a793c0907397ab663696ddeb72d588c431) Co-authored-by: Logesh Periyasamy --- .../material_request/test_material_request.py | 44 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 34 +++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 7bcab314e1a..ce5affdfdbe 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -19,6 +19,7 @@ from erpnext.stock.doctype.material_request.material_request import ( make_supplier_quotation, raise_work_orders, ) +from erpnext.stock.doctype.stock_entry.stock_entry import make_stock_in_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -926,6 +927,49 @@ class TestMaterialRequest(FrappeTestCase): pl_for_pending = create_pick_list(mr.name) self.assertEqual(pl_for_pending.locations[0].qty, 5) + def test_mr_status_with_partial_and_excess_end_transit(self): + material_request = make_material_request( + material_request_type="Material Transfer", + item_code="_Test Item Home Desktop 100", + ) + + in_transit_wh = get_in_transit_warehouse(material_request.company) + + # Make sure stock is available in source warehouse + self._insert_stock_entry(20.0, 20.0) + + # Stock Entry (Transfer to In-Transit) + stock_entry_1 = make_in_transit_stock_entry(material_request.name, in_transit_wh) + stock_entry_1.items[0].update( + { + "qty": 5, + "s_warehouse": "_Test Warehouse 1 - _TC", + } + ) + stock_entry_1.save().submit() + + stock_entry_2 = make_in_transit_stock_entry(material_request.name, in_transit_wh) + stock_entry_2.items[0].update( + { + "qty": 5, + "s_warehouse": "_Test Warehouse 1 - _TC", + } + ) + stock_entry_2.save().submit() + + end_transit_1 = make_stock_in_entry(stock_entry_1.name) + end_transit_1.save().submit() + + # Material Request Transfer Status should still be In Transit + material_request.load_from_db() + self.assertEqual(material_request.transfer_status, "In Transit") + + end_transit_2 = make_stock_in_entry(stock_entry_2.name) + end_transit_2.items[0].update({"qty": 6}) # More than transferred + end_transit_2.save() + + self.assertRaises(frappe.ValidationError, end_transit_2.submit) + def get_in_transit_warehouse(company): if not frappe.db.exists("Warehouse Type", "Transit"): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e3e7c461ef7..a441664ab9c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -8,6 +8,7 @@ from collections import defaultdict import frappe from frappe import _, bold from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import DocType from frappe.query_builder.functions import Sum from frappe.utils import ( cint, @@ -2815,6 +2816,17 @@ class StockEntry(StockController): }, ) + if d.docstatus == 1: + transfer_qty = frappe.get_value("Stock Entry Detail", d.ste_detail, "transfer_qty") + + if transferred_qty and transferred_qty[0]: + if transferred_qty[0].qty > transfer_qty: + frappe.throw( + _( + "Row {0}: Transferred quantity cannot be greater than the requested quantity." + ).format(d.idx) + ) + stock_entries[(d.against_stock_entry, d.ste_detail)] = ( transferred_qty[0].qty if transferred_qty and transferred_qty[0] else 0.0 ) or 0.0 @@ -2878,7 +2890,7 @@ class StockEntry(StockController): parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, "add_to_transit") for item in self.items: - material_request = item.material_request or None + material_request = item.get("material_request") if self.purpose == "Material Transfer" and material_request not in material_requests: if self.outgoing_stock_entry and parent_se: material_request = frappe.get_value( @@ -2887,6 +2899,11 @@ class StockEntry(StockController): if material_request and material_request not in material_requests: material_requests.append(material_request) + if status == "Completed": + qty = get_transferred_qty(material_request) + if qty.get("transfer_qty") > qty.get("transferred_qty"): + status = "In Transit" + frappe.db.set_value("Material Request", material_request, "transfer_status", status) def set_serial_no_batch_for_finished_good(self): @@ -3545,3 +3562,18 @@ def get_batchwise_serial_nos(item_code, row): batchwise_serial_nos[batch_no] = sorted([serial_no.name for serial_no in serial_nos]) return batchwise_serial_nos + + +def get_transferred_qty(material_request): + sed = DocType("Stock Entry Detail") + + query = ( + frappe.qb.from_(sed) + .select( + Sum(sed.transfer_qty).as_("transfer_qty"), + Sum(sed.transferred_qty).as_("transferred_qty"), + ) + .where((sed.material_request == material_request) & (sed.docstatus == 1)) + ).run(as_dict=True) + + return query[0] From 981c9c76c1b8b3916eea1f3809290b5958a0a3b5 Mon Sep 17 00:00:00 2001 From: Afsal Syed Date: Tue, 16 Dec 2025 13:03:31 +0530 Subject: [PATCH 42/43] fix: add link filters for item group in quickentry (cherry picked from commit 3bef6bf5efbb11fc2c660dcd5f28cdd7d5482b52) --- erpnext/stock/doctype/item/item.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 1b9ddd13f2c..ecd11a4fcd6 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -167,6 +167,7 @@ "in_preview": 1, "in_standard_filter": 1, "label": "Item Group", + "link_filters": "[[\"Item Group\",\"is_group\",\"=\",0]]", "oldfieldname": "item_group", "oldfieldtype": "Link", "options": "Item Group", @@ -894,7 +895,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2025-12-04 09:11:56.029567", + "modified": "2025-12-15 20:08:35.634046", "modified_by": "Administrator", "module": "Stock", "name": "Item", From 2aff16928c7b242ea74de9829b1ca330de72e999 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:24:50 +0100 Subject: [PATCH 43/43] fix(Rename Tool): use "Link" field instead of "Select" (cherry picked from commit ba9bbed038611bd6cf4c4ffe3192f5b78d78425e) # Conflicts: # erpnext/utilities/doctype/rename_tool/rename_tool.json # erpnext/utilities/doctype/rename_tool/rename_tool.py --- .../doctype/rename_tool/rename_tool.js | 8 --- .../doctype/rename_tool/rename_tool.json | 58 +++++++------------ .../doctype/rename_tool/rename_tool.py | 4 +- 3 files changed, 23 insertions(+), 47 deletions(-) diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.js b/erpnext/utilities/doctype/rename_tool/rename_tool.js index 47677a62500..621f8484f53 100644 --- a/erpnext/utilities/doctype/rename_tool/rename_tool.js +++ b/erpnext/utilities/doctype/rename_tool/rename_tool.js @@ -2,14 +2,6 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Rename Tool", { - onload: function (frm) { - return frappe.call({ - method: "erpnext.utilities.doctype.rename_tool.rename_tool.get_doctypes", - callback: function (r) { - frm.set_df_property("select_doctype", "options", r.message); - }, - }); - }, refresh: function (frm) { frm.disable_save(); diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.json b/erpnext/utilities/doctype/rename_tool/rename_tool.json index 617354d91ce..cfa08670c76 100644 --- a/erpnext/utilities/doctype/rename_tool/rename_tool.json +++ b/erpnext/utilities/doctype/rename_tool/rename_tool.json @@ -8,27 +8,13 @@ "doctype": "DocType", "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "Type of document to rename.", - "fieldname": "select_doctype", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Select DocType", - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "Type of document to rename.", + "fieldname": "select_doctype", + "fieldtype": "Link", + "label": "Select DocType", + "link_filters": "[[\"DocType\",\"allow_rename\",\"=\",1],[\"DocType\",\"module\",\"!=\",\"Core\"]]", + "options": "DocType" + }, { "allow_on_submit": 0, "bold": 0, @@ -72,22 +58,18 @@ "set_only_once": 0, "unique": 0 } - ], - "hide_heading": 0, - "hide_toolbar": 1, - "icon": "fa fa-magic", - "idx": 1, - "in_create": 0, - - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 1, - "modified": "2015-10-19 03:04:49.097140", - "modified_by": "Administrator", - "module": "Utilities", - "name": "Rename Tool", - "owner": "Administrator", + ], + "hide_toolbar": 1, + "icon": "fa fa-magic", + "idx": 1, + "issingle": 1, + "links": [], + "max_attachments": 1, + "modified": "2025-12-09 14:18:33.838623", + "modified_by": "Administrator", + "module": "Utilities", + "name": "Rename Tool", + "owner": "Administrator", "permissions": [ { "amend": 0, @@ -112,4 +94,4 @@ ], "read_only": 0, "read_only_onload": 0 -} \ No newline at end of file +} diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.py b/erpnext/utilities/doctype/rename_tool/rename_tool.py index 230845e55de..0134d9c3e1e 100644 --- a/erpnext/utilities/doctype/rename_tool/rename_tool.py +++ b/erpnext/utilities/doctype/rename_tool/rename_tool.py @@ -7,6 +7,7 @@ import frappe from frappe.model.document import Document from frappe.model.rename_doc import bulk_rename +from frappe.utils.deprecations import deprecated class RenameTool(Document): @@ -19,13 +20,14 @@ class RenameTool(Document): from frappe.types import DF file_to_rename: DF.Attach | None - select_doctype: DF.Literal + select_doctype: DF.Link | None # end: auto-generated types pass @frappe.whitelist() +@deprecated def get_doctypes(): return frappe.db.sql_list( """select name from tabDocType