diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index af8f63f071..31bef96f92 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -447,7 +447,7 @@ def get_title_values_for_table_and_multiselect_fields(doc, table_fields=None): if not table_fields: meta = frappe.get_meta(doc.doctype) - table_fields = meta.get_table_fields() + table_fields = meta.get_table_fields(ignore_virtual=False) for field in table_fields: if not doc.get(field.fieldname): diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index dbaf83a134..d9cf3989cc 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -61,14 +61,6 @@ TABLE_DOCTYPES_FOR_CHILD_TABLES = MappingProxyType({}) DOCTYPES_FOR_DOCTYPE = {"DocType", *TABLE_DOCTYPES_FOR_DOCTYPE.values()} -UNPICKLABLE_KEYS = ( - "meta", - "permitted_fieldnames", - "_parent_doc", - "_weakref", - "_table_fieldnames", -) - def _reduce_extended_instance(doc): """Make extended class instances pickle-able. @@ -458,6 +450,7 @@ class BaseDocument: controller = get_controller(doctype) child = controller.__new__(controller) child._table_fieldnames = TABLE_DOCTYPES_FOR_CHILD_TABLES + child._non_virtual_table_fieldnames = TABLE_DOCTYPES_FOR_CHILD_TABLES child.__init__(value) __dict = child.__dict__ @@ -484,7 +477,14 @@ class BaseDocument: return self.meta._table_doctypes - def _get_table_fields(self): + @cached_property + def _non_virtual_table_fieldnames(self) -> dict: + if self.doctype in DOCTYPES_FOR_DOCTYPE: + return self._table_fieldnames + + return self.meta._non_virtual_table_doctypes + + def _get_table_fields(self, ignore_virtual=True): """ To get table fields during Document init Meta.get_table_fields goes into recursion for special doctypes @@ -497,7 +497,7 @@ class BaseDocument: if self.doctype in DOCTYPES_FOR_DOCTYPE: return () - return self.meta.get_table_fields() + return self.meta.get_table_fields(ignore_virtual=ignore_virtual) def _evaluate_virtual_field_options(self, options): from frappe.utils.safe_exec import get_safe_globals @@ -581,12 +581,12 @@ class BaseDocument: without worrying about whether or not they have values """ - if not self._table_fieldnames: + if not self._non_virtual_table_fieldnames: return __dict = self.__dict__ - for fieldname in self._table_fieldnames: + for fieldname in self._non_virtual_table_fieldnames: if __dict.get(fieldname) is None: __dict[fieldname] = [] @@ -645,11 +645,16 @@ class BaseDocument: convert_dates_to_str=False, no_child_table_fields=False, no_private_properties=False, + *, + ignore_virtual_child_tables=False, ) -> dict: doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str, ignore_nulls=no_nulls) doc["doctype"] = self.doctype - for fieldname in self._table_fieldnames: + table_fieldnames = ( + self._non_virtual_table_fieldnames if ignore_virtual_child_tables else self._table_fieldnames + ) + for fieldname in table_fieldnames: children = getattr(self, fieldname) or [] doc[fieldname] = [ d.as_dict( @@ -806,7 +811,7 @@ class BaseDocument: """Raw update parent + children DOES NOT VALIDATE AND CALL TRIGGERS""" self.db_update() - for fieldname in self._table_fieldnames: + for fieldname in self._non_virtual_table_fieldnames: for doc in self.get(fieldname): doc.db_update() @@ -1524,3 +1529,8 @@ def _filter(data, filters, limit=None): break return out + + +UNPICKLABLE_KEYS = frozenset( + prop for prop, value in vars(BaseDocument).items() if isinstance(value, cached_property) +) diff --git a/frappe/model/document.py b/frappe/model/document.py index 76c6d8d1a2..4991dd4a6b 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -276,6 +276,8 @@ 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): + # Remove cache so that the virtual field loads again + self.__dict__.pop(fieldname, None) continue if is_doctype: @@ -876,7 +878,7 @@ class Document(BaseDocument): return all_fields = self.meta.fields.copy() - for table_field in self.meta.get_table_fields(): + for table_field in self.meta.get_table_fields(ignore_virtual=False): all_fields += frappe.get_meta(table_field.options).fields or [] if all(df.permlevel == 0 for df in all_fields): @@ -892,7 +894,7 @@ class Document(BaseDocument): # hasattr might return True for class attribute which can't be delattr-ed. continue - for table_field in self.meta.get_table_fields(): + for table_field in self.meta.get_table_fields(ignore_virtual=False): for df in frappe.get_meta(table_field.options).fields or []: if df.permlevel and df.permlevel not in has_access_to: for child in self.get(table_field.fieldname) or []: @@ -1105,12 +1107,13 @@ class Document(BaseDocument): msg = ", ".join(each[2] for each in cancelled_links) frappe.throw(_("Cannot link cancelled document: {0}").format(msg), frappe.CancelledLinkError) - def get_all_children(self, parenttype=None) -> list["Document"]: + def get_all_children(self, parenttype=None, *, ignore_virtual=True) -> list["Document"]: """Return all children documents from **Table** type fields in a list.""" children = [] + table_fieldnames = self._non_virtual_table_fieldnames if ignore_virtual else self._table_fieldnames - for fieldname, child_doctype in self._table_fieldnames.items(): + for fieldname, child_doctype in table_fieldnames.items(): if parenttype and child_doctype != parenttype: continue @@ -1310,7 +1313,7 @@ class Document(BaseDocument): return frappe.clear_last_message() - for fieldname in self._table_fieldnames: + for fieldname in self._non_virtual_table_fieldnames: for row in self.get(fieldname): row._doc_before_save = next( (d for d in (self._doc_before_save.get(fieldname) or []) if d.name == row.name), None @@ -1972,7 +1975,7 @@ def get_lazy_controller(doctype): # Dynamically construct a class that subclasses LazyDocument and original controller. lazy_controller = type(f"Lazy{original_controller.__name__}", (LazyDocument, original_controller), {}) - for df in meta.get_table_fields(include_virtual=False): + for df in meta.get_table_fields(): setattr(lazy_controller, df.fieldname, LazyChildTable(df.fieldname, df.options)) lazy_controllers[doctype] = lazy_controller diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 4c7a284ada..1fd4012973 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -221,8 +221,8 @@ class Meta(Document): return set_only_once_fields - def get_table_fields(self, include_virtual=True): - return self._table_fields if include_virtual else self._non_virtual_table_fields + def get_table_fields(self, ignore_virtual=True): + return self._non_virtual_table_fields if ignore_virtual else self._table_fields def get_global_search_fields(self): """Return list of fields with `in_global_search` set and `name` if set.""" @@ -482,23 +482,40 @@ class Meta(Document): if get_datetime(recent_change) > add_to_date(None, days=-1 * LARGE_TABLE_RECENCY_THRESHOLD): self.is_large_table = True - def init_field_caches(self): - # field map - self._fields = {field.fieldname: field for field in self.fields} + @cached_property + def _fields(self): + return {field.fieldname: field for field in self.fields} - # table fields + @cached_property + def _table_fields(self): if self.name == "DocType": - self._table_fields = DOCTYPE_TABLE_FIELDS - else: - self._table_fields = self.get("fields", {"fieldtype": ["in", table_fields]}) - self._non_virtual_table_fields = ( - [] - if self.get("is_virtual") - else self.get("fields", {"fieldtype": ["in", table_fields], "is_virtual": 0}) - ) + return DOCTYPE_TABLE_FIELDS + return self.get("fields", {"fieldtype": ["in", table_fields]}) - # table fieldname: doctype map - self._table_doctypes = {field.fieldname: field.options for field in self._table_fields} + @cached_property + def _non_virtual_table_fields(self): + if self.name == "DocType": + return self._table_fields + + if self.get("is_virtual"): + return [] + + return self.get("fields", {"fieldtype": ["in", table_fields], "is_virtual": 0}) + + @cached_property + def _table_doctypes(self): + return {field.fieldname: field.options for field in self._table_fields} + + @cached_property + def _non_virtual_table_doctypes(self): + return {field.fieldname: field.options for field in self._non_virtual_table_fields} + + def init_field_caches(self): + self._fields + self._table_fields + self._non_virtual_table_fields + self._table_doctypes + self._non_virtual_table_doctypes def sort_fields(self): """ @@ -961,14 +978,7 @@ def _update_field_order_based_on_insert_after(field_order, insert_after_map): field_order.extend(fields) -CACHE_PROPERTIES = frozenset( - ( - "_fields", - "_table_fields", - "_table_doctypes", - *(prop for prop, value in vars(Meta).items() if isinstance(value, cached_property)), - ) -) +CACHE_PROPERTIES = frozenset(prop for prop, value in vars(Meta).items() if isinstance(value, cached_property)) def _serialize(doc, no_nulls=False, *, is_child=False): diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index a75552d0a3..ec0c1116f3 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -416,7 +416,7 @@ def rename_doctype(doctype: str, old: str, new: str) -> None: def update_child_docs(old: str, new: str, meta: "Meta") -> None: # update "parent" - for df in meta.get_table_fields(include_virtual=False): + for df in meta.get_table_fields(): ( frappe.qb.update(df.options) .set("parent", new) diff --git a/frappe/modules/export_file.py b/frappe/modules/export_file.py index 8686f68cc9..2cf19094d3 100644 --- a/frappe/modules/export_file.py +++ b/frappe/modules/export_file.py @@ -33,7 +33,7 @@ def export_to_files(record_list=None, record_module=None, verbose=0, create_init def write_document_file(doc, record_module=None, create_init=True, folder_name=None): - doc_export = doc.as_dict(no_nulls=True) + doc_export = doc.as_dict(no_nulls=True, ignore_virtual_child_tables=True) doc.run_method("before_export", doc_export) doc_export = strip_default_fields(doc, doc_export) diff --git a/frappe/permissions.py b/frappe/permissions.py index d1ff3933de..d00d556fa8 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -802,7 +802,9 @@ def has_child_permission( if parent_meta.istable or not ( valid_parentfields := [ - df.fieldname for df in parent_meta.get_table_fields() if df.options == child_doctype + df.fieldname + for df in parent_meta.get_table_fields(ignore_virtual=False) + if df.options == child_doctype ] ): push_perm_check_log( diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 8443d535d3..e649857fe8 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1866,7 +1866,7 @@ frappe.ui.form.Form = class FrappeForm { // returns list of children that are selected. returns [parentfield, name] for each var selected = {}, me = this; - frappe.meta.get_table_fields(this.doctype).forEach(function (df) { + frappe.meta.get_table_fields(this.doctype, false).forEach(function (df) { // handle TableMultiselect child fields let _selected = []; @@ -1887,7 +1887,7 @@ frappe.ui.form.Form = class FrappeForm { if (frappe.meta.docfield_map[this.doctype][fieldname]) { doctype = this.doctype; } else { - frappe.meta.get_table_fields(this.doctype).every(function (df) { + frappe.meta.get_table_fields(this.doctype, false).every(function (df) { if (frappe.meta.docfield_map[df.options][fieldname]) { doctype = df.options; return false; diff --git a/frappe/public/js/frappe/model/meta.js b/frappe/public/js/frappe/model/meta.js index 07b0f3bb44..08c6c54517 100644 --- a/frappe/public/js/frappe/model/meta.js +++ b/frappe/public/js/frappe/model/meta.js @@ -149,9 +149,17 @@ $.extend(frappe.meta, { return docfield_map && docfield_map[fn]; }, - get_table_fields: function (dt) { + get_table_fields: function (dt, ignore_virtual = true) { return $.map(frappe.meta.docfield_list[dt], function (d) { - return frappe.model.table_fields.includes(d.fieldtype) ? d : null; + if (!frappe.model.table_fields.includes(d.fieldtype)) { + return null; + } + + if (ignore_virtual && d.is_virtual) { + return null; + } + + return d; }); }, diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 5ff2105214..999c41d166 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -2150,7 +2150,7 @@ def get_filter(doctype: str, filters: FilterSignature, filters_config=None) -> " meta = frappe.get_meta(f.doctype) if not meta.has_field(f.fieldname): # try and match the doctype name from child tables - for df in meta.get_table_fields(): + for df in meta.get_table_fields(ignore_virtual=False): if frappe.get_meta(df.options).has_field(f.fieldname): f.doctype = df.options break diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 32bfd013ed..0c275f34e8 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -308,7 +308,7 @@ def set_title_values_for_link_and_dynamic_link_fields( def set_title_values_for_table_and_multiselect_fields(meta: "Meta", doc: "Document") -> None: - for field in meta.get_table_fields(): + for field in meta.get_table_fields(ignore_virtual=False): if not doc.get(field.fieldname): continue