You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

622 line
18 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. from __future__ import unicode_literals
  4. import frappe
  5. import os, json
  6. from frappe import _
  7. from frappe.modules import scrub, get_module_path
  8. from frappe.utils import flt, cint, get_html_format, cstr, get_url_to_form
  9. from frappe.model.utils import render_include
  10. from frappe.translate import send_translations
  11. import frappe.desk.reportview
  12. from frappe.permissions import get_role_permissions
  13. from six import string_types, iteritems
  14. from datetime import timedelta
  15. from frappe.utils import gzip_decompress
  16. def get_report_doc(report_name):
  17. doc = frappe.get_doc("Report", report_name)
  18. doc.custom_columns = []
  19. if doc.report_type == 'Custom Report':
  20. custom_report_doc = doc
  21. reference_report = custom_report_doc.reference_report
  22. doc = frappe.get_doc("Report", reference_report)
  23. doc.custom_report = report_name
  24. doc.custom_columns = custom_report_doc.json
  25. doc.is_custom_report = True
  26. if not doc.is_permitted():
  27. frappe.throw(_("You don't have access to Report: {0}").format(report_name), frappe.PermissionError)
  28. if not frappe.has_permission(doc.ref_doctype, "report"):
  29. frappe.throw(_("You don't have permission to get a report on: {0}").format(doc.ref_doctype),
  30. frappe.PermissionError)
  31. if doc.disabled:
  32. frappe.throw(_("Report {0} is disabled").format(report_name))
  33. return doc
  34. def generate_report_result(report, filters=None, user=None):
  35. status = None
  36. if not user:
  37. user = frappe.session.user
  38. if not filters:
  39. filters = []
  40. if filters and isinstance(filters, string_types):
  41. filters = json.loads(filters)
  42. columns, result, message, chart, data_to_be_printed, skip_total_row = [], [], None, None, None, 0
  43. if report.report_type == "Query Report":
  44. if not report.query:
  45. status = "error"
  46. frappe.msgprint(_("Must specify a Query to run"), raise_exception=True)
  47. if not report.query.lower().startswith("select"):
  48. status = "error"
  49. frappe.msgprint(_("Query must be a SELECT"), raise_exception=True)
  50. result = [list(t) for t in frappe.db.sql(report.query, filters)]
  51. columns = [cstr(c[0]) for c in frappe.db.get_description()]
  52. elif report.report_type == 'Script Report':
  53. res = report.execute_script_report(filters)
  54. columns, result = res[0], res[1]
  55. if len(res) > 2:
  56. message = res[2]
  57. if len(res) > 3:
  58. chart = res[3]
  59. if len(res) > 4:
  60. data_to_be_printed = res[4]
  61. if len(res) > 5:
  62. skip_total_row = cint(res[5])
  63. if report.custom_columns:
  64. columns = json.loads(report.custom_columns)
  65. result = add_data_to_custom_columns(columns, result)
  66. if result:
  67. result = get_filtered_data(report.ref_doctype, columns, result, user)
  68. if cint(report.add_total_row) and result and not skip_total_row:
  69. result = add_total_row(result, columns)
  70. return {
  71. "result": result,
  72. "columns": columns,
  73. "message": message,
  74. "chart": chart,
  75. "data_to_be_printed": data_to_be_printed,
  76. "skip_total_row": skip_total_row,
  77. "status": status,
  78. "execution_time": frappe.cache().hget('report_execution_time', report.name) or 0
  79. }
  80. @frappe.whitelist()
  81. def background_enqueue_run(report_name, filters=None, user=None):
  82. """run reports in background"""
  83. if not user:
  84. user = frappe.session.user
  85. report = get_report_doc(report_name)
  86. track_instance = \
  87. frappe.get_doc({
  88. "doctype": "Prepared Report",
  89. "report_name": report_name,
  90. # This looks like an insanity but, without this it'd be very hard to find Prepared Reports matching given condition
  91. # We're ensuring that spacing is consistent. e.g. JS seems to put no spaces after ":", Python on the other hand does.
  92. "filters": json.dumps(json.loads(filters)),
  93. "ref_report_doctype": report_name,
  94. "report_type": report.report_type,
  95. "query": report.query,
  96. "module": report.module,
  97. })
  98. track_instance.insert(ignore_permissions=True)
  99. frappe.db.commit()
  100. track_instance.enqueue_report()
  101. return {
  102. "name": track_instance.name,
  103. "redirect_url": get_url_to_form("Prepared Report", track_instance.name)
  104. }
  105. @frappe.whitelist()
  106. def get_script(report_name):
  107. report = get_report_doc(report_name)
  108. module = report.module or frappe.db.get_value("DocType", report.ref_doctype, "module")
  109. module_path = get_module_path(module)
  110. report_folder = os.path.join(module_path, "report", scrub(report.name))
  111. script_path = os.path.join(report_folder, scrub(report.name) + ".js")
  112. print_path = os.path.join(report_folder, scrub(report.name) + ".html")
  113. script = None
  114. if os.path.exists(script_path):
  115. with open(script_path, "r") as f:
  116. script = f.read()
  117. html_format = get_html_format(print_path)
  118. if not script and report.javascript:
  119. script = report.javascript
  120. if not script:
  121. script = "frappe.query_reports['%s']={}" % report_name
  122. # load translations
  123. if frappe.lang != "en":
  124. send_translations(frappe.get_lang_dict("report", report_name))
  125. return {
  126. "script": render_include(script),
  127. "html_format": html_format,
  128. "execution_time": frappe.cache().hget('report_execution_time', report_name) or 0
  129. }
  130. @frappe.whitelist()
  131. @frappe.read_only()
  132. def run(report_name, filters=None, user=None, ignore_prepared_report=False):
  133. report = get_report_doc(report_name)
  134. if not user:
  135. user = frappe.session.user
  136. if not frappe.has_permission(report.ref_doctype, "report"):
  137. frappe.msgprint(_("Must have report permission to access this report."),
  138. raise_exception=True)
  139. result = None
  140. if report.prepared_report and not report.disable_prepared_report and not ignore_prepared_report:
  141. if filters:
  142. if isinstance(filters, string_types):
  143. filters = json.loads(filters)
  144. dn = filters.get("prepared_report_name")
  145. filters.pop("prepared_report_name", None)
  146. else:
  147. dn = ""
  148. result = get_prepared_report_result(report, filters, dn, user)
  149. else:
  150. result = generate_report_result(report, filters, user)
  151. result["add_total_row"] = report.add_total_row and not result.get('skip_total_row', False)
  152. return result
  153. def add_data_to_custom_columns(columns, result):
  154. custom_fields_data = get_data_for_custom_report(columns)
  155. data = []
  156. for row in result:
  157. row_obj = {}
  158. if isinstance(row, tuple):
  159. row = list(row)
  160. if isinstance(row, list):
  161. for idx, column in enumerate(columns):
  162. if column.get('link_field'):
  163. row_obj[column['fieldname']] = None
  164. row.insert(idx, None)
  165. else:
  166. row_obj[column['fieldname']] = row[idx]
  167. data.append(row_obj)
  168. else:
  169. data.append(row)
  170. for row in data:
  171. for column in columns:
  172. if column.get('link_field'):
  173. fieldname = column['fieldname']
  174. key = (column['doctype'], fieldname)
  175. link_field = column['link_field']
  176. row[fieldname] = custom_fields_data.get(key, {}).get(row.get(link_field))
  177. return data
  178. def get_prepared_report_result(report, filters, dn="", user=None):
  179. latest_report_data = {}
  180. doc = None
  181. if dn:
  182. # Get specified dn
  183. doc = frappe.get_doc("Prepared Report", dn)
  184. else:
  185. # Only look for completed prepared reports with given filters.
  186. doc_list = frappe.get_all("Prepared Report",
  187. filters={
  188. "status": "Completed",
  189. "filters": json.dumps(filters),
  190. "owner": user,
  191. "report_name": report.get('custom_report') or report.get('report_name')
  192. },
  193. order_by = 'creation desc'
  194. )
  195. if doc_list:
  196. # Get latest
  197. doc = frappe.get_doc("Prepared Report", doc_list[0])
  198. if doc:
  199. try:
  200. # Prepared Report data is stored in a GZip compressed JSON file
  201. attached_file_name = frappe.db.get_value("File", {"attached_to_doctype": doc.doctype, "attached_to_name":doc.name}, "name")
  202. attached_file = frappe.get_doc('File', attached_file_name)
  203. compressed_content = attached_file.get_content()
  204. uncompressed_content = gzip_decompress(compressed_content)
  205. data = json.loads(uncompressed_content)
  206. if data:
  207. columns = json.loads(doc.columns) if doc.columns else data[0]
  208. for column in columns:
  209. if isinstance(column, dict):
  210. column["label"] = _(column["label"])
  211. latest_report_data = {
  212. "columns": columns,
  213. "result": data
  214. }
  215. except Exception:
  216. frappe.log_error(frappe.get_traceback())
  217. frappe.delete_doc("Prepared Report", doc.name)
  218. frappe.db.commit()
  219. doc = None
  220. latest_report_data.update({
  221. "prepared_report": True,
  222. "doc": doc
  223. })
  224. return latest_report_data
  225. @frappe.whitelist()
  226. def export_query():
  227. """export from query reports"""
  228. data = frappe._dict(frappe.local.form_dict)
  229. del data["cmd"]
  230. if "csrf_token" in data:
  231. del data["csrf_token"]
  232. if isinstance(data.get("filters"), string_types):
  233. filters = json.loads(data["filters"])
  234. if isinstance(data.get("report_name"), string_types):
  235. report_name = data["report_name"]
  236. frappe.permissions.can_export(
  237. frappe.get_cached_value('Report', report_name, 'ref_doctype'),
  238. raise_exception=True
  239. )
  240. if isinstance(data.get("file_format_type"), string_types):
  241. file_format_type = data["file_format_type"]
  242. include_indentation = data["include_indentation"]
  243. if isinstance(data.get("visible_idx"), string_types):
  244. visible_idx = json.loads(data.get("visible_idx"))
  245. else:
  246. visible_idx = None
  247. if file_format_type == "Excel":
  248. data = run(report_name, filters)
  249. data = frappe._dict(data)
  250. if not data.columns:
  251. frappe.respond_as_web_page(_("No data to export"),
  252. _("You can try changing the filters of your report."))
  253. return
  254. columns = get_columns_dict(data.columns)
  255. from frappe.utils.xlsxutils import make_xlsx
  256. xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation)
  257. xlsx_file = make_xlsx(xlsx_data, "Query Report")
  258. frappe.response['filename'] = report_name + '.xlsx'
  259. frappe.response['filecontent'] = xlsx_file.getvalue()
  260. frappe.response['type'] = 'binary'
  261. def build_xlsx_data(columns, data, visible_idx,include_indentation):
  262. result = [[]]
  263. # add column headings
  264. for idx in range(len(data.columns)):
  265. result[0].append(columns[idx]["label"])
  266. # build table from result
  267. for i, row in enumerate(data.result):
  268. # only pick up rows that are visible in the report
  269. if i in visible_idx:
  270. row_data = []
  271. if isinstance(row, dict) and row:
  272. for idx in range(len(data.columns)):
  273. label = columns[idx]["label"]
  274. fieldname = columns[idx]["fieldname"]
  275. cell_value = row.get(fieldname, row.get(label, ""))
  276. if cint(include_indentation) and 'indent' in row and idx == 0:
  277. cell_value = (' ' * cint(row['indent'])) + cell_value
  278. row_data.append(cell_value)
  279. else:
  280. row_data = row
  281. result.append(row_data)
  282. return result
  283. def add_total_row(result, columns, meta = None):
  284. total_row = [""]*len(columns)
  285. has_percent = []
  286. for i, col in enumerate(columns):
  287. fieldtype, options, fieldname = None, None, None
  288. if isinstance(col, string_types):
  289. if meta:
  290. # get fieldtype from the meta
  291. field = meta.get_field(col)
  292. if field:
  293. fieldtype = meta.get_field(col).fieldtype
  294. fieldname = meta.get_field(col).fieldname
  295. else:
  296. col = col.split(":")
  297. if len(col) > 1:
  298. if col[1]:
  299. fieldtype = col[1]
  300. if "/" in fieldtype:
  301. fieldtype, options = fieldtype.split("/")
  302. else:
  303. fieldtype = "Data"
  304. else:
  305. fieldtype = col.get("fieldtype")
  306. fieldname = col.get("fieldname")
  307. options = col.get("options")
  308. for row in result:
  309. if i >= len(row): continue
  310. cell = row.get(fieldname) if isinstance(row, dict) else row[i]
  311. if fieldtype in ["Currency", "Int", "Float", "Percent"] and flt(cell):
  312. total_row[i] = flt(total_row[i]) + flt(cell)
  313. if fieldtype == "Percent" and i not in has_percent:
  314. has_percent.append(i)
  315. if fieldtype == "Time" and cell:
  316. if not total_row[i]:
  317. total_row[i]=timedelta(hours=0,minutes=0,seconds=0)
  318. total_row[i] = total_row[i] + cell
  319. if fieldtype=="Link" and options == "Currency":
  320. total_row[i] = result[0].get(fieldname) if isinstance(result[0], dict) else result[0][i]
  321. for i in has_percent:
  322. total_row[i] = flt(total_row[i]) / len(result)
  323. first_col_fieldtype = None
  324. if isinstance(columns[0], string_types):
  325. first_col = columns[0].split(":")
  326. if len(first_col) > 1:
  327. first_col_fieldtype = first_col[1].split("/")[0]
  328. else:
  329. first_col_fieldtype = columns[0].get("fieldtype")
  330. if first_col_fieldtype not in ["Currency", "Int", "Float", "Percent", "Date"]:
  331. total_row[0] = _("Total")
  332. result.append(total_row)
  333. return result
  334. @frappe.whitelist()
  335. def get_data_for_custom_field(doctype, field):
  336. value_map = frappe._dict(frappe.get_all(doctype,
  337. fields=["name", field],
  338. as_list=1))
  339. return value_map
  340. def get_data_for_custom_report(columns):
  341. doc_field_value_map = {}
  342. for column in columns:
  343. if column.get('link_field'):
  344. fieldname = column.get('fieldname')
  345. doctype = column.get('doctype')
  346. doc_field_value_map[(doctype, fieldname)] = get_data_for_custom_field(doctype, fieldname)
  347. return doc_field_value_map
  348. @frappe.whitelist()
  349. def save_report(reference_report, report_name, columns):
  350. report_doc = get_report_doc(reference_report)
  351. docname = frappe.db.exists("Report",
  352. {'report_name': report_name, 'is_standard': 'No', 'report_type': 'Custom Report'})
  353. if docname:
  354. report = frappe.get_doc("Report", docname)
  355. report.update({"json": columns})
  356. report.save()
  357. frappe.msgprint(_("Report updated successfully"))
  358. return docname
  359. else:
  360. new_report = frappe.get_doc({
  361. 'doctype': 'Report',
  362. 'report_name': report_name,
  363. 'json': columns,
  364. 'ref_doctype': report_doc.ref_doctype,
  365. 'is_standard': 'No',
  366. 'report_type': 'Custom Report',
  367. 'reference_report': reference_report
  368. }).insert(ignore_permissions = True)
  369. frappe.msgprint(_("{0} saved successfully").format(new_report.name))
  370. return new_report.name
  371. def get_filtered_data(ref_doctype, columns, data, user):
  372. result = []
  373. linked_doctypes = get_linked_doctypes(columns, data)
  374. match_filters_per_doctype = get_user_match_filters(linked_doctypes, user=user)
  375. shared = frappe.share.get_shared(ref_doctype, user)
  376. columns_dict = get_columns_dict(columns)
  377. role_permissions = get_role_permissions(frappe.get_meta(ref_doctype), user)
  378. if_owner = role_permissions.get("if_owner", {}).get("report")
  379. if match_filters_per_doctype:
  380. for row in data:
  381. # Why linked_doctypes.get(ref_doctype)? because if column is empty, linked_doctypes[ref_doctype] is removed
  382. if linked_doctypes.get(ref_doctype) and shared and row[linked_doctypes[ref_doctype]] in shared:
  383. result.append(row)
  384. elif has_match(row, linked_doctypes, match_filters_per_doctype, ref_doctype, if_owner, columns_dict, user):
  385. result.append(row)
  386. else:
  387. result = list(data)
  388. return result
  389. def has_match(row, linked_doctypes, doctype_match_filters, ref_doctype, if_owner, columns_dict, user):
  390. """Returns True if after evaluating permissions for each linked doctype
  391. - There is an owner match for the ref_doctype
  392. - `and` There is a user permission match for all linked doctypes
  393. Returns True if the row is empty
  394. Note:
  395. Each doctype could have multiple conflicting user permission doctypes.
  396. Hence even if one of the sets allows a match, it is true.
  397. This behavior is equivalent to the trickling of user permissions of linked doctypes to the ref doctype.
  398. """
  399. resultant_match = True
  400. if not row:
  401. # allow empty rows :)
  402. return resultant_match
  403. for doctype, filter_list in doctype_match_filters.items():
  404. matched_for_doctype = False
  405. if doctype==ref_doctype and if_owner:
  406. idx = linked_doctypes.get("User")
  407. if (idx is not None
  408. and row[idx]==user
  409. and columns_dict[idx]==columns_dict.get("owner")):
  410. # owner match is true
  411. matched_for_doctype = True
  412. if not matched_for_doctype:
  413. for match_filters in filter_list:
  414. match = True
  415. for dt, idx in linked_doctypes.items():
  416. # case handled above
  417. if dt=="User" and columns_dict[idx]==columns_dict.get("owner"):
  418. continue
  419. cell_value = None
  420. if isinstance(row, dict):
  421. cell_value = row.get(idx)
  422. elif isinstance(row, (list, tuple)):
  423. cell_value = row[idx]
  424. if dt in match_filters and cell_value not in match_filters.get(dt) and frappe.db.exists(dt, cell_value):
  425. match = False
  426. break
  427. # each doctype could have multiple conflicting user permission doctypes, hence using OR
  428. # so that even if one of the sets allows a match, it is true
  429. matched_for_doctype = matched_for_doctype or match
  430. if matched_for_doctype:
  431. break
  432. # each doctype's user permissions should match the row! hence using AND
  433. resultant_match = resultant_match and matched_for_doctype
  434. if not resultant_match:
  435. break
  436. return resultant_match
  437. def get_linked_doctypes(columns, data):
  438. linked_doctypes = {}
  439. columns_dict = get_columns_dict(columns)
  440. for idx, col in enumerate(columns):
  441. df = columns_dict[idx]
  442. if df.get("fieldtype")=="Link":
  443. if data and isinstance(data[0], (list, tuple)):
  444. linked_doctypes[df["options"]] = idx
  445. else:
  446. # dict
  447. linked_doctypes[df["options"]] = df["fieldname"]
  448. # remove doctype if column is empty
  449. columns_with_value = []
  450. for row in data:
  451. if row:
  452. if len(row) != len(columns_with_value):
  453. if isinstance(row, (list, tuple)):
  454. row = enumerate(row)
  455. elif isinstance(row, dict):
  456. row = row.items()
  457. for col, val in row:
  458. if val and col not in columns_with_value:
  459. columns_with_value.append(col)
  460. items = list(iteritems(linked_doctypes))
  461. for doctype, key in items:
  462. if key not in columns_with_value:
  463. del linked_doctypes[doctype]
  464. return linked_doctypes
  465. def get_columns_dict(columns):
  466. """Returns a dict with column docfield values as dict
  467. The keys for the dict are both idx and fieldname,
  468. so either index or fieldname can be used to search for a column's docfield properties
  469. """
  470. columns_dict = frappe._dict()
  471. for idx, col in enumerate(columns):
  472. col_dict = frappe._dict()
  473. # string
  474. if isinstance(col, string_types):
  475. col = col.split(":")
  476. if len(col) > 1:
  477. if "/" in col[1]:
  478. col_dict["fieldtype"], col_dict["options"] = col[1].split("/")
  479. else:
  480. col_dict["fieldtype"] = col[1]
  481. col_dict["label"] = col[0]
  482. col_dict["fieldname"] = frappe.scrub(col[0])
  483. # dict
  484. else:
  485. col_dict.update(col)
  486. if "fieldname" not in col_dict:
  487. col_dict["fieldname"] = frappe.scrub(col_dict["label"])
  488. columns_dict[idx] = col_dict
  489. columns_dict[col_dict["fieldname"]] = col_dict
  490. return columns_dict
  491. def get_user_match_filters(doctypes, user):
  492. match_filters = {}
  493. for dt in doctypes:
  494. filter_list = frappe.desk.reportview.build_match_conditions(dt, user, False)
  495. if filter_list:
  496. match_filters[dt] = filter_list
  497. return match_filters