fix: more foolproof implementation for virtal ct

This commit is contained in:
Sagar Vora 2025-09-10 12:32:12 +05:30
parent 76c04fef27
commit dddfe7a02f
3 changed files with 51 additions and 22 deletions

View file

@ -145,7 +145,44 @@ def import_controller(doctype):
if not issubclass(class_, BaseDocument):
raise ImportError(f"{doctype}: {classname} is not a subclass of BaseDocument")
return _get_extended_class(class_, doctype)
class_ = _get_extended_class(class_, doctype)
return _update_virtual_ct_props(class_, doctype)
def _update_virtual_ct_props(class_, doctype):
if doctype in DOCTYPES_FOR_DOCTYPE or getattr(class_, "_virtual_ct_props_updated", False):
return class_
meta = frappe.get_meta(doctype)
for df in meta.get_table_fields():
if df.is_virtual:
_update_virtual_ct_prop(class_, df)
class_._virtual_ct_props_updated = True
return class_
def _update_virtual_ct_prop(class_, df):
fieldname = df.fieldname
original_prop = getattr(class_, fieldname, None)
def virtual_ct_prop(self):
if original_prop and is_a_property(original_prop):
value = original_prop.__get__(self, type(self))
elif options := getattr(df, "options", None):
value = self._evaluate_virtual_field_options(options)
else:
# no property or options found
# to compare, default value is None for non-child table virtual fields
value = []
# converting to document objects + caching
self.set(fieldname, value)
return self.__dict__[fieldname]
setattr(class_, fieldname, property(virtual_ct_prop))
def _get_extended_class(base_class, doctype):
@ -462,6 +499,15 @@ class BaseDocument:
return self.meta.get_table_fields()
def _evaluate_virtual_field_options(self, options):
from frappe.utils.safe_exec import get_safe_globals
return frappe.safe_eval(
code=options,
eval_globals=get_safe_globals(),
eval_locals={"doc": self},
)
def get_valid_dict(
self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False
) -> _dict:
@ -489,13 +535,7 @@ class BaseDocument:
value = getattr(self, fieldname)
elif options := getattr(df, "options", None):
from frappe.utils.safe_exec import get_safe_globals
value = frappe.safe_eval(
code=options,
eval_globals=get_safe_globals(),
eval_locals={"doc": self},
)
value = self._evaluate_virtual_field_options(options)
fieldtype = df.fieldtype
if isinstance(value, list) and fieldtype not in table_fields:
@ -610,7 +650,7 @@ class BaseDocument:
doc["doctype"] = self.doctype
for fieldname in self._table_fieldnames:
children = self.get(fieldname) or []
children = getattr(self, fieldname) or []
doc[fieldname] = [
d.as_dict(
convert_dates_to_str=convert_dates_to_str,

View file

@ -275,18 +275,7 @@ class Document(BaseDocument):
for fieldname, child_doctype in self._table_fieldnames.items():
# Make sure not to query the DB for a child table, if it is a virtual one.
if (
not is_doctype
and is_virtual_doctype(child_doctype)
and self.meta.get_field(fieldname).is_virtual
):
# Users must specify non-data descriptor/cached_property for computed table
# Remove previous value if any, required for reload.
self.__dict__.pop(fieldname, None)
values = getattr(self, fieldname, [])
# Convert to documents
self.__dict__[fieldname] = []
self.extend(fieldname, values)
if not is_doctype and is_virtual_doctype(child_doctype):
continue
if is_doctype:

View file

@ -493,7 +493,7 @@ class Meta(Document):
self._table_fields = self.get("fields", {"fieldtype": ["in", table_fields]})
self._non_virtual_table_fields = (
[]
if self.is_virtual
if self.get("is_virtual")
else self.get("fields", {"fieldtype": ["in", table_fields], "is_virtual": 0})
)