From d96999de7f992ae41c54d2c472c44d99638d7115 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 28 Jul 2026 10:56:06 +0530 Subject: [PATCH] fix: stop storing "{supplier_name}" / "{customer_name}" as the document title Purchase Order, Sales Order and Subcontracting Order point title_field at the party name field, so Document.set_title_field() never rendered their title template and every new record stored the placeholder verbatim. On Purchase Order the field is also mandatory, so the junk value is guaranteed. Drop the dead defaults (and Purchase Order's reqd, which would otherwise make an always-empty field mandatory) and backfill the affected rows. --- .../purchase_order/purchase_order.json | 6 ++---- .../doctype/purchase_order/purchase_order.py | 2 +- erpnext/patches.txt | 1 + erpnext/patches/v15_0/fix_titles.py | 20 +++++++++++++++++++ .../doctype/sales_order/sales_order.json | 3 +-- .../subcontracting_order.json | 3 +-- erpnext/tests/test_init.py | 15 ++++++++++++++ 7 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 erpnext/patches/v15_0/fix_titles.py diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index cfea482d217..7d92a197a82 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -171,14 +171,12 @@ }, { "allow_on_submit": 1, - "default": "{supplier_name}", "fieldname": "title", "fieldtype": "Data", "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1, - "reqd": 1 + "print_hide": 1 }, { "fieldname": "naming_series", @@ -1309,7 +1307,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2025-07-31 17:19:40.816883", + "modified": "2026-07-28 12:20:11.284370", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 95b67d49429..651c781f4f6 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -162,7 +162,7 @@ class PurchaseOrder(BuyingController): taxes_and_charges_deducted: DF.Currency tc_name: DF.Link | None terms: DF.TextEditor | None - title: DF.Data + title: DF.Data | None to_date: DF.Date | None total: DF.Currency total_net_weight: DF.Float diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4be98e72eea..02103c8071a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -444,3 +444,4 @@ erpnext.patches.v16_0.crm_settings_handle_allowed_users_for_frappe_crm erpnext.patches.v16_0.backfill_pick_list_transferred_qty erpnext.patches.v16_0.access_control_for_project_users erpnext.patches.v16_0.rename_ar_ap_ageing_filter +erpnext.patches.v15_0.fix_titles diff --git a/erpnext/patches/v15_0/fix_titles.py b/erpnext/patches/v15_0/fix_titles.py new file mode 100644 index 00000000000..32352413c3a --- /dev/null +++ b/erpnext/patches/v15_0/fix_titles.py @@ -0,0 +1,20 @@ +import frappe + + +def execute(): + """ + These doctypes point `title_field` at the party name field, so their `title` + default was never rendered and got stored as the literal template string. + """ + + for doctype, source_field in ( + ("Purchase Order", "supplier_name"), + ("Subcontracting Order", "supplier_name"), + ("Sales Order", "customer_name"), + ): + table = frappe.qb.DocType(doctype) + ( + frappe.qb.update(table) + .set(table.title, table[source_field]) + .where(table.title == f"{{{source_field}}}") + ).run() diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index e649b8e9383..876cc5bb760 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -187,7 +187,6 @@ }, { "allow_on_submit": 1, - "default": "{customer_name}", "fieldname": "title", "fieldtype": "Data", "hidden": 1, @@ -1680,7 +1679,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2026-03-06 15:33:49.059029", + "modified": "2026-07-28 12:20:44.130918", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json index 206e3135dfb..cbcf1ddc03a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json @@ -66,7 +66,6 @@ "fields": [ { "allow_on_submit": 1, - "default": "{supplier_name}", "fieldname": "title", "fieldtype": "Data", "hidden": 1, @@ -465,7 +464,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2024-12-06 15:21:49.924146", + "modified": "2026-07-28 12:21:09.663812", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order", diff --git a/erpnext/tests/test_init.py b/erpnext/tests/test_init.py index 2b4ea9fa8dc..1509fb16c2d 100644 --- a/erpnext/tests/test_init.py +++ b/erpnext/tests/test_init.py @@ -49,3 +49,18 @@ class TestInit(unittest.TestCase): from frappe.tests.test_patches import check_patch_files check_patch_files("erpnext") + + def test_no_unrendered_title_templates(self): + modules = frappe.get_all("Module Def", filters={"app_name": "erpnext"}, pluck="name") + for doctype in frappe.get_all("DocType", filters={"module": ("in", modules)}, pluck="name"): + meta = frappe.get_meta(doctype) + field = meta.get_field("title") + if not field or not field.default or "{" not in field.default: + continue + + self.assertEqual( + meta.title_field, + "title", + f"{doctype}: title default {field.default!r} is stored verbatim because " + "Document.set_title_field() only renders it when title_field is 'title'", + )