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.
 
 
 
 
 
 

385 rader
11 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
  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. def get_report_doc(report_name):
  14. doc = frappe.get_doc("Report", report_name)
  15. if not doc.is_permitted():
  16. frappe.throw(_("You don't have access to Report: {0}").format(report_name), frappe.PermissionError)
  17. if not frappe.has_permission(doc.ref_doctype, "report"):
  18. frappe.throw(_("You don't have permission to get a report on: {0}").format(doc.ref_doctype),
  19. frappe.PermissionError)
  20. if doc.disabled:
  21. frappe.throw(_("Report {0} is disabled").format(report_name))
  22. return doc
  23. @frappe.whitelist()
  24. def get_script(report_name):
  25. report = get_report_doc(report_name)
  26. module = report.module or frappe.db.get_value("DocType", report.ref_doctype, "module")
  27. module_path = get_module_path(module)
  28. report_folder = os.path.join(module_path, "report", scrub(report.name))
  29. script_path = os.path.join(report_folder, scrub(report.name) + ".js")
  30. print_path = os.path.join(report_folder, scrub(report.name) + ".html")
  31. script = None
  32. if os.path.exists(script_path):
  33. with open(script_path, "r") as f:
  34. script = f.read()
  35. html_format = get_html_format(print_path)
  36. if not script and report.javascript:
  37. script = report.javascript
  38. if not script:
  39. script = "frappe.query_reports['%s']={}" % report_name
  40. # load translations
  41. if frappe.lang != "en":
  42. send_translations(frappe.get_lang_dict("report", report_name))
  43. return {
  44. "script": render_include(script),
  45. "html_format": html_format
  46. }
  47. @frappe.whitelist()
  48. def run(report_name, filters=None, user=None):
  49. report = get_report_doc(report_name)
  50. if not user:
  51. user = frappe.session.user
  52. if not filters:
  53. filters = []
  54. if filters and isinstance(filters, basestring):
  55. filters = json.loads(filters)
  56. if not frappe.has_permission(report.ref_doctype, "report"):
  57. frappe.msgprint(_("Must have report permission to access this report."),
  58. raise_exception=True)
  59. columns, result, message, chart = [], [], None, None
  60. if report.report_type=="Query Report":
  61. if not report.query:
  62. frappe.msgprint(_("Must specify a Query to run"), raise_exception=True)
  63. if not report.query.lower().startswith("select"):
  64. frappe.msgprint(_("Query must be a SELECT"), raise_exception=True)
  65. result = [list(t) for t in frappe.db.sql(report.query, filters)]
  66. columns = [cstr(c[0]) for c in frappe.db.get_description()]
  67. else:
  68. module = report.module or frappe.db.get_value("DocType", report.ref_doctype, "module")
  69. if report.is_standard=="Yes":
  70. method_name = get_report_module_dotted_path(module, report.name) + ".execute"
  71. res = frappe.get_attr(method_name)(frappe._dict(filters))
  72. columns, result = res[0], res[1]
  73. if len(res) > 2:
  74. message = res[2]
  75. if len(res) > 3:
  76. chart = res[3]
  77. if report.apply_user_permissions and result:
  78. result = get_filtered_data(report.ref_doctype, columns, result, user)
  79. if cint(report.add_total_row) and result:
  80. result = add_total_row(result, columns)
  81. return {
  82. "result": result,
  83. "columns": columns,
  84. "message": message,
  85. "chart": chart
  86. }
  87. @frappe.whitelist()
  88. def export_query():
  89. """export from query reports"""
  90. data = frappe._dict(frappe.local.form_dict)
  91. del data["cmd"]
  92. if "csrf_token" in data:
  93. del data["csrf_token"]
  94. if isinstance(data.get("filters"), basestring):
  95. filters = json.loads(data["filters"])
  96. if isinstance(data.get("report_name"), basestring):
  97. report_name = data["report_name"]
  98. if isinstance(data.get("file_format_type"), basestring):
  99. file_format_type = data["file_format_type"]
  100. if isinstance(data.get("visible_idx"), basestring):
  101. visible_idx = json.loads(data.get("visible_idx"))
  102. else:
  103. visible_idx = None
  104. if file_format_type == "Excel":
  105. data = run(report_name, filters)
  106. data = frappe._dict(data)
  107. columns = get_columns_dict(data.columns)
  108. result = [[]]
  109. # add column headings
  110. for idx in range(len(data.columns)):
  111. result[0].append(columns[idx]["label"])
  112. # build table from dict
  113. if isinstance(data.result[0], dict):
  114. for i,row in enumerate(data.result):
  115. # only rows which are visible in the report
  116. if row and (i+1 in visible_idx):
  117. row_list = []
  118. for idx in range(len(data.columns)):
  119. row_list.append(row.get(columns[idx]["fieldname"],""))
  120. result.append(row_list)
  121. elif not row:
  122. result.append([])
  123. else:
  124. result = result + [d for i,d in enumerate(data.result) if (i+1 in visible_idx)]
  125. from frappe.utils.xlsxutils import make_xlsx
  126. xlsx_file = make_xlsx(result, "Query Report")
  127. frappe.response['filename'] = report_name + '.xlsx'
  128. frappe.response['filecontent'] = xlsx_file.getvalue()
  129. frappe.response['type'] = 'binary'
  130. def get_report_module_dotted_path(module, report_name):
  131. return frappe.local.module_app[scrub(module)] + "." + scrub(module) \
  132. + ".report." + scrub(report_name) + "." + scrub(report_name)
  133. def add_total_row(result, columns, meta = None):
  134. total_row = [""]*len(columns)
  135. has_percent = []
  136. for i, col in enumerate(columns):
  137. fieldtype, options = None, None
  138. if isinstance(col, basestring):
  139. if meta:
  140. # get fieldtype from the meta
  141. field = meta.get_field(col)
  142. if field:
  143. fieldtype = meta.get_field(col).fieldtype
  144. else:
  145. col = col.split(":")
  146. if len(col) > 1:
  147. if col[1]:
  148. fieldtype = col[1]
  149. if "/" in fieldtype:
  150. fieldtype, options = fieldtype.split("/")
  151. else:
  152. fieldtype = "Data"
  153. else:
  154. fieldtype = col.get("fieldtype")
  155. options = col.get("options")
  156. for row in result:
  157. if fieldtype in ["Currency", "Int", "Float", "Percent"] and flt(row[i]):
  158. total_row[i] = flt(total_row[i]) + flt(row[i])
  159. if fieldtype == "Percent" and i not in has_percent:
  160. has_percent.append(i)
  161. if fieldtype=="Link" and options == "Currency":
  162. total_row[i] = result[0][i]
  163. for i in has_percent:
  164. total_row[i] = total_row[i] / len(result)
  165. first_col_fieldtype = None
  166. if isinstance(columns[0], basestring):
  167. first_col = columns[0].split(":")
  168. if len(first_col) > 1:
  169. first_col_fieldtype = first_col[1].split("/")[0]
  170. else:
  171. first_col_fieldtype = columns[0].get("fieldtype")
  172. if first_col_fieldtype not in ["Currency", "Int", "Float", "Percent", "Date"]:
  173. if first_col_fieldtype == "Link":
  174. total_row[0] = "'" + _("Total") + "'"
  175. else:
  176. total_row[0] = _("Total")
  177. result.append(total_row)
  178. return result
  179. def get_filtered_data(ref_doctype, columns, data, user):
  180. result = []
  181. linked_doctypes = get_linked_doctypes(columns, data)
  182. match_filters_per_doctype = get_user_match_filters(linked_doctypes, ref_doctype)
  183. shared = frappe.share.get_shared(ref_doctype, user)
  184. columns_dict = get_columns_dict(columns)
  185. role_permissions = get_role_permissions(frappe.get_meta(ref_doctype), user)
  186. if_owner = role_permissions.get("if_owner", {}).get("report")
  187. if match_filters_per_doctype:
  188. for row in data:
  189. # Why linked_doctypes.get(ref_doctype)? because if column is empty, linked_doctypes[ref_doctype] is removed
  190. if linked_doctypes.get(ref_doctype) and shared and row[linked_doctypes[ref_doctype]] in shared:
  191. result.append(row)
  192. elif has_match(row, linked_doctypes, match_filters_per_doctype, ref_doctype, if_owner, columns_dict, user):
  193. result.append(row)
  194. else:
  195. result = list(data)
  196. return result
  197. def has_match(row, linked_doctypes, doctype_match_filters, ref_doctype, if_owner, columns_dict, user):
  198. """Returns True if after evaluating permissions for each linked doctype
  199. - There is an owner match for the ref_doctype
  200. - `and` There is a user permission match for all linked doctypes
  201. Returns True if the row is empty
  202. Note:
  203. Each doctype could have multiple conflicting user permission doctypes.
  204. Hence even if one of the sets allows a match, it is true.
  205. This behavior is equivalent to the trickling of user permissions of linked doctypes to the ref doctype.
  206. """
  207. resultant_match = True
  208. if not row:
  209. # allow empty rows :)
  210. return resultant_match
  211. for doctype, filter_list in doctype_match_filters.items():
  212. matched_for_doctype = False
  213. if doctype==ref_doctype and if_owner:
  214. idx = linked_doctypes.get("User")
  215. if (idx is not None
  216. and row[idx]==user
  217. and columns_dict[idx]==columns_dict.get("owner")):
  218. # owner match is true
  219. matched_for_doctype = True
  220. if not matched_for_doctype:
  221. for match_filters in filter_list:
  222. match = True
  223. for dt, idx in linked_doctypes.items():
  224. # case handled above
  225. if dt=="User" and columns_dict[idx]==columns_dict.get("owner"):
  226. continue
  227. if dt in match_filters and row[idx] not in match_filters[dt] and frappe.db.exists(dt, row[idx]):
  228. match = False
  229. break
  230. # each doctype could have multiple conflicting user permission doctypes, hence using OR
  231. # so that even if one of the sets allows a match, it is true
  232. matched_for_doctype = matched_for_doctype or match
  233. if matched_for_doctype:
  234. break
  235. # each doctype's user permissions should match the row! hence using AND
  236. resultant_match = resultant_match and matched_for_doctype
  237. if not resultant_match:
  238. break
  239. return resultant_match
  240. def get_linked_doctypes(columns, data):
  241. linked_doctypes = {}
  242. columns_dict = get_columns_dict(columns)
  243. for idx, col in enumerate(columns):
  244. df = columns_dict[idx]
  245. if df.get("fieldtype")=="Link":
  246. if isinstance(col, basestring):
  247. linked_doctypes[df["options"]] = idx
  248. else:
  249. # dict
  250. linked_doctypes[df["options"]] = df["fieldname"]
  251. # remove doctype if column is empty
  252. columns_with_value = []
  253. for row in data:
  254. if row:
  255. if len(row) != len(columns_with_value):
  256. if isinstance(row, (list, tuple)):
  257. row = enumerate(row)
  258. elif isinstance(row, dict):
  259. row = row.items()
  260. for col, val in row:
  261. if val and col not in columns_with_value:
  262. columns_with_value.append(col)
  263. for doctype, key in linked_doctypes.items():
  264. if key not in columns_with_value:
  265. del linked_doctypes[doctype]
  266. return linked_doctypes
  267. def get_columns_dict(columns):
  268. """Returns a dict with column docfield values as dict
  269. The keys for the dict are both idx and fieldname,
  270. so either index or fieldname can be used to search for a column's docfield properties
  271. """
  272. columns_dict = frappe._dict()
  273. for idx, col in enumerate(columns):
  274. col_dict = frappe._dict()
  275. # string
  276. if isinstance(col, basestring):
  277. col = col.split(":")
  278. if len(col) > 1:
  279. if "/" in col[1]:
  280. col_dict["fieldtype"], col_dict["options"] = col[1].split("/")
  281. else:
  282. col_dict["fieldtype"] = col[1]
  283. col_dict["label"] = col[0]
  284. col_dict["fieldname"] = frappe.scrub(col[0])
  285. # dict
  286. else:
  287. col_dict.update(col)
  288. if "fieldname" not in col_dict:
  289. col_dict["fieldname"] = frappe.scrub(col_dict["label"])
  290. columns_dict[idx] = col_dict
  291. columns_dict[col_dict["fieldname"]] = col_dict
  292. return columns_dict
  293. def get_user_match_filters(doctypes, ref_doctype):
  294. match_filters = {}
  295. for dt in doctypes:
  296. filter_list = frappe.desk.reportview.build_match_conditions(dt, False)
  297. if filter_list:
  298. match_filters[dt] = filter_list
  299. return match_filters