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

608 行
16 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. """build query for doclistview and return results"""
  4. import frappe, json
  5. import frappe.permissions
  6. from frappe.model.db_query import DatabaseQuery
  7. from frappe.model import default_fields, optional_fields
  8. from frappe import _
  9. from io import StringIO
  10. from frappe.core.doctype.access_log.access_log import make_access_log
  11. from frappe.utils import cstr, format_duration
  12. from frappe.model.base_document import get_controller
  13. from frappe.utils import add_user_info
  14. @frappe.whitelist()
  15. @frappe.read_only()
  16. def get():
  17. args = get_form_params()
  18. # If virtual doctype get data from controller het_list method
  19. if frappe.db.get_value("DocType", filters={"name": args.doctype}, fieldname="is_virtual"):
  20. controller = get_controller(args.doctype)
  21. data = compress(controller(args.doctype).get_list(args))
  22. else:
  23. data = compress(execute(**args), args=args)
  24. return data
  25. @frappe.whitelist()
  26. @frappe.read_only()
  27. def get_list():
  28. # uncompressed (refactored from frappe.model.db_query.get_list)
  29. return execute(**get_form_params())
  30. @frappe.whitelist()
  31. @frappe.read_only()
  32. def get_count():
  33. args = get_form_params()
  34. distinct = 'distinct ' if args.distinct=='true' else ''
  35. args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"]
  36. return execute(**args)[0].get('total_count')
  37. def execute(doctype, *args, **kwargs):
  38. return DatabaseQuery(doctype).execute(*args, **kwargs)
  39. def get_form_params():
  40. """Stringify GET request parameters."""
  41. data = frappe._dict(frappe.local.form_dict)
  42. clean_params(data)
  43. validate_args(data)
  44. return data
  45. def validate_args(data):
  46. parse_json(data)
  47. setup_group_by(data)
  48. validate_fields(data)
  49. if data.filters:
  50. validate_filters(data, data.filters)
  51. if data.or_filters:
  52. validate_filters(data, data.or_filters)
  53. data.strict = None
  54. return data
  55. def validate_fields(data):
  56. wildcard = update_wildcard_field_param(data)
  57. for field in data.fields or []:
  58. fieldname = extract_fieldname(field)
  59. if is_standard(fieldname):
  60. continue
  61. meta, df = get_meta_and_docfield(fieldname, data)
  62. if not df:
  63. if wildcard:
  64. continue
  65. else:
  66. raise_invalid_field(fieldname)
  67. # remove the field from the query if the report hide flag is set and current view is Report
  68. if df.report_hide and data.view == 'Report':
  69. data.fields.remove(field)
  70. continue
  71. if df.fieldname in [_df.fieldname for _df in meta.get_high_permlevel_fields()]:
  72. if df.get('permlevel') not in meta.get_permlevel_access(parenttype=data.doctype):
  73. data.fields.remove(field)
  74. def validate_filters(data, filters):
  75. if isinstance(filters, list):
  76. # filters as list
  77. for condition in filters:
  78. if len(condition)==3:
  79. # [fieldname, condition, value]
  80. fieldname = condition[0]
  81. if is_standard(fieldname):
  82. continue
  83. meta, df = get_meta_and_docfield(fieldname, data)
  84. if not df:
  85. raise_invalid_field(condition[0])
  86. else:
  87. # [doctype, fieldname, condition, value]
  88. fieldname = condition[1]
  89. if is_standard(fieldname):
  90. continue
  91. meta = frappe.get_meta(condition[0])
  92. if not meta.get_field(fieldname):
  93. raise_invalid_field(fieldname)
  94. else:
  95. for fieldname in filters:
  96. if is_standard(fieldname):
  97. continue
  98. meta, df = get_meta_and_docfield(fieldname, data)
  99. if not df:
  100. raise_invalid_field(fieldname)
  101. def setup_group_by(data):
  102. '''Add columns for aggregated values e.g. count(name)'''
  103. if data.group_by and data.aggregate_function:
  104. if data.aggregate_function.lower() not in ('count', 'sum', 'avg'):
  105. frappe.throw(_('Invalid aggregate function'))
  106. if frappe.db.has_column(data.aggregate_on_doctype, data.aggregate_on_field):
  107. data.fields.append('{aggregate_function}(`tab{aggregate_on_doctype}`.`{aggregate_on_field}`) AS _aggregate_column'.format(**data))
  108. if data.aggregate_on_field:
  109. data.fields.append(f"`tab{data.aggregate_on_doctype}`.`{data.aggregate_on_field}`")
  110. else:
  111. raise_invalid_field(data.aggregate_on_field)
  112. data.pop('aggregate_on_doctype')
  113. data.pop('aggregate_on_field')
  114. data.pop('aggregate_function')
  115. def raise_invalid_field(fieldname):
  116. frappe.throw(_('Field not permitted in query') + ': {0}'.format(fieldname), frappe.DataError)
  117. def is_standard(fieldname):
  118. if '.' in fieldname:
  119. parenttype, fieldname = get_parenttype_and_fieldname(fieldname, None)
  120. return fieldname in default_fields or fieldname in optional_fields
  121. def extract_fieldname(field):
  122. for text in (',', '/*', '#'):
  123. if text in field:
  124. raise_invalid_field(field)
  125. fieldname = field
  126. for sep in (' as ', ' AS '):
  127. if sep in fieldname:
  128. fieldname = fieldname.split(sep)[0]
  129. # certain functions allowed, extract the fieldname from the function
  130. if (fieldname.startswith('count(')
  131. or fieldname.startswith('sum(')
  132. or fieldname.startswith('avg(')):
  133. if not fieldname.strip().endswith(')'):
  134. raise_invalid_field(field)
  135. fieldname = fieldname.split('(', 1)[1][:-1]
  136. return fieldname
  137. def get_meta_and_docfield(fieldname, data):
  138. parenttype, fieldname = get_parenttype_and_fieldname(fieldname, data)
  139. meta = frappe.get_meta(parenttype)
  140. df = meta.get_field(fieldname)
  141. return meta, df
  142. def update_wildcard_field_param(data):
  143. if ((isinstance(data.fields, str) and data.fields == "*")
  144. or (isinstance(data.fields, (list, tuple)) and len(data.fields) == 1 and data.fields[0] == "*")):
  145. data.fields = frappe.db.get_table_columns(data.doctype)
  146. return True
  147. return False
  148. def clean_params(data):
  149. for param in (
  150. "cmd",
  151. "data",
  152. "ignore_permissions",
  153. "view",
  154. "user",
  155. "csrf_token",
  156. "join"
  157. ):
  158. data.pop(param, None)
  159. def parse_json(data):
  160. if isinstance(data.get("filters"), str):
  161. data["filters"] = json.loads(data["filters"])
  162. if isinstance(data.get("or_filters"), str):
  163. data["or_filters"] = json.loads(data["or_filters"])
  164. if isinstance(data.get("fields"), str):
  165. data["fields"] = json.loads(data["fields"])
  166. if isinstance(data.get("docstatus"), str):
  167. data["docstatus"] = json.loads(data["docstatus"])
  168. if isinstance(data.get("save_user_settings"), str):
  169. data["save_user_settings"] = json.loads(data["save_user_settings"])
  170. else:
  171. data["save_user_settings"] = True
  172. def get_parenttype_and_fieldname(field, data):
  173. if "." in field:
  174. parenttype, fieldname = field.split(".")[0][4:-1], field.split(".")[1].strip("`")
  175. else:
  176. parenttype = data.doctype
  177. fieldname = field.strip("`")
  178. return parenttype, fieldname
  179. def compress(data, args=None):
  180. """separate keys and values"""
  181. from frappe.desk.query_report import add_total_row
  182. user_info = {}
  183. if not data: return data
  184. if args is None:
  185. args = {}
  186. values = []
  187. keys = list(data[0])
  188. for row in data:
  189. new_row = []
  190. for key in keys:
  191. new_row.append(row.get(key))
  192. values.append(new_row)
  193. # add user info for assignments (avatar)
  194. if row._assign:
  195. for user in json.loads(row._assign):
  196. add_user_info(user, user_info)
  197. if args.get("add_total_row"):
  198. meta = frappe.get_meta(args.doctype)
  199. values = add_total_row(values, keys, meta)
  200. return {
  201. "keys": keys,
  202. "values": values,
  203. "user_info": user_info
  204. }
  205. @frappe.whitelist()
  206. def save_report():
  207. """save report"""
  208. data = frappe.local.form_dict
  209. if frappe.db.exists('Report', data['name']):
  210. d = frappe.get_doc('Report', data['name'])
  211. else:
  212. d = frappe.new_doc('Report')
  213. d.report_name = data['name']
  214. d.ref_doctype = data['doctype']
  215. d.report_type = "Report Builder"
  216. d.json = data['json']
  217. frappe.get_doc(d).save()
  218. frappe.msgprint(_("{0} is saved").format(d.name), alert=True)
  219. return d.name
  220. @frappe.whitelist()
  221. @frappe.read_only()
  222. def export_query():
  223. """export from report builder"""
  224. title = frappe.form_dict.title
  225. frappe.form_dict.pop('title', None)
  226. form_params = get_form_params()
  227. form_params["limit_page_length"] = None
  228. form_params["as_list"] = True
  229. doctype = form_params.doctype
  230. add_totals_row = None
  231. file_format_type = form_params["file_format_type"]
  232. title = title or doctype
  233. del form_params["doctype"]
  234. del form_params["file_format_type"]
  235. if 'add_totals_row' in form_params and form_params['add_totals_row']=='1':
  236. add_totals_row = 1
  237. del form_params["add_totals_row"]
  238. frappe.permissions.can_export(doctype, raise_exception=True)
  239. if 'selected_items' in form_params:
  240. si = json.loads(frappe.form_dict.get('selected_items'))
  241. form_params["filters"] = {"name": ("in", si)}
  242. del form_params["selected_items"]
  243. make_access_log(doctype=doctype,
  244. file_type=file_format_type,
  245. report_name=form_params.report_name,
  246. filters=form_params.filters)
  247. db_query = DatabaseQuery(doctype)
  248. ret = db_query.execute(**form_params)
  249. if add_totals_row:
  250. ret = append_totals_row(ret)
  251. data = [[_('Sr')] + get_labels(db_query.fields, doctype)]
  252. for i, row in enumerate(ret):
  253. data.append([i+1] + list(row))
  254. data = handle_duration_fieldtype_values(doctype, data, db_query.fields)
  255. if file_format_type == "CSV":
  256. # convert to csv
  257. import csv
  258. from frappe.utils.xlsxutils import handle_html
  259. f = StringIO()
  260. writer = csv.writer(f)
  261. for r in data:
  262. # encode only unicode type strings and not int, floats etc.
  263. writer.writerow([handle_html(frappe.as_unicode(v)) \
  264. if isinstance(v, str) else v for v in r])
  265. f.seek(0)
  266. frappe.response['result'] = cstr(f.read())
  267. frappe.response['type'] = 'csv'
  268. frappe.response['doctype'] = title
  269. elif file_format_type == "Excel":
  270. from frappe.utils.xlsxutils import make_xlsx
  271. xlsx_file = make_xlsx(data, doctype)
  272. frappe.response['filename'] = title + '.xlsx'
  273. frappe.response['filecontent'] = xlsx_file.getvalue()
  274. frappe.response['type'] = 'binary'
  275. def append_totals_row(data):
  276. if not data:
  277. return data
  278. data = list(data)
  279. totals = []
  280. totals.extend([""]*len(data[0]))
  281. for row in data:
  282. for i in range(len(row)):
  283. if isinstance(row[i], (float, int)):
  284. totals[i] = (totals[i] or 0) + row[i]
  285. if not isinstance(totals[0], (int, float)):
  286. totals[0] = 'Total'
  287. data.append(totals)
  288. return data
  289. def get_labels(fields, doctype):
  290. """get column labels based on column names"""
  291. labels = []
  292. for key in fields:
  293. key = key.split(" as ")[0]
  294. if key.startswith(('count(', 'sum(', 'avg(')):
  295. continue
  296. if "." in key:
  297. parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`")
  298. else:
  299. parenttype = doctype
  300. fieldname = fieldname.strip("`")
  301. if parenttype == doctype and fieldname == "name":
  302. label = _("ID", context="Label of name column in report")
  303. else:
  304. df = frappe.get_meta(parenttype).get_field(fieldname)
  305. label = _(df.label if df else fieldname.title())
  306. if parenttype != doctype:
  307. # If the column is from a child table, append the child doctype.
  308. # For example, "Item Code (Sales Invoice Item)".
  309. label += f" ({ _(parenttype) })"
  310. labels.append(label)
  311. return labels
  312. def handle_duration_fieldtype_values(doctype, data, fields):
  313. for field in fields:
  314. key = field.split(" as ")[0]
  315. if key.startswith(('count(', 'sum(', 'avg(')): continue
  316. if "." in key:
  317. parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`")
  318. else:
  319. parenttype = doctype
  320. fieldname = field.strip("`")
  321. df = frappe.get_meta(parenttype).get_field(fieldname)
  322. if df and df.fieldtype == 'Duration':
  323. index = fields.index(field) + 1
  324. for i in range(1, len(data)):
  325. val_in_seconds = data[i][index]
  326. if val_in_seconds:
  327. duration_val = format_duration(val_in_seconds, df.hide_days)
  328. data[i][index] = duration_val
  329. return data
  330. @frappe.whitelist()
  331. def delete_items():
  332. """delete selected items"""
  333. import json
  334. items = sorted(json.loads(frappe.form_dict.get('items')), reverse=True)
  335. doctype = frappe.form_dict.get('doctype')
  336. if len(items) > 10:
  337. frappe.enqueue('frappe.desk.reportview.delete_bulk',
  338. doctype=doctype, items=items)
  339. else:
  340. delete_bulk(doctype, items)
  341. def delete_bulk(doctype, items):
  342. for i, d in enumerate(items):
  343. try:
  344. frappe.delete_doc(doctype, d)
  345. if len(items) >= 5:
  346. frappe.publish_realtime("progress",
  347. dict(progress=[i+1, len(items)], title=_('Deleting {0}').format(doctype), description=d),
  348. user=frappe.session.user)
  349. # Commit after successful deletion
  350. frappe.db.commit()
  351. except Exception:
  352. # rollback if any record failed to delete
  353. # if not rollbacked, queries get committed on after_request method in app.py
  354. frappe.db.rollback()
  355. @frappe.whitelist()
  356. @frappe.read_only()
  357. def get_sidebar_stats(stats, doctype, filters=None):
  358. if filters is None:
  359. filters = []
  360. return {"stats": get_stats(stats, doctype, filters)}
  361. @frappe.whitelist()
  362. @frappe.read_only()
  363. def get_stats(stats, doctype, filters=None):
  364. """get tag info"""
  365. import json
  366. if filters is None:
  367. filters = []
  368. tags = json.loads(stats)
  369. if filters:
  370. filters = json.loads(filters)
  371. stats = {}
  372. try:
  373. columns = frappe.db.get_table_columns(doctype)
  374. except (frappe.db.InternalError, frappe.db.ProgrammingError):
  375. # raised when _user_tags column is added on the fly
  376. # raised if its a virtual doctype
  377. columns = []
  378. for tag in tags:
  379. if not tag in columns: continue
  380. try:
  381. tag_count = frappe.get_list(doctype,
  382. fields=[tag, "count(*)"],
  383. filters=filters + [[tag, '!=', '']],
  384. group_by=tag,
  385. as_list=True,
  386. distinct=1,
  387. )
  388. if tag == '_user_tags':
  389. stats[tag] = scrub_user_tags(tag_count)
  390. no_tag_count = frappe.get_list(doctype,
  391. fields=[tag, "count(*)"],
  392. filters=filters + [[tag, "in", ('', ',')]],
  393. as_list=True,
  394. group_by=tag,
  395. order_by=tag,
  396. )
  397. no_tag_count = no_tag_count[0][1] if no_tag_count else 0
  398. stats[tag].append([_("No Tags"), no_tag_count])
  399. else:
  400. stats[tag] = tag_count
  401. except frappe.db.SQLError:
  402. pass
  403. except frappe.db.InternalError as e:
  404. # raised when _user_tags column is added on the fly
  405. pass
  406. return stats
  407. @frappe.whitelist()
  408. def get_filter_dashboard_data(stats, doctype, filters=None):
  409. """get tags info"""
  410. import json
  411. tags = json.loads(stats)
  412. filters = json.loads(filters or [])
  413. stats = {}
  414. columns = frappe.db.get_table_columns(doctype)
  415. for tag in tags:
  416. if not tag["name"] in columns: continue
  417. tagcount = []
  418. if tag["type"] not in ['Date', 'Datetime']:
  419. tagcount = frappe.get_list(doctype,
  420. fields=[tag["name"], "count(*)"],
  421. filters = filters + ["ifnull(`%s`,'')!=''" % tag["name"]],
  422. group_by = tag["name"],
  423. as_list = True)
  424. if tag["type"] not in ['Check','Select','Date','Datetime','Int',
  425. 'Float','Currency','Percent'] and tag['name'] not in ['docstatus']:
  426. stats[tag["name"]] = list(tagcount)
  427. if stats[tag["name"]]:
  428. data =["No Data", frappe.get_list(doctype,
  429. fields=[tag["name"], "count(*)"],
  430. filters=filters + ["({0} = '' or {0} is null)".format(tag["name"])],
  431. as_list=True)[0][1]]
  432. if data and data[1]!=0:
  433. stats[tag["name"]].append(data)
  434. else:
  435. stats[tag["name"]] = tagcount
  436. return stats
  437. def scrub_user_tags(tagcount):
  438. """rebuild tag list for tags"""
  439. rdict = {}
  440. tagdict = dict(tagcount)
  441. for t in tagdict:
  442. if not t:
  443. continue
  444. alltags = t.split(',')
  445. for tag in alltags:
  446. if tag:
  447. if not tag in rdict:
  448. rdict[tag] = 0
  449. rdict[tag] += tagdict[t]
  450. rlist = []
  451. for tag in rdict:
  452. rlist.append([tag, rdict[tag]])
  453. return rlist
  454. # used in building query in queries.py
  455. def get_match_cond(doctype, as_condition=True):
  456. cond = DatabaseQuery(doctype).build_match_conditions(as_condition=as_condition)
  457. if not as_condition:
  458. return cond
  459. return ((' and ' + cond) if cond else "").replace("%", "%%")
  460. def build_match_conditions(doctype, user=None, as_condition=True):
  461. match_conditions = DatabaseQuery(doctype, user=user).build_match_conditions(as_condition=as_condition)
  462. if as_condition:
  463. return match_conditions.replace("%", "%%")
  464. else:
  465. return match_conditions
  466. def get_filters_cond(doctype, filters, conditions, ignore_permissions=None, with_match_conditions=False):
  467. if isinstance(filters, str):
  468. filters = json.loads(filters)
  469. if filters:
  470. flt = filters
  471. if isinstance(filters, dict):
  472. filters = filters.items()
  473. flt = []
  474. for f in filters:
  475. if isinstance(f[1], str) and f[1][0] == '!':
  476. flt.append([doctype, f[0], '!=', f[1][1:]])
  477. elif isinstance(f[1], (list, tuple)) and \
  478. f[1][0] in (">", "<", ">=", "<=", "!=", "like", "not like", "in", "not in", "between"):
  479. flt.append([doctype, f[0], f[1][0], f[1][1]])
  480. else:
  481. flt.append([doctype, f[0], '=', f[1]])
  482. query = DatabaseQuery(doctype)
  483. query.filters = flt
  484. query.conditions = conditions
  485. if with_match_conditions:
  486. query.build_match_conditions()
  487. query.build_filter_conditions(flt, conditions, ignore_permissions)
  488. cond = ' and ' + ' and '.join(query.conditions)
  489. else:
  490. cond = ''
  491. return cond