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'", + )