The license.txt file has been replaced with LICENSE for quite a while now. INAL but it didn't seem accurate to say "hey, checkout license.txt although there's no such file". Apart from this, there were inconsistencies in the headers altogether...this change brings consistency.
583 lines
16 KiB
Python
583 lines
16 KiB
Python
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# License: MIT. See LICENSE
|
|
|
|
"""build query for doclistview and return results"""
|
|
|
|
import frappe, json
|
|
import frappe.permissions
|
|
from frappe.model.db_query import DatabaseQuery
|
|
from frappe.model import default_fields, optional_fields
|
|
from frappe import _
|
|
from io import StringIO
|
|
from frappe.core.doctype.access_log.access_log import make_access_log
|
|
from frappe.utils import cstr, format_duration
|
|
from frappe.model.base_document import get_controller
|
|
|
|
|
|
@frappe.whitelist(allow_guest=True)
|
|
@frappe.read_only()
|
|
def get():
|
|
args = get_form_params()
|
|
# If virtual doctype get data from controller het_list method
|
|
if frappe.db.get_value("DocType", filters={"name": args.doctype}, fieldname="is_virtual"):
|
|
controller = get_controller(args.doctype)
|
|
data = compress(controller(args.doctype).get_list(args))
|
|
else:
|
|
data = compress(execute(**args), args=args)
|
|
return data
|
|
|
|
@frappe.whitelist()
|
|
@frappe.read_only()
|
|
def get_list():
|
|
# uncompressed (refactored from frappe.model.db_query.get_list)
|
|
return execute(**get_form_params())
|
|
|
|
@frappe.whitelist()
|
|
@frappe.read_only()
|
|
def get_count():
|
|
args = get_form_params()
|
|
|
|
distinct = 'distinct ' if args.distinct=='true' else ''
|
|
args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"]
|
|
return execute(**args)[0].get('total_count')
|
|
|
|
def execute(doctype, *args, **kwargs):
|
|
return DatabaseQuery(doctype).execute(*args, **kwargs)
|
|
|
|
def get_form_params():
|
|
"""Stringify GET request parameters."""
|
|
data = frappe._dict(frappe.local.form_dict)
|
|
clean_params(data)
|
|
validate_args(data)
|
|
return data
|
|
|
|
def validate_args(data):
|
|
parse_json(data)
|
|
setup_group_by(data)
|
|
|
|
validate_fields(data)
|
|
if data.filters:
|
|
validate_filters(data, data.filters)
|
|
if data.or_filters:
|
|
validate_filters(data, data.or_filters)
|
|
|
|
data.strict = None
|
|
|
|
return data
|
|
|
|
def validate_fields(data):
|
|
wildcard = update_wildcard_field_param(data)
|
|
|
|
for field in data.fields or []:
|
|
fieldname = extract_fieldname(field)
|
|
if is_standard(fieldname):
|
|
continue
|
|
|
|
meta, df = get_meta_and_docfield(fieldname, data)
|
|
|
|
if not df:
|
|
if wildcard:
|
|
continue
|
|
else:
|
|
raise_invalid_field(fieldname)
|
|
|
|
# remove the field from the query if the report hide flag is set and current view is Report
|
|
if df.report_hide and data.view == 'Report':
|
|
data.fields.remove(field)
|
|
continue
|
|
|
|
if df.fieldname in [_df.fieldname for _df in meta.get_high_permlevel_fields()]:
|
|
if df.get('permlevel') not in meta.get_permlevel_access(parenttype=data.doctype):
|
|
data.fields.remove(field)
|
|
|
|
def validate_filters(data, filters):
|
|
if isinstance(filters, list):
|
|
# filters as list
|
|
for condition in filters:
|
|
if len(condition)==3:
|
|
# [fieldname, condition, value]
|
|
fieldname = condition[0]
|
|
if is_standard(fieldname):
|
|
continue
|
|
meta, df = get_meta_and_docfield(fieldname, data)
|
|
if not df:
|
|
raise_invalid_field(condition[0])
|
|
else:
|
|
# [doctype, fieldname, condition, value]
|
|
fieldname = condition[1]
|
|
if is_standard(fieldname):
|
|
continue
|
|
meta = frappe.get_meta(condition[0])
|
|
if not meta.get_field(fieldname):
|
|
raise_invalid_field(fieldname)
|
|
|
|
else:
|
|
for fieldname in filters:
|
|
if is_standard(fieldname):
|
|
continue
|
|
meta, df = get_meta_and_docfield(fieldname, data)
|
|
if not df:
|
|
raise_invalid_field(fieldname)
|
|
|
|
def setup_group_by(data):
|
|
'''Add columns for aggregated values e.g. count(name)'''
|
|
if data.group_by:
|
|
if data.aggregate_function.lower() not in ('count', 'sum', 'avg'):
|
|
frappe.throw(_('Invalid aggregate function'))
|
|
|
|
if frappe.db.has_column(data.aggregate_on_doctype, data.aggregate_on_field):
|
|
data.fields.append('{aggregate_function}(`tab{aggregate_on_doctype}`.`{aggregate_on_field}`) AS _aggregate_column'.format(**data))
|
|
else:
|
|
raise_invalid_field(data.aggregate_on_field)
|
|
|
|
data.pop('aggregate_on_doctype')
|
|
data.pop('aggregate_on_field')
|
|
data.pop('aggregate_function')
|
|
|
|
def raise_invalid_field(fieldname):
|
|
frappe.throw(_('Field not permitted in query') + ': {0}'.format(fieldname), frappe.DataError)
|
|
|
|
def is_standard(fieldname):
|
|
if '.' in fieldname:
|
|
parenttype, fieldname = get_parenttype_and_fieldname(fieldname, None)
|
|
return fieldname in default_fields or fieldname in optional_fields
|
|
|
|
def extract_fieldname(field):
|
|
for text in (',', '/*', '#'):
|
|
if text in field:
|
|
raise_invalid_field(field)
|
|
|
|
fieldname = field
|
|
for sep in (' as ', ' AS '):
|
|
if sep in fieldname:
|
|
fieldname = fieldname.split(sep)[0]
|
|
|
|
# certain functions allowed, extract the fieldname from the function
|
|
if (fieldname.startswith('count(')
|
|
or fieldname.startswith('sum(')
|
|
or fieldname.startswith('avg(')):
|
|
if not fieldname.strip().endswith(')'):
|
|
raise_invalid_field(field)
|
|
fieldname = fieldname.split('(', 1)[1][:-1]
|
|
|
|
return fieldname
|
|
|
|
def get_meta_and_docfield(fieldname, data):
|
|
parenttype, fieldname = get_parenttype_and_fieldname(fieldname, data)
|
|
meta = frappe.get_meta(parenttype)
|
|
df = meta.get_field(fieldname)
|
|
return meta, df
|
|
|
|
def update_wildcard_field_param(data):
|
|
if ((isinstance(data.fields, str) and data.fields == "*")
|
|
or (isinstance(data.fields, (list, tuple)) and len(data.fields) == 1 and data.fields[0] == "*")):
|
|
data.fields = frappe.db.get_table_columns(data.doctype)
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def clean_params(data):
|
|
data.pop('cmd', None)
|
|
data.pop('data', None)
|
|
data.pop('ignore_permissions', None)
|
|
data.pop('view', None)
|
|
data.pop('user', None)
|
|
|
|
if "csrf_token" in data:
|
|
del data["csrf_token"]
|
|
|
|
|
|
def parse_json(data):
|
|
if isinstance(data.get("filters"), str):
|
|
data["filters"] = json.loads(data["filters"])
|
|
if isinstance(data.get("or_filters"), str):
|
|
data["or_filters"] = json.loads(data["or_filters"])
|
|
if isinstance(data.get("fields"), str):
|
|
data["fields"] = json.loads(data["fields"])
|
|
if isinstance(data.get("docstatus"), str):
|
|
data["docstatus"] = json.loads(data["docstatus"])
|
|
if isinstance(data.get("save_user_settings"), str):
|
|
data["save_user_settings"] = json.loads(data["save_user_settings"])
|
|
else:
|
|
data["save_user_settings"] = True
|
|
|
|
|
|
def get_parenttype_and_fieldname(field, data):
|
|
if "." in field:
|
|
parenttype, fieldname = field.split(".")[0][4:-1], field.split(".")[1].strip("`")
|
|
else:
|
|
parenttype = data.doctype
|
|
fieldname = field.strip("`")
|
|
|
|
return parenttype, fieldname
|
|
|
|
def compress(data, args = {}):
|
|
"""separate keys and values"""
|
|
from frappe.desk.query_report import add_total_row
|
|
|
|
if not data: return data
|
|
values = []
|
|
keys = list(data[0])
|
|
for row in data:
|
|
new_row = []
|
|
for key in keys:
|
|
new_row.append(row.get(key))
|
|
values.append(new_row)
|
|
|
|
if args.get("add_total_row"):
|
|
meta = frappe.get_meta(args.doctype)
|
|
values = add_total_row(values, keys, meta)
|
|
|
|
return {
|
|
"keys": keys,
|
|
"values": values
|
|
}
|
|
|
|
@frappe.whitelist()
|
|
def save_report():
|
|
"""save report"""
|
|
|
|
data = frappe.local.form_dict
|
|
if frappe.db.exists('Report', data['name']):
|
|
d = frappe.get_doc('Report', data['name'])
|
|
else:
|
|
d = frappe.new_doc('Report')
|
|
d.report_name = data['name']
|
|
d.ref_doctype = data['doctype']
|
|
|
|
d.report_type = "Report Builder"
|
|
d.json = data['json']
|
|
frappe.get_doc(d).save()
|
|
frappe.msgprint(_("{0} is saved").format(d.name), alert=True)
|
|
return d.name
|
|
|
|
@frappe.whitelist()
|
|
@frappe.read_only()
|
|
def export_query():
|
|
"""export from report builder"""
|
|
title = frappe.form_dict.title
|
|
frappe.form_dict.pop('title', None)
|
|
|
|
form_params = get_form_params()
|
|
form_params["limit_page_length"] = None
|
|
form_params["as_list"] = True
|
|
doctype = form_params.doctype
|
|
add_totals_row = None
|
|
file_format_type = form_params["file_format_type"]
|
|
title = title or doctype
|
|
|
|
del form_params["doctype"]
|
|
del form_params["file_format_type"]
|
|
|
|
if 'add_totals_row' in form_params and form_params['add_totals_row']=='1':
|
|
add_totals_row = 1
|
|
del form_params["add_totals_row"]
|
|
|
|
frappe.permissions.can_export(doctype, raise_exception=True)
|
|
|
|
if 'selected_items' in form_params:
|
|
si = json.loads(frappe.form_dict.get('selected_items'))
|
|
form_params["filters"] = {"name": ("in", si)}
|
|
del form_params["selected_items"]
|
|
|
|
make_access_log(doctype=doctype,
|
|
file_type=file_format_type,
|
|
report_name=form_params.report_name,
|
|
filters=form_params.filters)
|
|
|
|
db_query = DatabaseQuery(doctype)
|
|
ret = db_query.execute(**form_params)
|
|
|
|
if add_totals_row:
|
|
ret = append_totals_row(ret)
|
|
|
|
data = [['Sr'] + get_labels(db_query.fields, doctype)]
|
|
for i, row in enumerate(ret):
|
|
data.append([i+1] + list(row))
|
|
|
|
data = handle_duration_fieldtype_values(doctype, data, db_query.fields)
|
|
|
|
if file_format_type == "CSV":
|
|
|
|
# convert to csv
|
|
import csv
|
|
from frappe.utils.xlsxutils import handle_html
|
|
|
|
f = StringIO()
|
|
writer = csv.writer(f)
|
|
for r in data:
|
|
# encode only unicode type strings and not int, floats etc.
|
|
writer.writerow([handle_html(frappe.as_unicode(v)) \
|
|
if isinstance(v, str) else v for v in r])
|
|
|
|
f.seek(0)
|
|
frappe.response['result'] = cstr(f.read())
|
|
frappe.response['type'] = 'csv'
|
|
frappe.response['doctype'] = title
|
|
|
|
elif file_format_type == "Excel":
|
|
|
|
from frappe.utils.xlsxutils import make_xlsx
|
|
xlsx_file = make_xlsx(data, doctype)
|
|
|
|
frappe.response['filename'] = title + '.xlsx'
|
|
frappe.response['filecontent'] = xlsx_file.getvalue()
|
|
frappe.response['type'] = 'binary'
|
|
|
|
|
|
def append_totals_row(data):
|
|
if not data:
|
|
return data
|
|
data = list(data)
|
|
totals = []
|
|
totals.extend([""]*len(data[0]))
|
|
|
|
for row in data:
|
|
for i in range(len(row)):
|
|
if isinstance(row[i], (float, int)):
|
|
totals[i] = (totals[i] or 0) + row[i]
|
|
|
|
if not isinstance(totals[0], (int, float)):
|
|
totals[0] = 'Total'
|
|
|
|
data.append(totals)
|
|
|
|
return data
|
|
|
|
def get_labels(fields, doctype):
|
|
"""get column labels based on column names"""
|
|
labels = []
|
|
for key in fields:
|
|
key = key.split(" as ")[0]
|
|
|
|
if key.startswith(('count(', 'sum(', 'avg(')): continue
|
|
|
|
if "." in key:
|
|
parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`")
|
|
else:
|
|
parenttype = doctype
|
|
fieldname = fieldname.strip("`")
|
|
|
|
df = frappe.get_meta(parenttype).get_field(fieldname)
|
|
label = df.label if df else fieldname.title()
|
|
if label in labels:
|
|
label = doctype + ": " + label
|
|
labels.append(label)
|
|
|
|
return labels
|
|
|
|
def handle_duration_fieldtype_values(doctype, data, fields):
|
|
for field in fields:
|
|
key = field.split(" as ")[0]
|
|
|
|
if key.startswith(('count(', 'sum(', 'avg(')): continue
|
|
|
|
if "." in key:
|
|
parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`")
|
|
else:
|
|
parenttype = doctype
|
|
fieldname = field.strip("`")
|
|
|
|
df = frappe.get_meta(parenttype).get_field(fieldname)
|
|
|
|
if df and df.fieldtype == 'Duration':
|
|
index = fields.index(field) + 1
|
|
for i in range(1, len(data)):
|
|
val_in_seconds = data[i][index]
|
|
if val_in_seconds:
|
|
duration_val = format_duration(val_in_seconds, df.hide_days)
|
|
data[i][index] = duration_val
|
|
return data
|
|
|
|
@frappe.whitelist()
|
|
def delete_items():
|
|
"""delete selected items"""
|
|
import json
|
|
|
|
items = sorted(json.loads(frappe.form_dict.get('items')), reverse=True)
|
|
doctype = frappe.form_dict.get('doctype')
|
|
|
|
if len(items) > 10:
|
|
frappe.enqueue('frappe.desk.reportview.delete_bulk',
|
|
doctype=doctype, items=items)
|
|
else:
|
|
delete_bulk(doctype, items)
|
|
|
|
def delete_bulk(doctype, items):
|
|
for i, d in enumerate(items):
|
|
try:
|
|
frappe.delete_doc(doctype, d)
|
|
if len(items) >= 5:
|
|
frappe.publish_realtime("progress",
|
|
dict(progress=[i+1, len(items)], title=_('Deleting {0}').format(doctype), description=d),
|
|
user=frappe.session.user)
|
|
# Commit after successful deletion
|
|
frappe.db.commit()
|
|
except Exception:
|
|
# rollback if any record failed to delete
|
|
# if not rollbacked, queries get committed on after_request method in app.py
|
|
frappe.db.rollback()
|
|
|
|
@frappe.whitelist()
|
|
@frappe.read_only()
|
|
def get_sidebar_stats(stats, doctype, filters=[]):
|
|
|
|
return {"stats": get_stats(stats, doctype, filters)}
|
|
|
|
@frappe.whitelist()
|
|
@frappe.read_only()
|
|
def get_stats(stats, doctype, filters=[]):
|
|
"""get tag info"""
|
|
import json
|
|
tags = json.loads(stats)
|
|
if filters:
|
|
filters = json.loads(filters)
|
|
stats = {}
|
|
|
|
try:
|
|
columns = frappe.db.get_table_columns(doctype)
|
|
except (frappe.db.InternalError, frappe.db.ProgrammingError):
|
|
# raised when _user_tags column is added on the fly
|
|
# raised if its a virtual doctype
|
|
columns = []
|
|
|
|
for tag in tags:
|
|
if not tag in columns: continue
|
|
try:
|
|
tag_count = frappe.get_list(doctype,
|
|
fields=[tag, "count(*)"],
|
|
filters=filters + [[tag, '!=', '']],
|
|
group_by=tag,
|
|
as_list=True,
|
|
distinct=1,
|
|
)
|
|
|
|
if tag == '_user_tags':
|
|
stats[tag] = scrub_user_tags(tag_count)
|
|
no_tag_count = frappe.get_list(doctype,
|
|
fields=[tag, "count(*)"],
|
|
filters=filters + [[tag, "in", ('', ',')]],
|
|
as_list=True,
|
|
group_by=tag,
|
|
order_by=tag,
|
|
)
|
|
|
|
no_tag_count = no_tag_count[0][1] if no_tag_count else 0
|
|
|
|
stats[tag].append([_("No Tags"), no_tag_count])
|
|
else:
|
|
stats[tag] = tag_count
|
|
|
|
except frappe.db.SQLError:
|
|
pass
|
|
except frappe.db.InternalError as e:
|
|
# raised when _user_tags column is added on the fly
|
|
pass
|
|
|
|
return stats
|
|
|
|
@frappe.whitelist()
|
|
def get_filter_dashboard_data(stats, doctype, filters=[]):
|
|
"""get tags info"""
|
|
import json
|
|
tags = json.loads(stats)
|
|
if filters:
|
|
filters = json.loads(filters)
|
|
stats = {}
|
|
|
|
columns = frappe.db.get_table_columns(doctype)
|
|
for tag in tags:
|
|
if not tag["name"] in columns: continue
|
|
tagcount = []
|
|
if tag["type"] not in ['Date', 'Datetime']:
|
|
tagcount = frappe.get_list(doctype,
|
|
fields=[tag["name"], "count(*)"],
|
|
filters = filters + ["ifnull(`%s`,'')!=''" % tag["name"]],
|
|
group_by = tag["name"],
|
|
as_list = True)
|
|
|
|
if tag["type"] not in ['Check','Select','Date','Datetime','Int',
|
|
'Float','Currency','Percent'] and tag['name'] not in ['docstatus']:
|
|
stats[tag["name"]] = list(tagcount)
|
|
if stats[tag["name"]]:
|
|
data =["No Data", frappe.get_list(doctype,
|
|
fields=[tag["name"], "count(*)"],
|
|
filters=filters + ["({0} = '' or {0} is null)".format(tag["name"])],
|
|
as_list=True)[0][1]]
|
|
if data and data[1]!=0:
|
|
|
|
stats[tag["name"]].append(data)
|
|
else:
|
|
stats[tag["name"]] = tagcount
|
|
|
|
return stats
|
|
|
|
def scrub_user_tags(tagcount):
|
|
"""rebuild tag list for tags"""
|
|
rdict = {}
|
|
tagdict = dict(tagcount)
|
|
for t in tagdict:
|
|
if not t:
|
|
continue
|
|
alltags = t.split(',')
|
|
for tag in alltags:
|
|
if tag:
|
|
if not tag in rdict:
|
|
rdict[tag] = 0
|
|
|
|
rdict[tag] += tagdict[t]
|
|
|
|
rlist = []
|
|
for tag in rdict:
|
|
rlist.append([tag, rdict[tag]])
|
|
|
|
return rlist
|
|
|
|
# used in building query in queries.py
|
|
def get_match_cond(doctype, as_condition=True):
|
|
cond = DatabaseQuery(doctype).build_match_conditions(as_condition=as_condition)
|
|
if not as_condition:
|
|
return cond
|
|
|
|
return ((' and ' + cond) if cond else "").replace("%", "%%")
|
|
|
|
def build_match_conditions(doctype, user=None, as_condition=True):
|
|
match_conditions = DatabaseQuery(doctype, user=user).build_match_conditions(as_condition=as_condition)
|
|
if as_condition:
|
|
return match_conditions.replace("%", "%%")
|
|
else:
|
|
return match_conditions
|
|
|
|
def get_filters_cond(doctype, filters, conditions, ignore_permissions=None, with_match_conditions=False):
|
|
if isinstance(filters, str):
|
|
filters = json.loads(filters)
|
|
|
|
if filters:
|
|
flt = filters
|
|
if isinstance(filters, dict):
|
|
filters = filters.items()
|
|
flt = []
|
|
for f in filters:
|
|
if isinstance(f[1], str) and f[1][0] == '!':
|
|
flt.append([doctype, f[0], '!=', f[1][1:]])
|
|
elif isinstance(f[1], (list, tuple)) and \
|
|
f[1][0] in (">", "<", ">=", "<=", "!=", "like", "not like", "in", "not in", "between"):
|
|
|
|
flt.append([doctype, f[0], f[1][0], f[1][1]])
|
|
else:
|
|
flt.append([doctype, f[0], '=', f[1]])
|
|
|
|
query = DatabaseQuery(doctype)
|
|
query.filters = flt
|
|
query.conditions = conditions
|
|
|
|
if with_match_conditions:
|
|
query.build_match_conditions()
|
|
|
|
query.build_filter_conditions(flt, conditions, ignore_permissions)
|
|
|
|
cond = ' and ' + ' and '.join(query.conditions)
|
|
else:
|
|
cond = ''
|
|
return cond
|