2
0
mirror of https://github.com/frappe/frappe.git synced 2024-06-13 06:42:21 +00:00
frappe/frappe/custom/doctype/custom_field/custom_field.py
Ankush Menat d0ba31c911
refactor!: Prefix all custom fieldnames created from Desk (#21355)
* fix: actualy remove special characterso

`or "_"` is always truthy, this code didn't do anything

* refactor!: Prefix all custom fieldnames created from Desk

 Why?
 - Custom and standard field clashes are frequent. This will prevent it
   from happening. E.g. recent `incoterm` field addition in ERPNext.
 - Apps/fixtures can still specify exact fieldnames

* test: custom field naming
2023-06-19 18:22:36 +05:30

242 lines
7.0 KiB
Python

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import json
import frappe
from frappe import _
from frappe.model import core_doctypes_list
from frappe.model.docfield import supports_translation
from frappe.model.document import Document
from frappe.query_builder.functions import IfNull
from frappe.utils import cstr, random_string
class CustomField(Document):
def autoname(self):
self.set_fieldname()
self.name = self.dt + "-" + self.fieldname
def set_fieldname(self):
restricted = (
"name",
"parent",
"creation",
"modified",
"modified_by",
"parentfield",
"parenttype",
"file_list",
"flags",
"docstatus",
)
if not self.fieldname:
label = self.label
if not label:
if self.fieldtype in ["Section Break", "Column Break", "Tab Break"]:
label = self.fieldtype + "_" + str(random_string(5))
else:
frappe.throw(_("Label is mandatory"))
# remove special characters from fieldname
self.fieldname = "".join(
[c for c in cstr(label).replace(" ", "_") if c.isdigit() or c.isalpha() or c == "_"]
)
self.fieldname = f"custom_{self.fieldname}"
# fieldnames should be lowercase
self.fieldname = self.fieldname.lower()
if self.fieldname in restricted:
self.fieldname = self.fieldname + "1"
def before_insert(self):
self.set_fieldname()
def validate(self):
# these imports have been added to avoid cyclical import, should fix in future
from frappe.core.doctype.doctype.doctype import check_fieldname_conflicts
from frappe.custom.doctype.customize_form.customize_form import CustomizeForm
# don't always get meta to improve performance
# setting idx is just an improvement, not a requirement
if self.is_new() or self.insert_after == "append":
meta = frappe.get_meta(self.dt, cached=False)
fieldnames = [df.fieldname for df in meta.get("fields")]
if self.is_new() and self.fieldname in fieldnames:
frappe.throw(
_("A field with the name {0} already exists in {1}").format(
frappe.bold(self.fieldname), self.dt
)
)
if self.insert_after == "append":
self.insert_after = fieldnames[-1]
if self.insert_after and self.insert_after in fieldnames:
self.idx = fieldnames.index(self.insert_after) + 1
if (
not self.is_virtual
and (doc_before_save := self.get_doc_before_save())
and (old_fieldtype := doc_before_save.fieldtype) != self.fieldtype
and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype)
):
frappe.throw(
_("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype)
)
if not self.fieldname:
frappe.throw(_("Fieldname not set for Custom Field"))
if self.get("translatable", 0) and not supports_translation(self.fieldtype):
self.translatable = 0
check_fieldname_conflicts(self)
def on_update(self):
# validate field
if not self.flags.ignore_validate:
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
validate_fields_for_doctype(self.dt)
# clear cache and update the schema
if not frappe.flags.in_create_custom_fields:
frappe.clear_cache(doctype=self.dt)
frappe.db.updatedb(self.dt)
def on_trash(self):
# check if Admin owned field
if self.owner == "Administrator" and frappe.session.user != "Administrator":
frappe.throw(
_(
"Custom Field {0} is created by the Administrator and can only be deleted through the Administrator account."
).format(frappe.bold(self.label))
)
# delete property setter entries
frappe.db.delete("Property Setter", {"doc_type": self.dt, "field_name": self.fieldname})
# update doctype layouts
doctype_layouts = frappe.get_all(
"DocType Layout", filters={"document_type": self.dt}, pluck="name"
)
for layout in doctype_layouts:
layout_doc = frappe.get_doc("DocType Layout", layout)
for field in layout_doc.fields:
if field.fieldname == self.fieldname:
layout_doc.remove(field)
layout_doc.save()
break
frappe.clear_cache(doctype=self.dt)
def validate_insert_after(self, meta):
if not meta.get_field(self.insert_after):
frappe.throw(
_(
"Insert After field '{0}' mentioned in Custom Field '{1}', with label '{2}', does not exist"
).format(self.insert_after, self.name, self.label),
frappe.DoesNotExistError,
)
if self.fieldname == self.insert_after:
frappe.throw(_("Insert After cannot be set as {0}").format(meta.get_label(self.insert_after)))
@frappe.whitelist()
def get_fields_label(doctype=None):
meta = frappe.get_meta(doctype)
if doctype in core_doctypes_list:
return frappe.msgprint(_("Custom Fields cannot be added to core DocTypes."))
if meta.custom:
return frappe.msgprint(_("Custom Fields can only be added to a standard DocType."))
return [
{"value": df.fieldname or "", "label": _(df.label) if df.label else ""}
for df in frappe.get_meta(doctype).get("fields")
]
def create_custom_field_if_values_exist(doctype, df):
df = frappe._dict(df)
if df.fieldname in frappe.db.get_table_columns(doctype) and frappe.db.count(
dt=doctype, filters=IfNull(df.fieldname, "") != ""
):
create_custom_field(doctype, df)
def create_custom_field(doctype, df, ignore_validate=False, is_system_generated=True):
df = frappe._dict(df)
if not df.fieldname and df.label:
df.fieldname = frappe.scrub(df.label)
if not frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": df.fieldname}):
custom_field = frappe.get_doc(
{
"doctype": "Custom Field",
"dt": doctype,
"permlevel": 0,
"fieldtype": "Data",
"hidden": 0,
"is_system_generated": is_system_generated,
}
)
custom_field.update(df)
custom_field.flags.ignore_validate = ignore_validate
custom_field.insert()
return custom_field
def create_custom_fields(custom_fields, ignore_validate=False, update=True):
"""Add / update multiple custom fields
:param custom_fields: example `{'Sales Invoice': [dict(fieldname='test')]}`"""
try:
frappe.flags.in_create_custom_fields = True
doctypes_to_update = set()
if frappe.flags.in_setup_wizard:
ignore_validate = True
for doctypes, fields in custom_fields.items():
if isinstance(fields, dict):
# only one field
fields = [fields]
if isinstance(doctypes, str):
# only one doctype
doctypes = (doctypes,)
for doctype in doctypes:
doctypes_to_update.add(doctype)
for df in fields:
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": df["fieldname"]})
if not field:
try:
df = df.copy()
df["owner"] = "Administrator"
create_custom_field(doctype, df, ignore_validate=ignore_validate)
except frappe.exceptions.DuplicateEntryError:
pass
elif update:
custom_field = frappe.get_doc("Custom Field", field)
custom_field.flags.ignore_validate = ignore_validate
custom_field.update(df)
custom_field.save()
for doctype in doctypes_to_update:
frappe.clear_cache(doctype=doctype)
frappe.db.updatedb(doctype)
finally:
frappe.flags.in_create_custom_fields = False