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.
 
 
 
 
 
 

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