Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 
 
 

578 rader
21 KiB

  1. # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. import json
  4. from collections import defaultdict
  5. import itertools
  6. from typing import Dict, List, Optional
  7. import frappe
  8. import frappe.desk.form.load
  9. import frappe.desk.form.meta
  10. from frappe import _
  11. from frappe.model.meta import is_single
  12. from frappe.modules import load_doctype_module
  13. @frappe.whitelist()
  14. def get_submitted_linked_docs(doctype: str, name: str) -> List[tuple]:
  15. """ Get all the nested submitted documents those are present in referencing tables (dependent tables).
  16. :param doctype: Document type
  17. :param name: Name of the document
  18. Usecase:
  19. * User should be able to cancel the linked documents along with the one user trying to cancel.
  20. Case1: If document sd1-n1 (document name n1 from sumittable doctype sd1) is linked to sd2-n2 and sd2-n2 is linked to sd3-n3,
  21. Getting submittable linked docs of `sd1-n1`should give both sd2-n2 and sd3-n3.
  22. Case2: If document sd1-n1 (document name n1 from sumittable doctype sd1) is linked to d2-n2 and d2-n2 is linked to sd3-n3,
  23. Getting submittable linked docs of `sd1-n1`should give None. (because d2-n2 is not a submittable doctype)
  24. Case3: If document sd1-n1 (document name n1 from submittable doctype sd1) is linked to d2-n2 & sd2-n2. d2-n2 is linked to sd3-n3.
  25. Getting submittable linked docs of `sd1-n1`should give sd2-n2.
  26. Logic:
  27. -----
  28. 1. We can find linked documents only if we know how the doctypes are related.
  29. 2. As we need only submittable documents, we can limit doctype relations search to submittable doctypes by
  30. finding the relationships(Foreign key references) across submittable doctypes.
  31. 3. Searching for links is going to be a tree like structure where at every level,
  32. you will be finding documents using parent document and parent document links.
  33. """
  34. tree = SubmittableDocumentTree(doctype, name)
  35. visited_documents = tree.get_all_children()
  36. docs = []
  37. for dt, names in visited_documents.items():
  38. docs.extend([{'doctype': dt, 'name': name, 'docstatus': 1} for name in names])
  39. return {
  40. "docs": docs,
  41. "count": len(docs)
  42. }
  43. class SubmittableDocumentTree:
  44. def __init__(self, doctype: str, name: str):
  45. """Construct a tree for the submitable linked documents.
  46. * Node has properties like doctype and docnames. Represented as Node(doctype, docnames).
  47. * Nodes are linked by doctype relationships like table, link and dynamic links.
  48. * Node is referenced(linked) by many other documents and those are the child nodes.
  49. NOTE: child document is a property of child node (not same as Frappe child docs of a table field).
  50. """
  51. self.root_doctype = doctype
  52. self.root_docname = name
  53. # Documents those are yet to be visited for linked documents.
  54. self.to_be_visited_documents = {doctype: [name]}
  55. self.visited_documents = defaultdict(list)
  56. self._submittable_doctypes = None # All submittable doctypes in the system
  57. self._references_across_doctypes = None # doctype wise links/references
  58. def get_all_children(self):
  59. """Get all nodes of a tree except the root node (all the nested submitted
  60. documents those are present in referencing tables (dependent tables).
  61. """
  62. while self.to_be_visited_documents:
  63. next_level_children = defaultdict(list)
  64. for parent_dt in list(self.to_be_visited_documents):
  65. parent_docs = self.to_be_visited_documents.get(parent_dt)
  66. if not parent_docs:
  67. del self.to_be_visited_documents[parent_dt]
  68. continue
  69. child_docs = self.get_next_level_children(parent_dt, parent_docs)
  70. self.visited_documents[parent_dt].extend(parent_docs)
  71. for linked_dt, linked_names in child_docs.items():
  72. not_visited_child_docs = set(linked_names) - set(self.visited_documents.get(linked_dt, []))
  73. next_level_children[linked_dt].extend(not_visited_child_docs)
  74. self.to_be_visited_documents = next_level_children
  75. # Remove root node from visited documents
  76. if self.root_docname in self.visited_documents.get(self.root_doctype, []):
  77. self.visited_documents[self.root_doctype].remove(self.root_docname)
  78. return self.visited_documents
  79. def get_next_level_children(self, parent_dt, parent_names):
  80. """Get immediate children of a Node(parent_dt, parent_names)
  81. """
  82. referencing_fields = self.get_doctype_references(parent_dt)
  83. child_docs = defaultdict(list)
  84. for field in referencing_fields:
  85. links = get_referencing_documents(parent_dt, parent_names.copy(), field, get_parent_if_child_table_doc=True,
  86. parent_filters=[('docstatus', '=', 1)], allowed_parents=self.get_link_sources()) or {}
  87. for dt, names in links.items():
  88. child_docs[dt].extend(names)
  89. return child_docs
  90. def get_doctype_references(self, doctype):
  91. """Get references for a given document.
  92. """
  93. if self._references_across_doctypes is None:
  94. get_links_to = self.get_document_sources()
  95. limit_link_doctypes = self.get_link_sources()
  96. self._references_across_doctypes = get_references_across_doctypes(
  97. get_links_to, limit_link_doctypes)
  98. return self._references_across_doctypes.get(doctype, [])
  99. def get_document_sources(self):
  100. """Returns list of doctypes from where we access submittable documents.
  101. """
  102. return list(set(self.get_link_sources() + [self.root_doctype]))
  103. def get_link_sources(self):
  104. """limit doctype links to these doctypes.
  105. """
  106. return list(set(self.get_submittable_doctypes()) - set(get_exempted_doctypes() or []))
  107. def get_submittable_doctypes(self) -> List[str]:
  108. """Returns list of submittable doctypes.
  109. """
  110. if not self._submittable_doctypes:
  111. self._submittable_doctypes = frappe.db.get_all('DocType', {'is_submittable': 1}, pluck='name')
  112. return self._submittable_doctypes
  113. def get_child_tables_of_doctypes(doctypes: List[str]=None):
  114. """Returns child tables by doctype.
  115. """
  116. filters=[['fieldtype','=', 'Table']]
  117. filters_for_docfield = filters
  118. filters_for_customfield = filters
  119. if doctypes:
  120. filters_for_docfield = filters + [['parent', 'in', tuple(doctypes)]]
  121. filters_for_customfield = filters + [['dt', 'in', tuple(doctypes)]]
  122. links = frappe.get_all("DocField",
  123. fields=["parent", "fieldname", "options as child_table"],
  124. filters=filters_for_docfield,
  125. as_list=1)
  126. links+= frappe.get_all("Custom Field",
  127. fields=["dt as parent", "fieldname", "options as child_table"],
  128. filters=filters_for_customfield,
  129. as_list=1)
  130. child_tables_by_doctype = defaultdict(list)
  131. for doctype, fieldname, child_table in links:
  132. child_tables_by_doctype[doctype].append(
  133. {'doctype': doctype, 'fieldname': fieldname, 'child_table': child_table})
  134. return child_tables_by_doctype
  135. def get_references_across_doctypes(to_doctypes: List[str]=None, limit_link_doctypes: List[str]=None) -> List:
  136. """Find doctype wise foreign key references.
  137. :param to_doctypes: Get links of these doctypes.
  138. :param limit_link_doctypes: limit links to these doctypes.
  139. * Include child table, link and dynamic link references.
  140. """
  141. if limit_link_doctypes:
  142. child_tables_by_doctype = get_child_tables_of_doctypes(limit_link_doctypes)
  143. all_child_tables = [each['child_table'] for each in itertools.chain(*child_tables_by_doctype.values())]
  144. limit_link_doctypes = limit_link_doctypes + all_child_tables
  145. else:
  146. child_tables_by_doctype = get_child_tables_of_doctypes()
  147. all_child_tables = [each['child_table'] for each in itertools.chain(*child_tables_by_doctype.values())]
  148. references_by_link_fields = get_references_across_doctypes_by_link_field(to_doctypes, limit_link_doctypes)
  149. references_by_dlink_fields = get_references_across_doctypes_by_dynamic_link_field(to_doctypes, limit_link_doctypes)
  150. references = references_by_link_fields.copy()
  151. for k, v in references_by_dlink_fields.items():
  152. references.setdefault(k, []).extend(v)
  153. for doctype, links in references.items():
  154. for link in links:
  155. link['is_child'] = (link['doctype'] in all_child_tables)
  156. return references
  157. def get_references_across_doctypes_by_link_field(to_doctypes: List[str]=None, limit_link_doctypes: List[str]=None):
  158. """Find doctype wise foreign key references based on link fields.
  159. :param to_doctypes: Get links to these doctypes.
  160. :param limit_link_doctypes: limit links to these doctypes.
  161. """
  162. filters=[['fieldtype','=', 'Link']]
  163. if to_doctypes:
  164. filters += [['options', 'in', tuple(to_doctypes)]]
  165. filters_for_docfield = filters[:]
  166. filters_for_customfield = filters[:]
  167. if limit_link_doctypes:
  168. filters_for_docfield += [['parent', 'in', tuple(limit_link_doctypes)]]
  169. filters_for_customfield += [['dt', 'in', tuple(limit_link_doctypes)]]
  170. links = frappe.get_all("DocField",
  171. fields=["parent", "fieldname", "options as linked_to"],
  172. filters=filters_for_docfield,
  173. as_list=1)
  174. links+= frappe.get_all("Custom Field",
  175. fields=["dt as parent", "fieldname", "options as linked_to"],
  176. filters=filters_for_customfield,
  177. as_list=1)
  178. links_by_doctype = defaultdict(list)
  179. for doctype, fieldname, linked_to in links:
  180. links_by_doctype[linked_to].append({'doctype': doctype, 'fieldname': fieldname})
  181. return links_by_doctype
  182. def get_references_across_doctypes_by_dynamic_link_field(to_doctypes: List[str]=None, limit_link_doctypes: List[str]=None):
  183. """Find doctype wise foreign key references based on dynamic link fields.
  184. :param to_doctypes: Get links to these doctypes.
  185. :param limit_link_doctypes: limit links to these doctypes.
  186. """
  187. filters=[['fieldtype','=', 'Dynamic Link']]
  188. filters_for_docfield = filters[:]
  189. filters_for_customfield = filters[:]
  190. if limit_link_doctypes:
  191. filters_for_docfield += [['parent', 'in', tuple(limit_link_doctypes)]]
  192. filters_for_customfield += [['dt', 'in', tuple(limit_link_doctypes)]]
  193. # find dynamic links of parents
  194. links = frappe.get_all("DocField",
  195. fields=["parent as doctype", "fieldname", "options as doctype_fieldname"],
  196. filters=filters_for_docfield,
  197. as_list=1)
  198. links += frappe.get_all("Custom Field",
  199. fields=["dt as doctype", "fieldname", "options as doctype_fieldname"],
  200. filters=filters_for_customfield,
  201. as_list=1)
  202. links_by_doctype = defaultdict(list)
  203. for doctype, fieldname, doctype_fieldname in links:
  204. try:
  205. filters = [[doctype_fieldname, 'in', to_doctypes]] if to_doctypes else []
  206. for linked_to in frappe.db.get_all(doctype, pluck=doctype_fieldname, filters = filters, distinct=1):
  207. if linked_to:
  208. links_by_doctype[linked_to].append({'doctype': doctype, 'fieldname': fieldname, 'doctype_fieldname': doctype_fieldname})
  209. except frappe.db.ProgrammingError:
  210. # TODO: FIXME
  211. continue
  212. return links_by_doctype
  213. def get_referencing_documents(reference_doctype: str, reference_names: List[str],
  214. link_info: dict, get_parent_if_child_table_doc: bool=True,
  215. parent_filters: List[list]=None, child_filters=None, allowed_parents=None):
  216. """Get linked documents based on link_info.
  217. :param reference_doctype: reference doctype to find links
  218. :param reference_names: reference document names to find links for
  219. :param link_info: linking details to get the linked documents
  220. Ex: {'doctype': 'Purchase Invoice Advance', 'fieldname': 'reference_name',
  221. 'doctype_fieldname': 'reference_type', 'is_child': True}
  222. :param get_parent_if_child_table_doc: Get parent record incase linked document is a child table record.
  223. :param parent_filters: filters to apply on if not a child table.
  224. :param child_filters: apply filters if it is a child table.
  225. :param allowed_parents: list of parents allowed in case of get_parent_if_child_table_doc
  226. is enabled.
  227. """
  228. from_table = link_info['doctype']
  229. filters = [[link_info['fieldname'], 'in', tuple(reference_names)]]
  230. if link_info.get('doctype_fieldname'):
  231. filters.append([link_info['doctype_fieldname'], '=', reference_doctype])
  232. if not link_info.get('is_child'):
  233. filters.extend(parent_filters or [])
  234. return {from_table: frappe.db.get_all(from_table, filters, pluck='name')}
  235. filters.extend(child_filters or [])
  236. res = frappe.db.get_all(from_table, filters = filters, fields = ['name', 'parenttype', 'parent'])
  237. documents = defaultdict(list)
  238. for parent, rows in itertools.groupby(res, key = lambda row: row['parenttype']):
  239. if allowed_parents and parent not in allowed_parents:
  240. continue
  241. filters = (parent_filters or []) + [['name', 'in', tuple([row.parent for row in rows])]]
  242. documents[parent].extend(frappe.db.get_all(parent, filters=filters, pluck='name') or [])
  243. return documents
  244. @frappe.whitelist()
  245. def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=None):
  246. """
  247. Cancel all linked doctype, optionally ignore doctypes specified in a list.
  248. Arguments:
  249. docs (json str) - It contains list of dictionaries of a linked documents.
  250. ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling.
  251. """
  252. if ignore_doctypes_on_cancel_all is None:
  253. ignore_doctypes_on_cancel_all = []
  254. docs = json.loads(docs)
  255. if isinstance(ignore_doctypes_on_cancel_all, str):
  256. ignore_doctypes_on_cancel_all = json.loads(ignore_doctypes_on_cancel_all)
  257. for i, doc in enumerate(docs, 1):
  258. if validate_linked_doc(doc, ignore_doctypes_on_cancel_all):
  259. linked_doc = frappe.get_doc(doc.get("doctype"), doc.get("name"))
  260. linked_doc.cancel()
  261. frappe.publish_progress(percent=i/len(docs) * 100, title=_("Cancelling documents"))
  262. def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=None):
  263. """
  264. Validate a document to be submitted and non-exempted from auto-cancel.
  265. Arguments:
  266. docinfo (dict): The document to check for submitted and non-exempt from auto-cancel
  267. ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling.
  268. Returns:
  269. bool: True if linked document passes all validations, else False
  270. """
  271. #ignore doctype to cancel
  272. if docinfo.get("doctype") in (ignore_doctypes_on_cancel_all or []):
  273. return False
  274. # skip non-submittable doctypes since they don't need to be cancelled
  275. if not frappe.get_meta(docinfo.get('doctype')).is_submittable:
  276. return False
  277. # skip draft or cancelled documents
  278. if docinfo.get('docstatus') != 1:
  279. return False
  280. # skip other doctypes since they don't need to be cancelled
  281. auto_cancel_exempt_doctypes = get_exempted_doctypes()
  282. if docinfo.get('doctype') in auto_cancel_exempt_doctypes:
  283. return False
  284. return True
  285. def get_exempted_doctypes():
  286. """ Get list of doctypes exempted from being auto-cancelled """
  287. auto_cancel_exempt_doctypes = []
  288. for doctypes in frappe.get_hooks('auto_cancel_exempted_doctypes'):
  289. auto_cancel_exempt_doctypes.append(doctypes)
  290. return auto_cancel_exempt_doctypes
  291. @frappe.whitelist()
  292. def get_linked_docs(doctype: str, name: str, linkinfo: Optional[Dict] = None) -> Dict[str, List]:
  293. if isinstance(linkinfo, str):
  294. # additional fields are added in linkinfo
  295. linkinfo = json.loads(linkinfo)
  296. results = {}
  297. if not linkinfo:
  298. return results
  299. for dt, link in linkinfo.items():
  300. filters = []
  301. link["doctype"] = dt
  302. try:
  303. link_meta_bundle = frappe.desk.form.load.get_meta_bundle(dt)
  304. except Exception as e:
  305. if isinstance(e, frappe.DoesNotExistError):
  306. if frappe.local.message_log:
  307. frappe.local.message_log.pop()
  308. continue
  309. linkmeta = link_meta_bundle[0]
  310. if not linkmeta.has_permission():
  311. continue
  312. if not linkmeta.get("issingle"):
  313. fields = [d.fieldname for d in linkmeta.get("fields", {
  314. "in_list_view": 1,
  315. "fieldtype": ["not in", ("Image", "HTML", "Button") + frappe.model.table_fields]
  316. })] + ["name", "modified", "docstatus"]
  317. if link.get("add_fields"):
  318. fields += link["add_fields"]
  319. fields = ["`tab{dt}`.`{fn}`".format(dt=dt, fn=sf.strip()) for sf in fields if sf
  320. and "`tab" not in sf]
  321. try:
  322. if link.get("filters"):
  323. ret = frappe.get_all(doctype=dt, fields=fields, filters=link.get("filters"))
  324. elif link.get("get_parent"):
  325. ret = None
  326. # check for child table
  327. if not frappe.get_meta(doctype).istable:
  328. continue
  329. me = frappe.db.get_value(doctype, name, ["parenttype", "parent"], as_dict=True)
  330. if me and me.parenttype == dt:
  331. ret = frappe.get_all(doctype=dt, fields=fields,
  332. filters=[[dt, "name", '=', me.parent]])
  333. elif link.get("child_doctype"):
  334. or_filters = [[link.get('child_doctype'), link_fieldnames, '=', name] for link_fieldnames in link.get("fieldname")]
  335. # dynamic link
  336. if link.get("doctype_fieldname"):
  337. filters.append([link.get('child_doctype'), link.get("doctype_fieldname"), "=", doctype])
  338. ret = frappe.get_all(doctype=dt, fields=fields, filters=filters, or_filters=or_filters, distinct=True)
  339. else:
  340. link_fieldnames = link.get("fieldname")
  341. if link_fieldnames:
  342. if isinstance(link_fieldnames, str):
  343. link_fieldnames = [link_fieldnames]
  344. or_filters = [[dt, fieldname, '=', name] for fieldname in link_fieldnames]
  345. # dynamic link
  346. if link.get("doctype_fieldname"):
  347. filters.append([dt, link.get("doctype_fieldname"), "=", doctype])
  348. ret = frappe.get_all(doctype=dt, fields=fields, filters=filters, or_filters=or_filters)
  349. else:
  350. ret = None
  351. except frappe.PermissionError:
  352. if frappe.local.message_log:
  353. frappe.local.message_log.pop()
  354. continue
  355. if ret:
  356. results[dt] = ret
  357. return results
  358. @frappe.whitelist()
  359. def get(doctype, docname):
  360. linked_doctypes = get_linked_doctypes(doctype=doctype)
  361. return get_linked_docs(doctype=doctype, name=docname, linkinfo=linked_doctypes)
  362. @frappe.whitelist()
  363. def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):
  364. """add list of doctypes this doctype is 'linked' with.
  365. Example, for Customer:
  366. {"Address": {"fieldname": "customer"}..}
  367. """
  368. if(without_ignore_user_permissions_enabled):
  369. return frappe.cache().hget("linked_doctypes_without_ignore_user_permissions_enabled",
  370. doctype, lambda: _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled))
  371. else:
  372. return frappe.cache().hget("linked_doctypes", doctype, lambda: _get_linked_doctypes(doctype))
  373. def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):
  374. ret = {}
  375. # find fields where this doctype is linked
  376. ret.update(get_linked_fields(doctype, without_ignore_user_permissions_enabled))
  377. ret.update(get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled))
  378. filters = [['fieldtype', 'in', frappe.model.table_fields], ['options', '=', doctype]]
  379. if without_ignore_user_permissions_enabled: filters.append(['ignore_user_permissions', '!=', 1])
  380. # find links of parents
  381. links = frappe.get_all("DocField", fields=["parent as dt"], filters=filters)
  382. links+= frappe.get_all("Custom Field", fields=["dt"], filters=filters)
  383. for dt, in links:
  384. if dt in ret: continue
  385. ret[dt] = {"get_parent": True}
  386. for dt in list(ret):
  387. try:
  388. doctype_module = load_doctype_module(dt)
  389. except (ImportError, KeyError):
  390. # in case of Custom DocType
  391. # or in case of module rename eg. (Schools -> Education)
  392. continue
  393. if getattr(doctype_module, "exclude_from_linked_with", False):
  394. del ret[dt]
  395. return ret
  396. def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False):
  397. filters = [['fieldtype','=', 'Link'], ['options', '=', doctype]]
  398. if without_ignore_user_permissions_enabled: filters.append(['ignore_user_permissions', '!=', 1])
  399. # find links of parents
  400. links = frappe.get_all("DocField", fields=["parent", "fieldname"], filters=filters, as_list=1)
  401. links += frappe.get_all("Custom Field", fields=["dt as parent", "fieldname"], filters=filters, as_list=1)
  402. ret = {}
  403. if not links: return ret
  404. links_dict = defaultdict(list)
  405. for doctype, fieldname in links:
  406. links_dict[doctype].append(fieldname)
  407. for doctype_name in links_dict:
  408. ret[doctype_name] = { "fieldname": links_dict.get(doctype_name) }
  409. table_doctypes = frappe.get_all("DocType", filters=[["istable", "=", "1"], ["name", "in", tuple(links_dict)]])
  410. child_filters = [['fieldtype','in', frappe.model.table_fields], ['options', 'in', tuple(doctype.name for doctype in table_doctypes)]]
  411. if without_ignore_user_permissions_enabled: child_filters.append(['ignore_user_permissions', '!=', 1])
  412. # find out if linked in a child table
  413. for parent, options in frappe.get_all("DocField", fields=["parent", "options"], filters=child_filters, as_list=1):
  414. ret[parent] = { "child_doctype": options, "fieldname": links_dict[options]}
  415. if options in ret: del ret[options]
  416. return ret
  417. def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=False):
  418. ret = {}
  419. filters = [['fieldtype','=', 'Dynamic Link']]
  420. if without_ignore_user_permissions_enabled: filters.append(['ignore_user_permissions', '!=', 1])
  421. # find dynamic links of parents
  422. links = frappe.get_all("DocField", fields=["parent as doctype", "fieldname", "options as doctype_fieldname"], filters=filters)
  423. links += frappe.get_all("Custom Field", fields=["dt as doctype", "fieldname", "options as doctype_fieldname"], filters=filters)
  424. for df in links:
  425. if is_single(df.doctype): continue
  426. is_child = frappe.get_meta(df.doctype).istable
  427. possible_link = frappe.get_all(
  428. df.doctype,
  429. filters={df.doctype_fieldname: doctype},
  430. fields=["parenttype"] if is_child else None,
  431. distinct=True
  432. )
  433. if not possible_link: continue
  434. if is_child:
  435. for d in possible_link:
  436. ret[d.parenttype] = {
  437. "child_doctype": df.doctype,
  438. "fieldname": [df.fieldname],
  439. "doctype_fieldname": df.doctype_fieldname
  440. }
  441. else:
  442. ret[df.doctype] = {
  443. "fieldname": [df.fieldname],
  444. "doctype_fieldname": df.doctype_fieldname
  445. }
  446. return ret