fix: correct table_fields references

This commit is contained in:
Sagar Vora 2025-10-01 12:25:00 +05:30
parent d93c177f45
commit 2c9c6c0fd5
11 changed files with 87 additions and 54 deletions

View file

@ -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):

View file

@ -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)
)

View file

@ -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

View file

@ -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):

View file

@ -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)

View file

@ -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)

View file

@ -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(

View file

@ -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;

View file

@ -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;
});
},

View file

@ -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

View file

@ -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