From 45443104195c7338a4147000cb68f868f410e483 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 2 Feb 2026 17:39:30 +0530 Subject: [PATCH 1/5] feat: evaluate virtual docfield value in get method --- frappe/model/base_document.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 6c88dfefd6..44e4371e40 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -311,7 +311,7 @@ class BaseDocument: def get_db_value(self, key): return frappe.db.get_value(self.doctype, self.name, key) - def get(self, key, filters=None, limit=None, default=None): + def get(self, key, filters=None, limit=None, default=None, ignore_virtual=False): if isinstance(key, dict): return _filter(self.get_all_children(), key, limit=limit) @@ -327,6 +327,20 @@ class BaseDocument: if limit and isinstance(value, list | tuple) and len(value) > limit: value = value[:limit] + if not value: + df = self.meta.get_field(key) + is_virtual_field = getattr(df, "is_virtual", False) + + if is_virtual_field: + if ignore_virtual or key not in self.permitted_fieldnames: + return value + + if (prop := getattr(type(self), key, None)) and is_a_property(prop): + value = getattr(self, key) + + elif options := getattr(df, "options", None): + value = self._evaluate_virtual_field_options(options) + return value def getone(self, key, filters=None): From dbfa0495ab0872c6d1ee8571953dcfd173aa6d20 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 2 Feb 2026 18:07:52 +0530 Subject: [PATCH 2/5] refactor: common util for fetching virtual field value --- frappe/model/base_document.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 44e4371e40..bd8699c0d1 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -311,6 +311,15 @@ class BaseDocument: def get_db_value(self, key): return frappe.db.get_value(self.doctype, self.name, key) + def get_virtual_field_value(self, df): + fieldname = df.fieldname + + if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop): + return getattr(self, fieldname) + + elif options := getattr(df, "options", None): + return self._evaluate_virtual_field_options(options) + def get(self, key, filters=None, limit=None, default=None, ignore_virtual=False): if isinstance(key, dict): return _filter(self.get_all_children(), key, limit=limit) @@ -329,17 +338,12 @@ class BaseDocument: if not value: df = self.meta.get_field(key) + is_virtual_field = getattr(df, "is_virtual", False) + ignore_virtual = ignore_virtual or key not in self.permitted_fieldnames - if is_virtual_field: - if ignore_virtual or key not in self.permitted_fieldnames: - return value - - if (prop := getattr(type(self), key, None)) and is_a_property(prop): - value = getattr(self, key) - - elif options := getattr(df, "options", None): - value = self._evaluate_virtual_field_options(options) + if is_virtual_field and not ignore_virtual: + value = self.get_virtual_field_value(df, ignore_virtual) return value @@ -530,12 +534,7 @@ class BaseDocument: if is_virtual_field: if ignore_virtual or fieldname not in self.permitted_fieldnames: continue - - if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop): - value = getattr(self, fieldname) - - elif options := getattr(df, "options", None): - value = self._evaluate_virtual_field_options(options) + value = self.get_virtual_field_value(df) fieldtype = df.fieldtype if isinstance(value, list) and fieldtype not in table_fields: From 7e9236289290ba120ea86a978028f089d63429dd Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 2 Feb 2026 22:12:01 +0530 Subject: [PATCH 3/5] fix: don't return virtual values before save --- frappe/model/base_document.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index bd8699c0d1..a9abec60a5 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -311,16 +311,7 @@ class BaseDocument: def get_db_value(self, key): return frappe.db.get_value(self.doctype, self.name, key) - def get_virtual_field_value(self, df): - fieldname = df.fieldname - - if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop): - return getattr(self, fieldname) - - elif options := getattr(df, "options", None): - return self._evaluate_virtual_field_options(options) - - def get(self, key, filters=None, limit=None, default=None, ignore_virtual=False): + def get(self, key, filters=None, limit=None, default=None): if isinstance(key, dict): return _filter(self.get_all_children(), key, limit=limit) @@ -336,15 +327,6 @@ class BaseDocument: if limit and isinstance(value, list | tuple) and len(value) > limit: value = value[:limit] - if not value: - df = self.meta.get_field(key) - - is_virtual_field = getattr(df, "is_virtual", False) - ignore_virtual = ignore_virtual or key not in self.permitted_fieldnames - - if is_virtual_field and not ignore_virtual: - value = self.get_virtual_field_value(df, ignore_virtual) - return value def getone(self, key, filters=None): @@ -512,6 +494,15 @@ class BaseDocument: eval_locals={"doc": self}, ) + def get_virtual_field_value(self, df): + fieldname = df.fieldname + + if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop): + return getattr(self, fieldname) + + elif options := getattr(df, "options", None): + return self._evaluate_virtual_field_options(options) + def get_valid_dict( self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False ) -> _dict: From 36d471b98d7e7d56507a5c35e0a088eb888e21b0 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 2 Feb 2026 22:14:13 +0530 Subject: [PATCH 4/5] fix: show title field values in link for virtual fields --- frappe/desk/search.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 0f9c86cf8e..0cf31ad770 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -337,7 +337,12 @@ def build_for_autosuggest(res: list[tuple], doctype: str) -> list[LinkSearchResu for item in res: item = list(item) if len(item) == 1: - item = [item[0], item[0]] + title_field = meta.title_field + docfield = meta.get_field(title_field) + if docfield and docfield.is_virtual: + doc = frappe.get_doc(meta.name, item[0]) + title_value = doc.get_virtual_field_value(docfield) + item = [item[0], title_value or item[0]] label = _(item[1]) if meta.translated_doctype else item[1] item[1] = item[0] From f1731981a82ca12eea1d927b601857da3cb0e1b2 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 2 Feb 2026 22:16:21 +0530 Subject: [PATCH 5/5] fix: query filters breaking for title virtual fields --- frappe/desk/search.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 0cf31ad770..ebb827f036 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -167,7 +167,9 @@ def search_widget( } search_fields = ["name"] if meta.title_field: - search_fields.append(meta.title_field) + is_virtual_field = getattr(meta.get_field(meta.title_field), "is_virtual", False) + if not is_virtual_field: + search_fields.append(meta.title_field) if meta.search_fields: search_fields.extend(meta.get_search_fields())