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

445 行
13 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. from typing import Dict, List, Union
  4. import frappe, json
  5. import frappe.utils
  6. import frappe.share
  7. import frappe.defaults
  8. import frappe.desk.form.meta
  9. from frappe.model.utils.user_settings import get_user_settings
  10. from frappe.permissions import get_doc_permissions
  11. from frappe.desk.form.document_follow import is_document_followed
  12. from frappe import _
  13. from urllib.parse import quote
  14. @frappe.whitelist()
  15. def getdoc(doctype, name, user=None):
  16. """
  17. Loads a doclist for a given document. This method is called directly from the client.
  18. Requries "doctype", "name" as form variables.
  19. Will also call the "onload" method on the document.
  20. """
  21. if not (doctype and name):
  22. raise Exception('doctype and name required!')
  23. if not name:
  24. name = doctype
  25. if not frappe.db.exists(doctype, name):
  26. return []
  27. try:
  28. doc = frappe.get_doc(doctype, name)
  29. run_onload(doc)
  30. if not doc.has_permission("read"):
  31. frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype + ' ' + name))
  32. raise frappe.PermissionError(("read", doctype, name))
  33. doc.apply_fieldlevel_read_permissions()
  34. # add file list
  35. doc.add_viewed()
  36. get_docinfo(doc)
  37. except Exception:
  38. frappe.errprint(frappe.utils.get_traceback())
  39. raise
  40. doc.add_seen()
  41. set_link_titles(doc)
  42. frappe.response.docs.append(doc)
  43. @frappe.whitelist()
  44. def getdoctype(doctype, with_parent=False, cached_timestamp=None):
  45. """load doctype"""
  46. docs = []
  47. parent_dt = None
  48. # with parent (called from report builder)
  49. if with_parent:
  50. parent_dt = frappe.model.meta.get_parent_dt(doctype)
  51. if parent_dt:
  52. docs = get_meta_bundle(parent_dt)
  53. frappe.response['parent_dt'] = parent_dt
  54. if not docs:
  55. docs = get_meta_bundle(doctype)
  56. frappe.response['user_settings'] = get_user_settings(parent_dt or doctype)
  57. if cached_timestamp and docs[0].modified==cached_timestamp:
  58. return "use_cache"
  59. frappe.response.docs.extend(docs)
  60. def get_meta_bundle(doctype):
  61. bundle = [frappe.desk.form.meta.get_meta(doctype)]
  62. for df in bundle[0].fields:
  63. if df.fieldtype in frappe.model.table_fields:
  64. bundle.append(frappe.desk.form.meta.get_meta(df.options, not frappe.conf.developer_mode))
  65. return bundle
  66. @frappe.whitelist()
  67. def get_docinfo(doc=None, doctype=None, name=None):
  68. if not doc:
  69. doc = frappe.get_doc(doctype, name)
  70. if not doc.has_permission("read"):
  71. raise frappe.PermissionError
  72. all_communications = _get_communications(doc.doctype, doc.name)
  73. automated_messages = [msg for msg in all_communications if msg['communication_type'] == 'Automated Message']
  74. communications_except_auto_messages = [msg for msg in all_communications if msg['communication_type'] != 'Automated Message']
  75. docinfo = frappe._dict(user_info = {})
  76. add_comments(doc, docinfo)
  77. docinfo.update({
  78. "attachments": get_attachments(doc.doctype, doc.name),
  79. "communications": communications_except_auto_messages,
  80. "automated_messages": automated_messages,
  81. 'total_comments': len(json.loads(doc.get('_comments') or '[]')),
  82. 'versions': get_versions(doc),
  83. "assignments": get_assignments(doc.doctype, doc.name),
  84. "permissions": get_doc_permissions(doc),
  85. "shared": frappe.share.get_users(doc.doctype, doc.name),
  86. "views": get_view_logs(doc.doctype, doc.name),
  87. "energy_point_logs": get_point_logs(doc.doctype, doc.name),
  88. "additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
  89. "milestones": get_milestones(doc.doctype, doc.name),
  90. "is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
  91. "tags": get_tags(doc.doctype, doc.name),
  92. "document_email": get_document_email(doc.doctype, doc.name),
  93. })
  94. update_user_info(docinfo)
  95. frappe.response["docinfo"] = docinfo
  96. return docinfo
  97. def add_comments(doc, docinfo):
  98. # divide comments into separate lists
  99. docinfo.comments = []
  100. docinfo.shared = []
  101. docinfo.assignment_logs = []
  102. docinfo.attachment_logs = []
  103. docinfo.info_logs = []
  104. docinfo.like_logs = []
  105. docinfo.workflow_logs = []
  106. comments = frappe.get_all("Comment",
  107. fields=["name", "creation", "content", "owner", "comment_type"],
  108. filters={
  109. "reference_doctype": doc.doctype,
  110. "reference_name": doc.name
  111. }
  112. )
  113. for c in comments:
  114. if c.comment_type == "Comment":
  115. c.content = frappe.utils.markdown(c.content)
  116. docinfo.comments.append(c)
  117. elif c.comment_type in ('Shared', 'Unshared'):
  118. docinfo.shared.append(c)
  119. elif c.comment_type in ('Assignment Completed', 'Assigned'):
  120. docinfo.assignment_logs.append(c)
  121. elif c.comment_type in ('Attachment', 'Attachment Removed'):
  122. docinfo.attachment_logs.append(c)
  123. elif c.comment_type in ('Info', 'Edit', 'Label'):
  124. docinfo.info_logs.append(c)
  125. elif c.comment_type == "Like":
  126. docinfo.like_logs.append(c)
  127. elif c.comment_type == "Workflow":
  128. docinfo.workflow_logs.append(c)
  129. frappe.utils.add_user_info(c.owner, docinfo.user_info)
  130. return comments
  131. def get_milestones(doctype, name):
  132. return frappe.db.get_all('Milestone', fields = ['creation', 'owner', 'track_field', 'value'],
  133. filters=dict(reference_type=doctype, reference_name=name))
  134. def get_attachments(dt, dn):
  135. return frappe.get_all("File", fields=["name", "file_name", "file_url", "is_private"],
  136. filters = {"attached_to_name": dn, "attached_to_doctype": dt})
  137. def get_versions(doc):
  138. return frappe.get_all('Version', filters=dict(ref_doctype=doc.doctype, docname=doc.name),
  139. fields=['name', 'owner', 'creation', 'data'], limit=10, order_by='creation desc')
  140. @frappe.whitelist()
  141. def get_communications(doctype, name, start=0, limit=20):
  142. doc = frappe.get_doc(doctype, name)
  143. if not doc.has_permission("read"):
  144. raise frappe.PermissionError
  145. return _get_communications(doctype, name, start, limit)
  146. def get_comments(doctype: str, name: str, comment_type : Union[str, List[str]] = "Comment") -> List[frappe._dict]:
  147. if isinstance(comment_type, list):
  148. comment_types = comment_type
  149. elif comment_type == 'share':
  150. comment_types = ['Shared', 'Unshared']
  151. elif comment_type == 'assignment':
  152. comment_types = ['Assignment Completed', 'Assigned']
  153. elif comment_type == 'attachment':
  154. comment_types = ['Attachment', 'Attachment Removed']
  155. else:
  156. comment_types = [comment_type]
  157. comments = frappe.get_all("Comment",
  158. fields=["name", "creation", "content", "owner", "comment_type"],
  159. filters={
  160. "reference_doctype": doctype,
  161. "reference_name": name,
  162. "comment_type": ['in', comment_types],
  163. }
  164. )
  165. # convert to markdown (legacy ?)
  166. for c in comments:
  167. if c.comment_type == "Comment":
  168. c.content = frappe.utils.markdown(c.content)
  169. return comments
  170. def get_point_logs(doctype, docname):
  171. return frappe.db.get_all('Energy Point Log', filters={
  172. 'reference_doctype': doctype,
  173. 'reference_name': docname,
  174. 'type': ['!=', 'Review']
  175. }, fields=['*'])
  176. def _get_communications(doctype, name, start=0, limit=20):
  177. communications = get_communication_data(doctype, name, start, limit)
  178. for c in communications:
  179. if c.communication_type=="Communication":
  180. c.attachments = json.dumps(frappe.get_all("File",
  181. fields=["file_url", "is_private"],
  182. filters={"attached_to_doctype": "Communication",
  183. "attached_to_name": c.name}
  184. ))
  185. return communications
  186. def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=None,
  187. group_by=None, as_dict=True):
  188. '''Returns list of communications for a given document'''
  189. if not fields:
  190. fields = '''
  191. C.name, C.communication_type, C.communication_medium,
  192. C.comment_type, C.communication_date, C.content,
  193. C.sender, C.sender_full_name, C.cc, C.bcc,
  194. C.creation AS creation, C.subject, C.delivery_status,
  195. C._liked_by, C.reference_doctype, C.reference_name,
  196. C.read_by_recipient, C.rating, C.recipients
  197. '''
  198. conditions = ''
  199. if after:
  200. # find after a particular date
  201. conditions += '''
  202. AND C.creation > {0}
  203. '''.format(after)
  204. if doctype=='User':
  205. conditions += '''
  206. AND NOT (C.reference_doctype='User' AND C.communication_type='Communication')
  207. '''
  208. # communications linked to reference_doctype
  209. part1 = '''
  210. SELECT {fields}
  211. FROM `tabCommunication` as C
  212. WHERE C.communication_type IN ('Communication', 'Feedback', 'Automated Message')
  213. AND (C.reference_doctype = %(doctype)s AND C.reference_name = %(name)s)
  214. {conditions}
  215. '''.format(fields=fields, conditions=conditions)
  216. # communications linked in Timeline Links
  217. part2 = '''
  218. SELECT {fields}
  219. FROM `tabCommunication` as C
  220. INNER JOIN `tabCommunication Link` ON C.name=`tabCommunication Link`.parent
  221. WHERE C.communication_type IN ('Communication', 'Feedback', 'Automated Message')
  222. AND `tabCommunication Link`.link_doctype = %(doctype)s AND `tabCommunication Link`.link_name = %(name)s
  223. {conditions}
  224. '''.format(fields=fields, conditions=conditions)
  225. communications = frappe.db.sql('''
  226. SELECT *
  227. FROM (({part1}) UNION ({part2})) AS combined
  228. {group_by}
  229. ORDER BY creation DESC
  230. LIMIT %(limit)s
  231. OFFSET %(start)s
  232. '''.format(part1=part1, part2=part2, group_by=(group_by or '')), dict(
  233. doctype=doctype,
  234. name=name,
  235. start=frappe.utils.cint(start),
  236. limit=limit
  237. ), as_dict=as_dict)
  238. return communications
  239. def get_assignments(dt, dn):
  240. return frappe.get_all("ToDo",
  241. fields=['name', 'allocated_to as owner', 'description', 'status'],
  242. filters={
  243. 'reference_type': dt,
  244. 'reference_name': dn,
  245. 'status': ('!=', 'Cancelled'),
  246. })
  247. @frappe.whitelist()
  248. def get_badge_info(doctypes, filters):
  249. filters = json.loads(filters)
  250. doctypes = json.loads(doctypes)
  251. filters["docstatus"] = ["!=", 2]
  252. out = {}
  253. for doctype in doctypes:
  254. out[doctype] = frappe.db.get_value(doctype, filters, "count(*)")
  255. return out
  256. def run_onload(doc):
  257. doc.set("__onload", frappe._dict())
  258. doc.run_method("onload")
  259. def get_view_logs(doctype, docname):
  260. """ get and return the latest view logs if available """
  261. logs = []
  262. if hasattr(frappe.get_meta(doctype), 'track_views') and frappe.get_meta(doctype).track_views:
  263. view_logs = frappe.get_all("View Log", filters={
  264. "reference_doctype": doctype,
  265. "reference_name": docname,
  266. }, fields=["name", "creation", "owner"], order_by="creation desc")
  267. if view_logs:
  268. logs = view_logs
  269. return logs
  270. def get_tags(doctype, name):
  271. tags = [tag.tag for tag in frappe.get_all("Tag Link", filters={
  272. "document_type": doctype,
  273. "document_name": name
  274. }, fields=["tag"])]
  275. return ",".join(tags)
  276. def get_document_email(doctype, name):
  277. email = get_automatic_email_link()
  278. if not email:
  279. return None
  280. email = email.split("@")
  281. return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(name), email[1])
  282. def get_automatic_email_link():
  283. return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")
  284. def get_additional_timeline_content(doctype, docname):
  285. contents = []
  286. hooks = frappe.get_hooks().get('additional_timeline_content', {})
  287. methods_for_all_doctype = hooks.get('*', [])
  288. methods_for_current_doctype = hooks.get(doctype, [])
  289. for method in methods_for_all_doctype + methods_for_current_doctype:
  290. contents.extend(frappe.get_attr(method)(doctype, docname) or [])
  291. return contents
  292. def set_link_titles(doc):
  293. link_titles = {}
  294. link_titles.update(get_title_values_for_link_and_dynamic_link_fields(doc))
  295. link_titles.update(get_title_values_for_table_and_multiselect_fields(doc))
  296. send_link_titles(link_titles)
  297. def get_title_values_for_link_and_dynamic_link_fields(doc, link_fields=None):
  298. link_titles = {}
  299. if not link_fields:
  300. meta = frappe.get_meta(doc.doctype)
  301. link_fields = meta.get_link_fields() + meta.get_dynamic_link_fields()
  302. for field in link_fields:
  303. if not doc.get(field.fieldname):
  304. continue
  305. doctype = field.options if field.fieldtype == "Link" else doc.get(field.options)
  306. meta = frappe.get_meta(doctype)
  307. if not meta or not (meta.title_field and meta.show_title_field_in_link):
  308. continue
  309. link_title = frappe.db.get_value(
  310. doctype, doc.get(field.fieldname), meta.title_field, cache=True
  311. )
  312. link_titles.update({doctype + "::" + doc.get(field.fieldname): link_title})
  313. return link_titles
  314. def get_title_values_for_table_and_multiselect_fields(doc, table_fields=None):
  315. link_titles = {}
  316. if not table_fields:
  317. meta = frappe.get_meta(doc.doctype)
  318. table_fields = meta.get_table_fields()
  319. for field in table_fields:
  320. if not doc.get(field.fieldname):
  321. continue
  322. for value in doc.get(field.fieldname):
  323. link_titles.update(get_title_values_for_link_and_dynamic_link_fields(value))
  324. return link_titles
  325. def send_link_titles(link_titles):
  326. """Append link titles dict in `frappe.local.response`."""
  327. if "_link_titles" not in frappe.local.response:
  328. frappe.local.response["_link_titles"] = {}
  329. frappe.local.response["_link_titles"].update(link_titles)
  330. def update_user_info(docinfo):
  331. for d in docinfo.communications:
  332. frappe.utils.add_user_info(d.sender, docinfo.user_info)
  333. for d in docinfo.shared:
  334. frappe.utils.add_user_info(d.user, docinfo.user_info)
  335. for d in docinfo.assignments:
  336. frappe.utils.add_user_info(d.owner, docinfo.user_info)
  337. for d in docinfo.views:
  338. frappe.utils.add_user_info(d.owner, docinfo.user_info)
  339. @frappe.whitelist()
  340. def get_user_info_for_viewers(users):
  341. user_info = {}
  342. for user in json.loads(users):
  343. frappe.utils.add_user_info(user, user_info)
  344. return user_info