您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

379 行
13 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. import os
  4. import shutil
  5. import frappe
  6. import frappe.defaults
  7. import frappe.model.meta
  8. from frappe import _
  9. from frappe import get_module_path
  10. from frappe.model.dynamic_links import get_dynamic_link_map
  11. from frappe.utils.file_manager import remove_all
  12. from frappe.utils.password import delete_all_passwords_for
  13. from frappe.model.naming import revert_series_if_last
  14. from frappe.utils.global_search import delete_for_document
  15. from frappe.desk.doctype.tag.tag import delete_tags_for_document
  16. doctypes_to_skip = ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", "File",
  17. "Version", "Document Follow", "Comment" , "View Log", "Tag Link", "Notification Log", "Email Queue")
  18. def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False, ignore_permissions=False,
  19. flags=None, ignore_on_trash=False, ignore_missing=True, delete_permanently=False):
  20. """
  21. Deletes a doc(dt, dn) and validates if it is not submitted and not linked in a live record
  22. """
  23. if not ignore_doctypes: ignore_doctypes = []
  24. # get from form
  25. if not doctype:
  26. doctype = frappe.form_dict.get('dt')
  27. name = frappe.form_dict.get('dn')
  28. names = name
  29. if isinstance(name, str) or isinstance(name, int):
  30. names = [name]
  31. for name in names or []:
  32. # already deleted..?
  33. if not frappe.db.exists(doctype, name):
  34. if not ignore_missing:
  35. raise frappe.DoesNotExistError
  36. else:
  37. return False
  38. # delete passwords
  39. delete_all_passwords_for(doctype, name)
  40. doc = None
  41. if doctype=="DocType":
  42. if for_reload:
  43. try:
  44. doc = frappe.get_doc(doctype, name)
  45. except frappe.DoesNotExistError:
  46. pass
  47. else:
  48. doc.run_method("before_reload")
  49. else:
  50. doc = frappe.get_doc(doctype, name)
  51. update_flags(doc, flags, ignore_permissions)
  52. check_permission_and_not_submitted(doc)
  53. frappe.db.delete("Custom Field", {"dt": name})
  54. frappe.db.delete("Client Script", {"dt": name})
  55. frappe.db.delete("Property Setter", {"doc_type": name})
  56. frappe.db.delete("Report", {"ref_doctype": name})
  57. frappe.db.delete("Custom DocPerm", {"parent": name})
  58. frappe.db.delete("__global_search", {"doctype": name})
  59. delete_from_table(doctype, name, ignore_doctypes, None)
  60. if frappe.conf.developer_mode and not doc.custom and not (
  61. for_reload
  62. or frappe.flags.in_migrate
  63. or frappe.flags.in_install
  64. or frappe.flags.in_uninstall
  65. ):
  66. try:
  67. delete_controllers(name, doc.module)
  68. except (FileNotFoundError, OSError, KeyError):
  69. # in case a doctype doesnt have any controller code nor any app and module
  70. pass
  71. else:
  72. doc = frappe.get_doc(doctype, name)
  73. if not for_reload:
  74. update_flags(doc, flags, ignore_permissions)
  75. check_permission_and_not_submitted(doc)
  76. if not ignore_on_trash:
  77. doc.run_method("on_trash")
  78. doc.flags.in_delete = True
  79. doc.run_method('on_change')
  80. # check if links exist
  81. if not force:
  82. check_if_doc_is_linked(doc)
  83. check_if_doc_is_dynamically_linked(doc)
  84. update_naming_series(doc)
  85. delete_from_table(doctype, name, ignore_doctypes, doc)
  86. doc.run_method("after_delete")
  87. # delete attachments
  88. remove_all(doctype, name, from_delete=True, delete_permanently=delete_permanently)
  89. if not for_reload:
  90. # Enqueued at the end, because it gets committed
  91. # All the linked docs should be checked beforehand
  92. frappe.enqueue('frappe.model.delete_doc.delete_dynamic_links',
  93. doctype=doc.doctype, name=doc.name,
  94. now=frappe.flags.in_test)
  95. # clear cache for Document
  96. doc.clear_cache()
  97. # delete global search entry
  98. delete_for_document(doc)
  99. # delete tag link entry
  100. delete_tags_for_document(doc)
  101. if for_reload:
  102. delete_permanently = True
  103. if not delete_permanently:
  104. add_to_deleted_document(doc)
  105. if doc and not for_reload:
  106. if not frappe.flags.in_patch:
  107. try:
  108. doc.notify_update()
  109. insert_feed(doc)
  110. except ImportError:
  111. pass
  112. # delete user_permissions
  113. frappe.defaults.clear_default(parenttype="User Permission", key=doctype, value=name)
  114. def add_to_deleted_document(doc):
  115. '''Add this document to Deleted Document table. Called after delete'''
  116. if doc.doctype != 'Deleted Document' and frappe.flags.in_install != 'frappe':
  117. frappe.get_doc(dict(
  118. doctype='Deleted Document',
  119. deleted_doctype=doc.doctype,
  120. deleted_name=doc.name,
  121. data=doc.as_json(),
  122. owner=frappe.session.user
  123. )).db_insert()
  124. def update_naming_series(doc):
  125. if doc.meta.autoname:
  126. if doc.meta.autoname.startswith("naming_series:") \
  127. and getattr(doc, "naming_series", None):
  128. revert_series_if_last(doc.naming_series, doc.name, doc)
  129. elif doc.meta.autoname.split(":")[0] not in ("Prompt", "field", "hash"):
  130. revert_series_if_last(doc.meta.autoname, doc.name, doc)
  131. def delete_from_table(doctype, name, ignore_doctypes, doc):
  132. if doctype!="DocType" and doctype==name:
  133. frappe.db.delete("Singles", {"doctype": name})
  134. else:
  135. frappe.db.delete(doctype, {"name": name})
  136. # get child tables
  137. if doc:
  138. tables = [d.options for d in doc.meta.get_table_fields()]
  139. else:
  140. def get_table_fields(field_doctype):
  141. if field_doctype == 'Custom Field':
  142. return []
  143. return [r[0] for r in frappe.get_all(field_doctype,
  144. fields='options',
  145. filters={
  146. 'fieldtype': ['in', frappe.model.table_fields],
  147. 'parent': doctype
  148. },
  149. as_list=1
  150. )]
  151. tables = get_table_fields("DocField")
  152. if not frappe.flags.in_install=="frappe":
  153. tables += get_table_fields("Custom Field")
  154. # delete from child tables
  155. for t in list(set(tables)):
  156. if t not in ignore_doctypes:
  157. frappe.db.delete(t, {"parenttype": doctype, "parent": name})
  158. def update_flags(doc, flags=None, ignore_permissions=False):
  159. if ignore_permissions:
  160. if not flags: flags = {}
  161. flags["ignore_permissions"] = ignore_permissions
  162. if flags:
  163. doc.flags.update(flags)
  164. def check_permission_and_not_submitted(doc):
  165. # permission
  166. if (not doc.flags.ignore_permissions
  167. and frappe.session.user!="Administrator"
  168. and (
  169. not doc.has_permission("delete")
  170. or (doc.doctype=="DocType" and not doc.custom))):
  171. frappe.msgprint(_("User not allowed to delete {0}: {1}")
  172. .format(doc.doctype, doc.name), raise_exception=frappe.PermissionError)
  173. # check if submitted
  174. if doc.docstatus.is_submitted():
  175. frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.").format(_(doc.doctype), doc.name, "<a href='https://docs.erpnext.com//docs/user/manual/en/setting-up/articles/delete-submitted-document' target='_blank'>", "</a>"),
  176. raise_exception=True)
  177. def check_if_doc_is_linked(doc, method="Delete"):
  178. """
  179. Raises excption if the given doc(dt, dn) is linked in another record.
  180. """
  181. from frappe.model.rename_doc import get_link_fields
  182. link_fields = get_link_fields(doc.doctype)
  183. ignore_linked_doctypes = doc.get('ignore_linked_doctypes') or []
  184. for lf in link_fields:
  185. link_dt, link_field, issingle = lf['parent'], lf['fieldname'], lf['issingle']
  186. if not issingle:
  187. fields = ["name", "docstatus"]
  188. if frappe.get_meta(link_dt).istable:
  189. fields.extend(["parent", "parenttype"])
  190. for item in frappe.db.get_values(link_dt, {link_field:doc.name}, fields , as_dict=True):
  191. # available only in child table cases
  192. item_parent = getattr(item, "parent", None)
  193. linked_doctype = item.parenttype if item_parent else link_dt
  194. if linked_doctype in doctypes_to_skip or (linked_doctype in ignore_linked_doctypes and method == 'Cancel'):
  195. # don't check for communication and todo!
  196. continue
  197. if method != "Delete" and (method != "Cancel" or item.docstatus != 1):
  198. # don't raise exception if not
  199. # linked to a non-cancelled doc when deleting or to a submitted doc when cancelling
  200. continue
  201. elif link_dt == doc.doctype and (item_parent or item.name) == doc.name:
  202. # don't raise exception if not
  203. # linked to same item or doc having same name as the item
  204. continue
  205. else:
  206. reference_docname = item_parent or item.name
  207. raise_link_exists_exception(doc, linked_doctype, reference_docname)
  208. else:
  209. if frappe.db.get_value(link_dt, None, link_field) == doc.name:
  210. raise_link_exists_exception(doc, link_dt, link_dt)
  211. def check_if_doc_is_dynamically_linked(doc, method="Delete"):
  212. '''Raise `frappe.LinkExistsError` if the document is dynamically linked'''
  213. for df in get_dynamic_link_map().get(doc.doctype, []):
  214. ignore_linked_doctypes = doc.get('ignore_linked_doctypes') or []
  215. if df.parent in doctypes_to_skip or (df.parent in ignore_linked_doctypes and method == 'Cancel'):
  216. # don't check for communication and todo!
  217. continue
  218. meta = frappe.get_meta(df.parent)
  219. if meta.issingle:
  220. # dynamic link in single doc
  221. refdoc = frappe.db.get_singles_dict(df.parent)
  222. if (refdoc.get(df.options)==doc.doctype
  223. and refdoc.get(df.fieldname)==doc.name
  224. and ((method=="Delete" and refdoc.docstatus < 2)
  225. or (method=="Cancel" and refdoc.docstatus==1))
  226. ):
  227. # raise exception only if
  228. # linked to an non-cancelled doc when deleting
  229. # or linked to a submitted doc when cancelling
  230. raise_link_exists_exception(doc, df.parent, df.parent)
  231. else:
  232. # dynamic link in table
  233. df["table"] = ", `parent`, `parenttype`, `idx`" if meta.istable else ""
  234. for refdoc in frappe.db.sql("""select `name`, `docstatus` {table} from `tab{parent}` where
  235. {options}=%s and {fieldname}=%s""".format(**df), (doc.doctype, doc.name), as_dict=True):
  236. if ((method=="Delete" and refdoc.docstatus < 2) or (method=="Cancel" and refdoc.docstatus==1)):
  237. # raise exception only if
  238. # linked to an non-cancelled doc when deleting
  239. # or linked to a submitted doc when cancelling
  240. reference_doctype = refdoc.parenttype if meta.istable else df.parent
  241. reference_docname = refdoc.parent if meta.istable else refdoc.name
  242. at_position = "at Row: {0}".format(refdoc.idx) if meta.istable else ""
  243. raise_link_exists_exception(doc, reference_doctype, reference_docname, at_position)
  244. def raise_link_exists_exception(doc, reference_doctype, reference_docname, row=''):
  245. doc_link = '<a href="/app/Form/{0}/{1}">{1}</a>'.format(doc.doctype, doc.name)
  246. reference_link = '<a href="/app/Form/{0}/{1}">{1}</a>'.format(reference_doctype, reference_docname)
  247. #hack to display Single doctype only once in message
  248. if reference_doctype == reference_docname:
  249. reference_doctype = ''
  250. frappe.throw(_('Cannot delete or cancel because {0} {1} is linked with {2} {3} {4}')
  251. .format(doc.doctype, doc_link, reference_doctype, reference_link, row), frappe.LinkExistsError)
  252. def delete_dynamic_links(doctype, name):
  253. delete_references('ToDo', doctype, name, 'reference_type')
  254. delete_references('Email Unsubscribe', doctype, name)
  255. delete_references('DocShare', doctype, name, 'share_doctype', 'share_name')
  256. delete_references('Version', doctype, name, 'ref_doctype', 'docname')
  257. delete_references('Comment', doctype, name)
  258. delete_references('View Log', doctype, name)
  259. delete_references('Document Follow', doctype, name, 'ref_doctype', 'ref_docname')
  260. delete_references('Notification Log', doctype, name, 'document_type', 'document_name')
  261. # unlink communications
  262. clear_timeline_references(doctype, name)
  263. clear_references('Communication', doctype, name)
  264. clear_references('Activity Log', doctype, name)
  265. clear_references('Activity Log', doctype, name, 'timeline_doctype', 'timeline_name')
  266. def delete_references(doctype, reference_doctype, reference_name,
  267. reference_doctype_field = 'reference_doctype', reference_name_field = 'reference_name'):
  268. frappe.db.delete(doctype, {
  269. reference_doctype_field: reference_doctype,
  270. reference_name_field: reference_name
  271. })
  272. def clear_references(doctype, reference_doctype, reference_name,
  273. reference_doctype_field = 'reference_doctype', reference_name_field = 'reference_name'):
  274. frappe.db.sql('''update
  275. `tab{0}`
  276. set
  277. {1}=NULL, {2}=NULL
  278. where
  279. {1}=%s and {2}=%s'''.format(doctype, reference_doctype_field, reference_name_field), # nosec
  280. (reference_doctype, reference_name))
  281. def clear_timeline_references(link_doctype, link_name):
  282. frappe.db.delete("Communication Link", {
  283. "link_doctype": link_doctype,
  284. "link_name": link_name
  285. })
  286. def insert_feed(doc):
  287. if (
  288. frappe.flags.in_install
  289. or frappe.flags.in_uninstall
  290. or frappe.flags.in_import
  291. or getattr(doc, "no_feed_on_delete", False)
  292. ):
  293. return
  294. from frappe.utils import get_fullname
  295. frappe.get_doc({
  296. "doctype": "Comment",
  297. "comment_type": "Deleted",
  298. "reference_doctype": doc.doctype,
  299. "subject": "{0} {1}".format(_(doc.doctype), doc.name),
  300. "full_name": get_fullname(doc.owner),
  301. }).insert(ignore_permissions=True)
  302. def delete_controllers(doctype, module):
  303. """
  304. Delete controller code in the doctype folder
  305. """
  306. module_path = get_module_path(module)
  307. dir_path = os.path.join(module_path, 'doctype', frappe.scrub(doctype))
  308. shutil.rmtree(dir_path)