選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 
 

388 行
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, data_to_be_printed = [], [], None, 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 len(res) > 4:
  78. data_to_be_printed = res[4]
  79. if report.apply_user_permissions and result:
  80. result = get_filtered_data(report.ref_doctype, columns, result, user)
  81. if cint(report.add_total_row) and result:
  82. result = add_total_row(result, columns)
  83. return {
  84. "result": result,
  85. "columns": columns,
  86. "message": message,
  87. "chart": chart,
  88. "data_to_be_printed": data_to_be_printed
  89. }
  90. @frappe.whitelist()
  91. def export_query():
  92. """export from query reports"""
  93. data = frappe._dict(frappe.local.form_dict)
  94. del data["cmd"]
  95. if "csrf_token" in data:
  96. del data["csrf_token"]
  97. if isinstance(data.get("filters"), basestring):
  98. filters = json.loads(data["filters"])
  99. if isinstance(data.get("report_name"), basestring):
  100. report_name = data["report_name"]
  101. if isinstance(data.get("file_format_type"), basestring):
  102. file_format_type = data["file_format_type"]
  103. if isinstance(data.get("visible_idx"), basestring):
  104. visible_idx = json.loads(data.get("visible_idx"))
  105. else:
  106. visible_idx = None
  107. if file_format_type == "Excel":
  108. data = run(report_name, filters)
  109. data = frappe._dict(data)
  110. columns = get_columns_dict(data.columns)
  111. result = [[]]
  112. # add column headings
  113. for idx in range(len(data.columns)):
  114. result[0].append(columns[idx]["label"])
  115. # build table from dict
  116. if isinstance(data.result[0], dict):
  117. for i,row in enumerate(data.result):
  118. # only rows which are visible in the report
  119. if row and (i+1 in visible_idx):
  120. row_list = []
  121. for idx in range(len(data.columns)):
  122. row_list.append(row.get(columns[idx]["fieldname"],""))
  123. result.append(row_list)
  124. elif not row:
  125. result.append([])
  126. else:
  127. result = result + [d for i,d in enumerate(data.result) if (i+1 in visible_idx)]
  128. from frappe.utils.xlsxutils import make_xlsx
  129. xlsx_file = make_xlsx(result, "Query Report")
  130. frappe.response['filename'] = report_name + '.xlsx'
  131. frappe.response['filecontent'] = xlsx_file.getvalue()
  132. frappe.response['type'] = 'binary'
  133. def get_report_module_dotted_path(module, report_name):
  134. return frappe.local.module_app[scrub(module)] + "." + scrub(module) \
  135. + ".report." + scrub(report_name) + "." + scrub(report_name)
  136. def add_total_row(result, columns, meta = None):
  137. total_row = [""]*len(columns)
  138. has_percent = []
  139. for i, col in enumerate(columns):
  140. fieldtype, options = None, None
  141. if isinstance(col, basestring):
  142. if meta:
  143. # get fieldtype from the meta
  144. field = meta.get_field(col)
  145. if field:
  146. fieldtype = meta.get_field(col).fieldtype
  147. else:
  148. col = col.split(":")
  149. if len(col) > 1:
  150. if col[1]:
  151. fieldtype = col[1]
  152. if "/" in fieldtype:
  153. fieldtype, options = fieldtype.split("/")
  154. else:
  155. fieldtype = "Data"
  156. else:
  157. fieldtype = col.get("fieldtype")
  158. options = col.get("options")
  159. for row in result:
  160. if fieldtype in ["Currency", "Int", "Float", "Percent"] and flt(row[i]):
  161. total_row[i] = flt(total_row[i]) + flt(row[i])
  162. if fieldtype == "Percent" and i not in has_percent:
  163. has_percent.append(i)
  164. if fieldtype=="Link" and options == "Currency":
  165. total_row[i] = result[0][i]
  166. for i in has_percent:
  167. total_row[i] = flt(total_row[i]) / len(result)
  168. first_col_fieldtype = None
  169. if isinstance(columns[0], basestring):
  170. first_col = columns[0].split(":")
  171. if len(first_col) > 1:
  172. first_col_fieldtype = first_col[1].split("/")[0]
  173. else:
  174. first_col_fieldtype = columns[0].get("fieldtype")
  175. if first_col_fieldtype not in ["Currency", "Int", "Float", "Percent", "Date"]:
  176. if first_col_fieldtype == "Link":
  177. total_row[0] = "'" + _("Total") + "'"
  178. else:
  179. total_row[0] = _("Total")
  180. result.append(total_row)
  181. return result
  182. def get_filtered_data(ref_doctype, columns, data, user):
  183. result = []
  184. linked_doctypes = get_linked_doctypes(columns, data)
  185. match_filters_per_doctype = get_user_match_filters(linked_doctypes, ref_doctype)
  186. shared = frappe.share.get_shared(ref_doctype, user)
  187. columns_dict = get_columns_dict(columns)
  188. role_permissions = get_role_permissions(frappe.get_meta(ref_doctype), user)
  189. if_owner = role_permissions.get("if_owner", {}).get("report")
  190. if match_filters_per_doctype:
  191. for row in data:
  192. # Why linked_doctypes.get(ref_doctype)? because if column is empty, linked_doctypes[ref_doctype] is removed
  193. if linked_doctypes.get(ref_doctype) and shared and row[linked_doctypes[ref_doctype]] in shared:
  194. result.append(row)
  195. elif has_match(row, linked_doctypes, match_filters_per_doctype, ref_doctype, if_owner, columns_dict, user):
  196. result.append(row)
  197. else:
  198. result = list(data)
  199. return result
  200. def has_match(row, linked_doctypes, doctype_match_filters, ref_doctype, if_owner, columns_dict, user):
  201. """Returns True if after evaluating permissions for each linked doctype
  202. - There is an owner match for the ref_doctype
  203. - `and` There is a user permission match for all linked doctypes
  204. Returns True if the row is empty
  205. Note:
  206. Each doctype could have multiple conflicting user permission doctypes.
  207. Hence even if one of the sets allows a match, it is true.
  208. This behavior is equivalent to the trickling of user permissions of linked doctypes to the ref doctype.
  209. """
  210. resultant_match = True
  211. if not row:
  212. # allow empty rows :)
  213. return resultant_match
  214. for doctype, filter_list in doctype_match_filters.items():
  215. matched_for_doctype = False
  216. if doctype==ref_doctype and if_owner:
  217. idx = linked_doctypes.get("User")
  218. if (idx is not None
  219. and row[idx]==user
  220. and columns_dict[idx]==columns_dict.get("owner")):
  221. # owner match is true
  222. matched_for_doctype = True
  223. if not matched_for_doctype:
  224. for match_filters in filter_list:
  225. match = True
  226. for dt, idx in linked_doctypes.items():
  227. # case handled above
  228. if dt=="User" and columns_dict[idx]==columns_dict.get("owner"):
  229. continue
  230. if dt in match_filters and row[idx] not in match_filters[dt] and frappe.db.exists(dt, row[idx]):
  231. match = False
  232. break
  233. # each doctype could have multiple conflicting user permission doctypes, hence using OR
  234. # so that even if one of the sets allows a match, it is true
  235. matched_for_doctype = matched_for_doctype or match
  236. if matched_for_doctype:
  237. break
  238. # each doctype's user permissions should match the row! hence using AND
  239. resultant_match = resultant_match and matched_for_doctype
  240. if not resultant_match:
  241. break
  242. return resultant_match
  243. def get_linked_doctypes(columns, data):
  244. linked_doctypes = {}
  245. columns_dict = get_columns_dict(columns)
  246. for idx, col in enumerate(columns):
  247. df = columns_dict[idx]
  248. if df.get("fieldtype")=="Link":
  249. if isinstance(col, basestring):
  250. linked_doctypes[df["options"]] = idx
  251. else:
  252. # dict
  253. linked_doctypes[df["options"]] = df["fieldname"]
  254. # remove doctype if column is empty
  255. columns_with_value = []
  256. for row in data:
  257. if row:
  258. if len(row) != len(columns_with_value):
  259. if isinstance(row, (list, tuple)):
  260. row = enumerate(row)
  261. elif isinstance(row, dict):
  262. row = row.items()
  263. for col, val in row:
  264. if val and col not in columns_with_value:
  265. columns_with_value.append(col)
  266. for doctype, key in linked_doctypes.items():
  267. if key not in columns_with_value:
  268. del linked_doctypes[doctype]
  269. return linked_doctypes
  270. def get_columns_dict(columns):
  271. """Returns a dict with column docfield values as dict
  272. The keys for the dict are both idx and fieldname,
  273. so either index or fieldname can be used to search for a column's docfield properties
  274. """
  275. columns_dict = frappe._dict()
  276. for idx, col in enumerate(columns):
  277. col_dict = frappe._dict()
  278. # string
  279. if isinstance(col, basestring):
  280. col = col.split(":")
  281. if len(col) > 1:
  282. if "/" in col[1]:
  283. col_dict["fieldtype"], col_dict["options"] = col[1].split("/")
  284. else:
  285. col_dict["fieldtype"] = col[1]
  286. col_dict["label"] = col[0]
  287. col_dict["fieldname"] = frappe.scrub(col[0])
  288. # dict
  289. else:
  290. col_dict.update(col)
  291. if "fieldname" not in col_dict:
  292. col_dict["fieldname"] = frappe.scrub(col_dict["label"])
  293. columns_dict[idx] = col_dict
  294. columns_dict[col_dict["fieldname"]] = col_dict
  295. return columns_dict
  296. def get_user_match_filters(doctypes, ref_doctype):
  297. match_filters = {}
  298. for dt in doctypes:
  299. filter_list = frappe.desk.reportview.build_match_conditions(dt, False)
  300. if filter_list:
  301. match_filters[dt] = filter_list
  302. return match_filters