25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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