The license.txt file has been replaced with LICENSE for quite a while now. INAL but it didn't seem accurate to say "hey, checkout license.txt although there's no such file". Apart from this, there were inconsistencies in the headers altogether...this change brings consistency.
521 lines
17 KiB
Python
521 lines
17 KiB
Python
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# License: MIT. See LICENSE
|
|
import frappe
|
|
from frappe import _, bold
|
|
from frappe.model.dynamic_links import get_dynamic_link_map
|
|
from frappe.model.naming import validate_name
|
|
from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data
|
|
from frappe.utils import cint
|
|
from frappe.utils.password import rename_password
|
|
|
|
|
|
@frappe.whitelist()
|
|
def update_document_title(doctype, docname, title_field=None, old_title=None, new_title=None, new_name=None, merge=False):
|
|
"""
|
|
Update title from header in form view
|
|
"""
|
|
if docname and new_name and not docname == new_name:
|
|
docname = rename_doc(doctype=doctype, old=docname, new=new_name, merge=merge)
|
|
|
|
if old_title and new_title and not old_title == new_title:
|
|
try:
|
|
frappe.db.set_value(doctype, docname, title_field, new_title)
|
|
frappe.msgprint(_('Saved'), alert=True, indicator='green')
|
|
except Exception as e:
|
|
if frappe.db.is_duplicate_entry(e):
|
|
frappe.throw(
|
|
_("{0} {1} already exists").format(doctype, frappe.bold(docname)),
|
|
title=_("Duplicate Name"),
|
|
exc=frappe.DuplicateEntryError
|
|
)
|
|
|
|
return docname
|
|
|
|
def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=False, ignore_if_exists=False, show_alert=True):
|
|
"""
|
|
Renames a doc(dt, old) to doc(dt, new) and
|
|
updates all linked fields of type "Link"
|
|
"""
|
|
if not frappe.db.exists(doctype, old):
|
|
return
|
|
|
|
if ignore_if_exists and frappe.db.exists(doctype, new):
|
|
return
|
|
|
|
if old==new:
|
|
frappe.msgprint(_('Please select a new name to rename'))
|
|
return
|
|
|
|
force = cint(force)
|
|
merge = cint(merge)
|
|
meta = frappe.get_meta(doctype)
|
|
|
|
# call before_rename
|
|
old_doc = frappe.get_doc(doctype, old)
|
|
out = old_doc.run_method("before_rename", old, new, merge) or {}
|
|
new = (out.get("new") or new) if isinstance(out, dict) else (out or new)
|
|
new = validate_rename(doctype, new, meta, merge, force, ignore_permissions)
|
|
|
|
if not merge:
|
|
rename_parent_and_child(doctype, old, new, meta)
|
|
else:
|
|
update_assignments(old, new, doctype)
|
|
|
|
# update link fields' values
|
|
link_fields = get_link_fields(doctype)
|
|
update_link_field_values(link_fields, old, new, doctype)
|
|
|
|
rename_dynamic_links(doctype, old, new)
|
|
|
|
# save the user settings in the db
|
|
update_user_settings(old, new, link_fields)
|
|
|
|
if doctype=='DocType':
|
|
rename_doctype(doctype, old, new, force)
|
|
|
|
update_attachments(doctype, old, new)
|
|
|
|
rename_versions(doctype, old, new)
|
|
|
|
# call after_rename
|
|
new_doc = frappe.get_doc(doctype, new)
|
|
|
|
# copy any flags if required
|
|
new_doc._local = getattr(old_doc, "_local", None)
|
|
|
|
new_doc.run_method("after_rename", old, new, merge)
|
|
|
|
if not merge:
|
|
rename_password(doctype, old, new)
|
|
|
|
# update user_permissions
|
|
frappe.db.sql("""UPDATE `tabDefaultValue` SET `defvalue`=%s WHERE `parenttype`='User Permission'
|
|
AND `defkey`=%s AND `defvalue`=%s""", (new, doctype, old))
|
|
|
|
if merge:
|
|
new_doc.add_comment('Edit', _("merged {0} into {1}").format(frappe.bold(old), frappe.bold(new)))
|
|
else:
|
|
new_doc.add_comment('Edit', _("renamed from {0} to {1}").format(frappe.bold(old), frappe.bold(new)))
|
|
|
|
if merge:
|
|
frappe.delete_doc(doctype, old)
|
|
|
|
frappe.clear_cache()
|
|
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype)
|
|
|
|
if show_alert:
|
|
frappe.msgprint(_('Document renamed from {0} to {1}').format(bold(old), bold(new)), alert=True, indicator='green')
|
|
|
|
return new
|
|
|
|
def update_assignments(old, new, doctype):
|
|
old_assignments = frappe.parse_json(frappe.db.get_value(doctype, old, '_assign')) or []
|
|
new_assignments = frappe.parse_json(frappe.db.get_value(doctype, new, '_assign')) or []
|
|
common_assignments = list(set(old_assignments).intersection(new_assignments))
|
|
|
|
for user in common_assignments:
|
|
# delete todos linked to old doc
|
|
todos = frappe.db.get_all('ToDo',
|
|
{
|
|
'owner': user,
|
|
'reference_type': doctype,
|
|
'reference_name': old,
|
|
},
|
|
['name', 'description']
|
|
)
|
|
|
|
for todo in todos:
|
|
frappe.delete_doc('ToDo', todo.name)
|
|
|
|
unique_assignments = list(set(old_assignments + new_assignments))
|
|
frappe.db.set_value(doctype, new, '_assign', frappe.as_json(unique_assignments, indent=0))
|
|
|
|
def update_user_settings(old, new, link_fields):
|
|
'''
|
|
Update the user settings of all the linked doctypes while renaming.
|
|
'''
|
|
|
|
# store the user settings data from the redis to db
|
|
sync_user_settings()
|
|
|
|
if not link_fields: return
|
|
|
|
# find the user settings for the linked doctypes
|
|
linked_doctypes = {d.parent for d in link_fields if not d.issingle}
|
|
user_settings_details = frappe.db.sql('''SELECT `user`, `doctype`, `data`
|
|
FROM `__UserSettings`
|
|
WHERE `data` like %s
|
|
AND `doctype` IN ('{doctypes}')'''.format(doctypes="', '".join(linked_doctypes)), (old), as_dict=1)
|
|
|
|
# create the dict using the doctype name as key and values as list of the user settings
|
|
from collections import defaultdict
|
|
user_settings_dict = defaultdict(list)
|
|
for user_setting in user_settings_details:
|
|
user_settings_dict[user_setting.doctype].append(user_setting)
|
|
|
|
# update the name in linked doctype whose user settings exists
|
|
for fields in link_fields:
|
|
user_settings = user_settings_dict.get(fields.parent)
|
|
if user_settings:
|
|
for user_setting in user_settings:
|
|
update_user_settings_data(user_setting, "value", old, new, "docfield", fields.fieldname)
|
|
else:
|
|
continue
|
|
|
|
|
|
def update_attachments(doctype, old, new):
|
|
try:
|
|
if old != "File Data" and doctype != "DocType":
|
|
frappe.db.sql("""update `tabFile` set attached_to_name=%s
|
|
where attached_to_name=%s and attached_to_doctype=%s""", (new, old, doctype))
|
|
except frappe.db.ProgrammingError as e:
|
|
if not frappe.db.is_column_missing(e):
|
|
raise
|
|
|
|
def rename_versions(doctype, old, new):
|
|
frappe.db.sql("""UPDATE `tabVersion` SET `docname`=%s WHERE `ref_doctype`=%s AND `docname`=%s""",
|
|
(new, doctype, old))
|
|
|
|
def rename_parent_and_child(doctype, old, new, meta):
|
|
# rename the doc
|
|
frappe.db.sql("UPDATE `tab{0}` SET `name`={1} WHERE `name`={1}".format(doctype, '%s'), (new, old))
|
|
update_autoname_field(doctype, new, meta)
|
|
update_child_docs(old, new, meta)
|
|
|
|
def update_autoname_field(doctype, new, meta):
|
|
# update the value of the autoname field on rename of the docname
|
|
if meta.get('autoname'):
|
|
field = meta.get('autoname').split(':')
|
|
if field and field[0] == "field":
|
|
frappe.db.sql("UPDATE `tab{0}` SET `{1}`={2} WHERE `name`={2}".format(doctype, field[1], '%s'), (new, new))
|
|
|
|
def validate_rename(doctype, new, meta, merge, force, ignore_permissions):
|
|
# using for update so that it gets locked and someone else cannot edit it while this rename is going on!
|
|
exists = frappe.db.sql("select name from `tab{doctype}` where name=%s for update".format(doctype=doctype), new)
|
|
exists = exists[0][0] if exists else None
|
|
|
|
if merge and not exists:
|
|
frappe.msgprint(_("{0} {1} does not exist, select a new target to merge").format(doctype, new), raise_exception=1)
|
|
|
|
if exists and exists != new:
|
|
# for fixing case, accents
|
|
exists = None
|
|
|
|
if (not merge) and exists:
|
|
frappe.msgprint(_("Another {0} with name {1} exists, select another name").format(doctype, new), raise_exception=1)
|
|
|
|
if not (ignore_permissions or frappe.permissions.has_permission(doctype, "write", raise_exception=False)):
|
|
frappe.msgprint(_("You need write permission to rename"), raise_exception=1)
|
|
|
|
if not (force or ignore_permissions) and not meta.allow_rename:
|
|
frappe.msgprint(_("{0} not allowed to be renamed").format(_(doctype)), raise_exception=1)
|
|
|
|
# validate naming like it's done in doc.py
|
|
new = validate_name(doctype, new, merge=merge)
|
|
|
|
return new
|
|
|
|
def rename_doctype(doctype, old, new, force=False):
|
|
# change options for fieldtype Table, Table MultiSelect and Link
|
|
fields_with_options = ("Link",) + frappe.model.table_fields
|
|
|
|
for fieldtype in fields_with_options:
|
|
update_options_for_fieldtype(fieldtype, old, new)
|
|
|
|
# change options where select options are hardcoded i.e. listed
|
|
select_fields = get_select_fields(old, new)
|
|
update_link_field_values(select_fields, old, new, doctype)
|
|
update_select_field_values(old, new)
|
|
|
|
# change parenttype for fieldtype Table
|
|
update_parenttype_values(old, new)
|
|
|
|
def update_child_docs(old, new, meta):
|
|
# update "parent"
|
|
for df in meta.get_table_fields():
|
|
frappe.db.sql("update `tab%s` set parent=%s where parent=%s" \
|
|
% (df.options, '%s', '%s'), (new, old))
|
|
|
|
def update_link_field_values(link_fields, old, new, doctype):
|
|
for field in link_fields:
|
|
if field['issingle']:
|
|
try:
|
|
single_doc = frappe.get_doc(field['parent'])
|
|
if single_doc.get(field['fieldname'])==old:
|
|
single_doc.set(field['fieldname'], new)
|
|
# update single docs using ORM rather then query
|
|
# as single docs also sometimes sets defaults!
|
|
single_doc.flags.ignore_mandatory = True
|
|
single_doc.save(ignore_permissions=True)
|
|
except ImportError:
|
|
# fails in patches where the doctype has been renamed
|
|
# or no longer exists
|
|
pass
|
|
else:
|
|
parent = field['parent']
|
|
docfield = field["fieldname"]
|
|
|
|
# Handles the case where one of the link fields belongs to
|
|
# the DocType being renamed.
|
|
# Here this field could have the current DocType as its value too.
|
|
|
|
# In this case while updating link field value, the field's parent
|
|
# or the current DocType table name hasn't been renamed yet,
|
|
# so consider it's old name.
|
|
if parent == new and doctype == "DocType":
|
|
parent = old
|
|
|
|
frappe.db.set_value(parent, {docfield: old}, docfield, new)
|
|
|
|
# update cached link_fields as per new
|
|
if doctype=='DocType' and field['parent'] == old:
|
|
field['parent'] = new
|
|
|
|
def get_link_fields(doctype):
|
|
# get link fields from tabDocField
|
|
if not frappe.flags.link_fields:
|
|
frappe.flags.link_fields = {}
|
|
|
|
if not doctype in frappe.flags.link_fields:
|
|
link_fields = frappe.db.sql("""\
|
|
select parent, fieldname,
|
|
(select issingle from tabDocType dt
|
|
where dt.name = df.parent) as issingle
|
|
from tabDocField df
|
|
where
|
|
df.options=%s and df.fieldtype='Link'""", (doctype,), as_dict=1)
|
|
|
|
# get link fields from tabCustom Field
|
|
custom_link_fields = frappe.db.sql("""\
|
|
select dt as parent, fieldname,
|
|
(select issingle from tabDocType dt
|
|
where dt.name = df.dt) as issingle
|
|
from `tabCustom Field` df
|
|
where
|
|
df.options=%s and df.fieldtype='Link'""", (doctype,), as_dict=1)
|
|
|
|
# add custom link fields list to link fields list
|
|
link_fields += custom_link_fields
|
|
|
|
# remove fields whose options have been changed using property setter
|
|
property_setter_link_fields = frappe.db.sql("""\
|
|
select ps.doc_type as parent, ps.field_name as fieldname,
|
|
(select issingle from tabDocType dt
|
|
where dt.name = ps.doc_type) as issingle
|
|
from `tabProperty Setter` ps
|
|
where
|
|
ps.property_type='options' and
|
|
ps.field_name is not null and
|
|
ps.value=%s""", (doctype,), as_dict=1)
|
|
|
|
link_fields += property_setter_link_fields
|
|
|
|
frappe.flags.link_fields[doctype] = link_fields
|
|
|
|
return frappe.flags.link_fields[doctype]
|
|
|
|
def update_options_for_fieldtype(fieldtype, old, new):
|
|
if frappe.conf.developer_mode:
|
|
for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"):
|
|
doctype = frappe.get_doc("DocType", name)
|
|
save = False
|
|
for f in doctype.fields:
|
|
if f.options == old:
|
|
f.options = new
|
|
save = True
|
|
if save:
|
|
doctype.save()
|
|
else:
|
|
frappe.db.sql("""update `tabDocField` set options=%s
|
|
where fieldtype=%s and options=%s""", (new, fieldtype, old))
|
|
|
|
frappe.db.sql("""update `tabCustom Field` set options=%s
|
|
where fieldtype=%s and options=%s""", (new, fieldtype, old))
|
|
|
|
frappe.db.sql("""update `tabProperty Setter` set value=%s
|
|
where property='options' and value=%s""", (new, old))
|
|
|
|
def get_select_fields(old, new):
|
|
"""
|
|
get select type fields where doctype's name is hardcoded as
|
|
new line separated list
|
|
"""
|
|
# get link fields from tabDocField
|
|
select_fields = frappe.db.sql("""
|
|
select parent, fieldname,
|
|
(select issingle from tabDocType dt
|
|
where dt.name = df.parent) as issingle
|
|
from tabDocField df
|
|
where
|
|
df.parent != %s and df.fieldtype = 'Select' and
|
|
df.options like {0} """.format(frappe.db.escape('%' + old + '%')), (new,), as_dict=1)
|
|
|
|
# get link fields from tabCustom Field
|
|
custom_select_fields = frappe.db.sql("""
|
|
select dt as parent, fieldname,
|
|
(select issingle from tabDocType dt
|
|
where dt.name = df.dt) as issingle
|
|
from `tabCustom Field` df
|
|
where
|
|
df.dt != %s and df.fieldtype = 'Select' and
|
|
df.options like {0} """ .format(frappe.db.escape('%' + old + '%')), (new,), as_dict=1)
|
|
|
|
# add custom link fields list to link fields list
|
|
select_fields += custom_select_fields
|
|
|
|
# remove fields whose options have been changed using property setter
|
|
property_setter_select_fields = frappe.db.sql("""
|
|
select ps.doc_type as parent, ps.field_name as fieldname,
|
|
(select issingle from tabDocType dt
|
|
where dt.name = ps.doc_type) as issingle
|
|
from `tabProperty Setter` ps
|
|
where
|
|
ps.doc_type != %s and
|
|
ps.property_type='options' and
|
|
ps.field_name is not null and
|
|
ps.value like {0} """.format(frappe.db.escape('%' + old + '%')), (new,), as_dict=1)
|
|
|
|
select_fields += property_setter_select_fields
|
|
|
|
return select_fields
|
|
|
|
def update_select_field_values(old, new):
|
|
frappe.db.sql("""
|
|
update `tabDocField` set options=replace(options, %s, %s)
|
|
where
|
|
parent != %s and fieldtype = 'Select' and
|
|
(options like {0} or options like {1})"""
|
|
.format(frappe.db.escape('%' + '\n' + old + '%'), frappe.db.escape('%' + old + '\n' + '%')), (old, new, new))
|
|
|
|
frappe.db.sql("""
|
|
update `tabCustom Field` set options=replace(options, %s, %s)
|
|
where
|
|
dt != %s and fieldtype = 'Select' and
|
|
(options like {0} or options like {1})"""
|
|
.format(frappe.db.escape('%' + '\n' + old + '%'), frappe.db.escape('%' + old + '\n' + '%')), (old, new, new))
|
|
|
|
frappe.db.sql("""
|
|
update `tabProperty Setter` set value=replace(value, %s, %s)
|
|
where
|
|
doc_type != %s and field_name is not null and
|
|
property='options' and
|
|
(value like {0} or value like {1})"""
|
|
.format(frappe.db.escape('%' + '\n' + old + '%'), frappe.db.escape('%' + old + '\n' + '%')), (old, new, new))
|
|
|
|
def update_parenttype_values(old, new):
|
|
child_doctypes = frappe.db.get_all('DocField',
|
|
fields=['options', 'fieldname'],
|
|
filters={
|
|
'parent': new,
|
|
'fieldtype': ['in', frappe.model.table_fields]
|
|
}
|
|
)
|
|
|
|
custom_child_doctypes = frappe.db.get_all('Custom Field',
|
|
fields=['options', 'fieldname'],
|
|
filters={
|
|
'dt': new,
|
|
'fieldtype': ['in', frappe.model.table_fields]
|
|
}
|
|
)
|
|
|
|
child_doctypes += custom_child_doctypes
|
|
fields = [d['fieldname'] for d in child_doctypes]
|
|
|
|
property_setter_child_doctypes = frappe.get_all(
|
|
"Property Setter",
|
|
filters={
|
|
"doc_type": new,
|
|
"property": "options",
|
|
"field_name": ("in", fields)
|
|
},
|
|
pluck="value"
|
|
)
|
|
|
|
child_doctypes = list(d['options'] for d in child_doctypes)
|
|
child_doctypes += property_setter_child_doctypes
|
|
|
|
for doctype in child_doctypes:
|
|
frappe.db.sql(f"update `tab{doctype}` set parenttype=%s where parenttype=%s", (new, old))
|
|
|
|
def rename_dynamic_links(doctype, old, new):
|
|
for df in get_dynamic_link_map().get(doctype, []):
|
|
# dynamic link in single, just one value to check
|
|
if frappe.get_meta(df.parent).issingle:
|
|
refdoc = frappe.db.get_singles_dict(df.parent)
|
|
if refdoc.get(df.options)==doctype and refdoc.get(df.fieldname)==old:
|
|
|
|
frappe.db.sql("""update tabSingles set value=%s where
|
|
field=%s and value=%s and doctype=%s""", (new, df.fieldname, old, df.parent))
|
|
else:
|
|
# because the table hasn't been renamed yet!
|
|
parent = df.parent if df.parent != new else old
|
|
frappe.db.sql("""update `tab{parent}` set {fieldname}=%s
|
|
where {options}=%s and {fieldname}=%s""".format(parent = parent,
|
|
fieldname=df.fieldname, options=df.options), (new, doctype, old))
|
|
|
|
def bulk_rename(doctype, rows=None, via_console = False):
|
|
"""Bulk rename documents
|
|
|
|
:param doctype: DocType to be renamed
|
|
:param rows: list of documents as `((oldname, newname), ..)`"""
|
|
if not rows:
|
|
frappe.throw(_("Please select a valid csv file with data"))
|
|
|
|
if not via_console:
|
|
max_rows = 500
|
|
if len(rows) > max_rows:
|
|
frappe.throw(_("Maximum {0} rows allowed").format(max_rows))
|
|
|
|
rename_log = []
|
|
for row in rows:
|
|
# if row has some content
|
|
if len(row) > 1 and row[0] and row[1]:
|
|
try:
|
|
if rename_doc(doctype, row[0], row[1]):
|
|
msg = _("Successful: {0} to {1}").format(row[0], row[1])
|
|
frappe.db.commit()
|
|
else:
|
|
msg = _("Ignored: {0} to {1}").format(row[0], row[1])
|
|
except Exception as e:
|
|
msg = _("** Failed: {0} to {1}: {2}").format(row[0], row[1], repr(e))
|
|
frappe.db.rollback()
|
|
|
|
if via_console:
|
|
print(msg)
|
|
else:
|
|
rename_log.append(msg)
|
|
|
|
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype)
|
|
|
|
if not via_console:
|
|
return rename_log
|
|
|
|
def update_linked_doctypes(doctype, docname, linked_to, value, ignore_doctypes=None):
|
|
from frappe.model.utils.rename_doc import update_linked_doctypes
|
|
show_deprecation_warning("update_linked_doctypes")
|
|
|
|
return update_linked_doctypes(
|
|
doctype=doctype,
|
|
docname=docname,
|
|
linked_to=linked_to,
|
|
value=value,
|
|
ignore_doctypes=ignore_doctypes,
|
|
)
|
|
|
|
|
|
def get_fetch_fields(doctype, linked_to, ignore_doctypes=None):
|
|
from frappe.model.utils.rename_doc import get_fetch_fields
|
|
show_deprecation_warning("get_fetch_fields")
|
|
|
|
return get_fetch_fields(
|
|
doctype=doctype, linked_to=linked_to, ignore_doctypes=ignore_doctypes
|
|
)
|
|
|
|
def show_deprecation_warning(funct):
|
|
from click import secho
|
|
message = (
|
|
f"Function frappe.model.rename_doc.{funct} has been deprecated and "
|
|
"moved to the frappe.model.utils.rename_doc"
|
|
)
|
|
secho(message, fg="yellow")
|