perf: idempotent bulk update for custom fields on migrate
Replace per-field delete+create loop (which forced ALTER TABLE on all 222 custom fields every migrate) with idempotent bulk update that only writes when a field's definition actually changed. Saves with ignore_version=True to bypass the Frappe Version diff bug that crashes on certain int values. Also remove the top-level create_custom_fields() call so the function only runs from after_migrate, not on every module import. Optional JEY_ERP_FORCE_RECREATE_FIELDS=1 env var preserves the old delete+create path for development. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
511e91cbf1
commit
3aa4e61a1d
|
|
@ -1,5 +1,64 @@
|
|||
import os
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.custom.doctype.custom_field.custom_field import (
|
||||
create_custom_field,
|
||||
get_existing_custom_fields,
|
||||
)
|
||||
|
||||
|
||||
def _apply_custom_fields(custom_fields: dict, ignore_validate: bool = False) -> None:
|
||||
"""Idempotent bulk create/update for Custom Fields.
|
||||
|
||||
Reimplements frappe.custom...create_custom_fields with two differences:
|
||||
- saves with ignore_version=True to skip Frappe's version diff (which has
|
||||
a bug formatting some int values for Custom Field), and
|
||||
- calls frappe.db.updatedb() once per affected doctype, only when needed.
|
||||
"""
|
||||
try:
|
||||
frappe.flags.in_create_custom_fields = True
|
||||
existing = get_existing_custom_fields(custom_fields)
|
||||
doctypes_to_update = set()
|
||||
|
||||
for doctype, fields in custom_fields.items():
|
||||
updated = False
|
||||
for df in fields:
|
||||
key = (doctype, df["fieldname"])
|
||||
field = existing.get(key)
|
||||
|
||||
if not field:
|
||||
try:
|
||||
df = df.copy()
|
||||
df["owner"] = "Administrator"
|
||||
cf = create_custom_field(doctype, df, ignore_validate=ignore_validate)
|
||||
if cf:
|
||||
existing[key] = cf.__dict__
|
||||
updated = True
|
||||
except frappe.exceptions.DuplicateEntryError:
|
||||
pass
|
||||
continue
|
||||
|
||||
cf = frappe.get_doc({"doctype": "Custom Field", **field})
|
||||
original = cf.__dict__.copy()
|
||||
cf.update(df)
|
||||
if original == cf.__dict__:
|
||||
continue
|
||||
|
||||
cf.flags.ignore_validate = ignore_validate
|
||||
# ignore_version must be passed as kwarg: _save() overwrites
|
||||
# self.flags.ignore_version from this argument on every call.
|
||||
cf.save(ignore_version=True)
|
||||
existing[key] = cf.__dict__
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
doctypes_to_update.add(doctype)
|
||||
|
||||
for doctype in doctypes_to_update:
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
frappe.db.updatedb(doctype)
|
||||
finally:
|
||||
frappe.flags.in_create_custom_fields = False
|
||||
|
||||
def create_custom_fields():
|
||||
custom_fields = {
|
||||
|
|
@ -1860,29 +1919,42 @@ def create_custom_fields():
|
|||
except Exception as e:
|
||||
print(f"Failed to delete legacy Cyrillic field: {e}")
|
||||
|
||||
# Filter out malformed entries (no fieldname) before passing to Frappe
|
||||
sanitized = {}
|
||||
for doctype, fields in custom_fields.items():
|
||||
clean = []
|
||||
for field in fields:
|
||||
# Проверяем наличие fieldname перед обработкой
|
||||
if 'fieldname' not in field:
|
||||
print(f"Skipping field without fieldname in {doctype}: {field}")
|
||||
continue
|
||||
|
||||
# Удаление существующего поля, если оно уже есть
|
||||
if frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": field['fieldname']}):
|
||||
try:
|
||||
custom_field_name = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": field['fieldname']}, "name")
|
||||
frappe.delete_doc("Custom Field", custom_field_name)
|
||||
frappe.db.commit()
|
||||
print(f"Deleted field {field['fieldname']} in {doctype}")
|
||||
except Exception as e:
|
||||
print(f"Failed to delete field {field['fieldname']} in {doctype}: {e}")
|
||||
clean.append(field)
|
||||
if clean:
|
||||
sanitized[doctype] = clean
|
||||
|
||||
# Создание нового поля
|
||||
try:
|
||||
create_custom_field(doctype, field)
|
||||
print(f"Created field {field['fieldname']} in {doctype}")
|
||||
except Exception as e:
|
||||
print(f"Failed to create field {field['fieldname']} in {doctype}: {e}")
|
||||
# Force-recreate mode for development: drop existing fields so Frappe
|
||||
# creates them from scratch. Slow — only use when bulk update is not
|
||||
# picking up something. Trigger via: JEY_ERP_FORCE_RECREATE_FIELDS=1 bench migrate
|
||||
if os.environ.get("JEY_ERP_FORCE_RECREATE_FIELDS"):
|
||||
for doctype, fields in sanitized.items():
|
||||
for field in fields:
|
||||
cf_name = frappe.db.get_value(
|
||||
"Custom Field",
|
||||
{"dt": doctype, "fieldname": field['fieldname']},
|
||||
"name",
|
||||
)
|
||||
if cf_name:
|
||||
try:
|
||||
frappe.delete_doc("Custom Field", cf_name)
|
||||
except Exception as e:
|
||||
print(f"Failed to delete field {field['fieldname']} in {doctype}: {e}")
|
||||
frappe.db.commit()
|
||||
|
||||
# Idempotent bulk create/update. Mirrors frappe.custom...create_custom_fields
|
||||
# but saves with ignore_version=True. Why: Custom Field has track_changes=1,
|
||||
# and Frappe's diff formatter (version.get_diff → format_value) crashes on
|
||||
# certain int values with "expected string or bytes-like object, got 'int'".
|
||||
# We don't need version history for system-managed fields anyway.
|
||||
_apply_custom_fields(sanitized, ignore_validate=True)
|
||||
|
||||
# === Sub-tab wiring for Salary Slip via custom_subtabs ===
|
||||
# The custom_subtabs app reads `js_parent_subtab` on Tab Break fields in meta.
|
||||
|
|
@ -1929,6 +2001,4 @@ def create_custom_fields():
|
|||
|
||||
# Коммит всех изменений
|
||||
frappe.db.commit()
|
||||
print("Setting up VAT calculation hooks...")
|
||||
|
||||
create_custom_fields()
|
||||
print("Setting up VAT calculation hooks...")
|
||||
Loading…
Reference in New Issue