diff --git a/frappe/config/setup.py b/frappe/config/setup.py index 9c6bc92904..a47bf25ccf 100644 --- a/frappe/config/setup.py +++ b/frappe/config/setup.py @@ -95,9 +95,16 @@ def get_data(): { "type": "doctype", "name": "Data Import", - "label": _("Import / Export Data"), + "label": _("Import Data"), "icon": "octicon octicon-cloud-upload", - "description": _("Import / Export Data from CSV and Excel files.") + "description": _("Import Data from CSV / Excel files.") + }, + { + "type": "doctype", + "name": "Data Export", + "label": _("Export Data"), + "icon": "octicon octicon-cloud-upload", + "description": _("Export Data in CSV / Excel format.") }, { "type": "doctype", diff --git a/frappe/core/doctype/data_export/__init__.py b/frappe/core/doctype/data_export/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/data_export/data_export.js b/frappe/core/doctype/data_export/data_export.js new file mode 100644 index 0000000000..4cbaa81f4e --- /dev/null +++ b/frappe/core/doctype/data_export/data_export.js @@ -0,0 +1,138 @@ +// Copyright (c) 2018, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Data Export', { + refresh: frm => { + frm.disable_save(); + frm.page.set_primary_action('Export', () => { + can_export(frm) ? export_data(frm) : null; + }); + $(frm.footer.wrapper).toggle(false); + }, + onload: (frm) => { + frm.set_query("reference_doctype", () => { + return { + "filters": { + "issingle": 0, + "istable": 0, + "name": ['in', frappe.boot.user.can_export] + } + }; + }); + }, + reference_doctype: frm => { + const doctype = frm.doc.reference_doctype; + if (doctype) { + frappe.model.with_doctype(doctype, () => set_field_options(frm)); + } else { + reset_filter_and_field(frm); + } + } +}); + +const can_export = frm => { + const doctype = frm.doc.reference_doctype; + const parent_multicheck_options = frm.fields_multicheck[doctype] ? + frm.fields_multicheck[doctype].get_checked_options() : []; + let is_valid_form = false; + if (!doctype) { + frappe.msgprint(__('Please select the Document Type.')); + } else if (!parent_multicheck_options.length) { + frappe.msgprint(__('Atleast one field of Parent Document Type is mandatory')); + } else { + is_valid_form = true; + } + return is_valid_form; +}; + +const export_data = frm => { + let get_template_url = '/api/method/frappe.core.doctype.data_export.exporter.export_data'; + var export_params = () => { + let columns = {}; + Object.keys(frm.fields_multicheck).forEach(dt => { + const options = frm.fields_multicheck[dt].get_checked_options(); + columns[dt] = options; + }); + return { + doctype: frm.doc.reference_doctype, + select_columns: JSON.stringify(columns), + filters: frm.filter_list.get_filters().map(filter => filter.slice(1, 4)), + file_type: frm.doc.file_type, + template: true, + with_data: true + }; + }; + + open_url_post(get_template_url, export_params()); +}; + +const reset_filter_and_field = (frm) => { + const parent_wrapper = frm.fields_dict.fields_multicheck.$wrapper; + const filter_wrapper = frm.fields_dict.filter_list.$wrapper; + parent_wrapper.empty(); + filter_wrapper.empty(); + frm.filter_list = []; + frm.fields_multicheck = {}; +}; + +const set_field_options = (frm) => { + const parent_wrapper = frm.fields_dict.fields_multicheck.$wrapper; + const filter_wrapper = frm.fields_dict.filter_list.$wrapper; + const doctype = frm.doc.reference_doctype; + const related_doctypes = get_doctypes(doctype); + + parent_wrapper.empty(); + filter_wrapper.empty(); + + frm.filter_list = new frappe.ui.FilterGroup({ + parent: filter_wrapper, + doctype: doctype, + on_change: () => { }, + }); + + frm.fields_multicheck = {}; + related_doctypes.forEach(dt => { + frm.fields_multicheck[dt] = add_doctype_field_multicheck_control(dt, parent_wrapper); + }); + + frm.refresh(); +}; + +const get_doctypes = parentdt => { + return [parentdt].concat( + frappe.meta.get_table_fields(parentdt).map(df => df.options) + ); +}; + +const add_doctype_field_multicheck_control = (doctype, parent_wrapper) => { + const fields = get_fields(doctype); + + const options = fields + .map(df => { + return { + label: df.label + (df.reqd ? ' (M)' : ''), + value: df.fieldname, + checked: 1 + }; + }); + + const multicheck_control = frappe.ui.form.make_control({ + parent: parent_wrapper, + df: { + "label": doctype, + "fieldname": doctype + '_fields', + "fieldtype": "MultiCheck", + "options": options, + "select_all": options.length > 5, + "columns": 3, + "hidden": 1, + }, + render_input: true + }); + + multicheck_control.refresh_input(); + return multicheck_control; +}; + +const filter_fields = df => frappe.model.is_value_type(df) && !df.hidden; +const get_fields = dt => frappe.meta.get_docfields(dt).filter(filter_fields); \ No newline at end of file diff --git a/frappe/core/doctype/data_export/data_export.json b/frappe/core/doctype/data_export/data_export.json new file mode 100644 index 0000000000..f106a0ce02 --- /dev/null +++ b/frappe/core/doctype/data_export/data_export.json @@ -0,0 +1,250 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2018-03-07 10:09:49.794764", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Select Doctype", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "CSV", + "fieldname": "file_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "File Type", + "length": 0, + "no_copy": 0, + "options": "Excel\nCSV", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "reference_doctype", + "fieldname": "section_break", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "filter_list", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Filter List", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "fields_multicheck", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Fields Multicheck", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 1, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2018-03-21 13:23:05.623052", + "modified_by": "Administrator", + "module": "Core", + "name": "Data Export", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/data_export/data_export.py b/frappe/core/doctype/data_export/data_export.py new file mode 100644 index 0000000000..fb4fae26d5 --- /dev/null +++ b/frappe/core/doctype/data_export/data_export.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class DataExport(Document): + pass diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py new file mode 100644 index 0000000000..98b657d71c --- /dev/null +++ b/frappe/core/doctype/data_export/exporter.py @@ -0,0 +1,340 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals + +import frappe +from frappe import _ +import frappe.permissions +import re, csv, os +from frappe.utils.csvutils import UnicodeWriter +from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint +from frappe.core.doctype.data_import.importer import get_data_keys +from six import string_types + +reflags = { + "I":re.I, + "L":re.L, + "M":re.M, + "U":re.U, + "S":re.S, + "X":re.X, + "D": re.DEBUG +} + +@frappe.whitelist() +def export_data(doctype=None, parent_doctype=None, all_doctypes=True, with_data=False, + select_columns=None, file_type='CSV', template=False, filters=None): + exporter = DataExporter(doctype=doctype, parent_doctype=parent_doctype, all_doctypes=all_doctypes, with_data=with_data, + select_columns=select_columns, file_type=file_type, template=template, filters=filters) + exporter.build_response() + +class DataExporter(): + def __init__(self, doctype=None, parent_doctype=None, all_doctypes=True, with_data=False, + select_columns=None, file_type='CSV', template=False, filters=None): + self.doctype = doctype + self.parent_doctype = parent_doctype + self.all_doctypes = all_doctypes + self.with_data = cint(with_data) + self.select_columns = select_columns + self.file_type = file_type + self.template = template + self.filters = filters + self.data_keys = get_data_keys() + + self.prepare_args() + + def prepare_args(self): + if self.select_columns: + self.select_columns = parse_json(self.select_columns) + if self.filters: + self.filters = parse_json(self.filters) + + self.docs_to_export = {} + if self.doctype: + if isinstance(self.doctype, string_types): + self.doctype = [self.doctype] + + if len(self.doctype) > 1: + self.docs_to_export = self.doctype[1] + self.doctype = self.doctype[0] + + if not self.parent_doctype: + self.parent_doctype = self.doctype + + self.column_start_end = {} + + if self.all_doctypes: + self.child_doctypes = [] + for df in frappe.get_meta(self.doctype).get_table_fields(): + self.child_doctypes.append(dict(doctype=df.options, parentfield=df.fieldname)) + + def build_response(self): + self.writer = UnicodeWriter() + self.name_field = 'parent' if self.parent_doctype != self.doctype else 'name' + + if self.template: + self.add_main_header() + + self.writer.writerow(['']) + self.tablerow = [self.data_keys.doctype, ""] + self.labelrow = [_("Column Labels:"), "ID"] + self.fieldrow = [self.data_keys.columns, self.name_field] + self.mandatoryrow = [_("Mandatory:"), _("Yes")] + self.typerow = [_('Type:'), 'Data (text)'] + self.inforow = [_('Info:'), ''] + self.columns = [self.name_field] + + self.build_field_columns(self.doctype) + + if self.all_doctypes: + for d in self.child_doctypes: + self.append_empty_field_column() + if (self.select_columns and self.select_columns.get(d['doctype'], None)) or not self.select_columns: + # if atleast one column is selected for this doctype + self.build_field_columns(d['doctype'], d['parentfield']) + + self.add_field_headings() + self.add_data() + if self.with_data and not self.data: + frappe.respond_as_web_page(_('No Data'), _('There is no data to be exported'), indicator_color='orange') + return + + if self.file_type == 'Excel': + return self.build_response_as_excel() + else: + # write out response as a type csv + frappe.response['result'] = cstr(self.writer.getvalue()) + frappe.response['type'] = 'csv' + frappe.response['doctype'] = self.doctype + + def add_main_header(self): + self.writer.writerow([_('Data Import Template')]) + self.writer.writerow([self.data_keys.main_table, self.doctype]) + + if self.parent_doctype != self.doctype: + self.writer.writerow([self.data_keys.parent_table, self.parent_doctype]) + else: + self.writer.writerow(['']) + + self.writer.writerow(['']) + self.writer.writerow([_('Notes:')]) + self.writer.writerow([_('Please do not change the template headings.')]) + self.writer.writerow([_('First data column must be blank.')]) + self.writer.writerow([_('If you are uploading new records, leave the "name" (ID) column blank.')]) + self.writer.writerow([_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')]) + self.writer.writerow([_('Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish.')]) + self.writer.writerow([_('For updating, you can update only selective columns.')]) + self.writer.writerow([_('You can only upload upto 5000 records in one go. (may be less in some cases)')]) + if self.name_field == "parent": + self.writer.writerow([_('"Parent" signifies the parent table in which this row must be added')]) + self.writer.writerow([_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')]) + + def build_field_columns(self, dt, parentfield=None): + meta = frappe.get_meta(dt) + + # build list of valid docfields + tablecolumns = [] + for f in frappe.db.sql('desc `tab%s`' % dt): + field = meta.get_field(f[0]) + if field and ((self.select_columns and f[0] in self.select_columns[dt]) or not self.select_columns): + tablecolumns.append(field) + + tablecolumns.sort(key = lambda a: int(a.idx)) + + _column_start_end = frappe._dict(start=0) + + if dt==self.doctype: + _column_start_end = frappe._dict(start=0) + else: + _column_start_end = frappe._dict(start=len(self.columns)) + + self.append_field_column(frappe._dict({ + "fieldname": "name", + "parent": dt, + "label": "ID", + "fieldtype": "Data", + "reqd": 1, + "idx": 0, + "info": _("Leave blank for new records") + }), True) + + for docfield in tablecolumns: + self.append_field_column(docfield, True) + + # all non mandatory fields + for docfield in tablecolumns: + self.append_field_column(docfield, False) + + # if there is one column, add a blank column (?) + if len(self.columns)-_column_start_end.start == 1: + self.append_empty_field_column() + + # append DocType name + self.tablerow[_column_start_end.start + 1] = dt + + if parentfield: + self.tablerow[_column_start_end.start + 2] = parentfield + + _column_start_end.end = len(self.columns) + 1 + + self.column_start_end[(dt, parentfield)] = _column_start_end + + def append_field_column(self, docfield, for_mandatory): + if not docfield: + return + if for_mandatory and not docfield.reqd: + return + if not for_mandatory and docfield.reqd: + return + if docfield.fieldname in ('parenttype', 'trash_reason'): + return + if docfield.hidden: + return + if self.select_columns and docfield.fieldname not in self.select_columns.get(docfield.parent, []): + return + + self.tablerow.append("") + self.fieldrow.append(docfield.fieldname) + self.labelrow.append(_(docfield.label)) + self.mandatoryrow.append(docfield.reqd and 'Yes' or 'No') + self.typerow.append(docfield.fieldtype) + self.inforow.append(self.getinforow(docfield)) + self.columns.append(docfield.fieldname) + + def append_empty_field_column(self): + self.tablerow.append("~") + self.fieldrow.append("~") + self.labelrow.append("") + self.mandatoryrow.append("") + self.typerow.append("") + self.inforow.append("") + self.columns.append("") + + @staticmethod + def getinforow(docfield): + """make info comment for options, links etc.""" + if docfield.fieldtype == 'Select': + if not docfield.options: + return '' + else: + return _("One of") + ': %s' % ', '.join(filter(None, docfield.options.split('\n'))) + elif docfield.fieldtype == 'Link': + return 'Valid %s' % docfield.options + elif docfield.fieldtype == 'Int': + return 'Integer' + elif docfield.fieldtype == "Check": + return "0 or 1" + elif docfield.fieldtype in ["Date", "Datetime"]: + return cstr(frappe.defaults.get_defaults().date_format) + elif hasattr(docfield, "info"): + return docfield.info + else: + return '' + + def add_field_headings(self): + self.writer.writerow(self.tablerow) + self.writer.writerow(self.labelrow) + self.writer.writerow(self.fieldrow) + self.writer.writerow(self.mandatoryrow) + self.writer.writerow(self.typerow) + self.writer.writerow(self.inforow) + if self.template: + self.writer.writerow([self.data_keys.data_separator]) + + def add_data(self): + if self.template and not self.with_data: + return + + frappe.permissions.can_export(self.parent_doctype, raise_exception=True) + + # sort nested set doctypes by `lft asc` + order_by = None + table_columns = frappe.db.get_table_columns(self.parent_doctype) + if 'lft' in table_columns and 'rgt' in table_columns: + order_by = '`tab{doctype}`.`lft` asc'.format(doctype=self.parent_doctype) + # get permitted data only + self.data = frappe.get_list(self.doctype, fields=["*"], filters=self.filters, limit_page_length=None, order_by=order_by) + + for doc in self.data: + op = self.docs_to_export.get("op") + names = self.docs_to_export.get("name") + + if names and op: + if op == '=' and doc.name not in names: + continue + elif op == '!=' and doc.name in names: + continue + elif names: + try: + sflags = self.docs_to_export.get("flags", "I,U").upper() + flags = 0 + for a in re.split('\W+',sflags): + flags = flags | reflags.get(a,0) + + c = re.compile(names, flags) + m = c.match(doc.name) + if not m: + continue + except Exception: + if doc.name not in names: + continue + # add main table + rows = [] + + self.add_data_row(rows, self.doctype, None, doc, 0) + + if self.all_doctypes: + # add child tables + for c in self.child_doctypes: + for ci, child in enumerate(frappe.db.sql("""select * from `tab{0}` + where parent=%s and parentfield=%s order by idx""".format(c['doctype']), + (doc.name, c['parentfield']), as_dict=1)): + self.add_data_row(rows, c['doctype'], c['parentfield'], child, ci) + + for row in rows: + self.writer.writerow(row) + + def add_data_row(self, rows, dt, parentfield, doc, rowidx): + d = doc.copy() + meta = frappe.get_meta(dt) + if self.all_doctypes: + d.name = '"'+ d.name+'"' + + if len(rows) < rowidx + 1: + rows.append([""] * (len(self.columns) + 1)) + row = rows[rowidx] + + _column_start_end = self.column_start_end.get((dt, parentfield)) + + if _column_start_end: + for i, c in enumerate(self.columns[_column_start_end.start:_column_start_end.end]): + df = meta.get_field(c) + fieldtype = df.fieldtype if df else "Data" + value = d.get(c, "") + if value: + if fieldtype == "Date": + value = formatdate(value) + elif fieldtype == "Datetime": + value = format_datetime(value) + + row[_column_start_end.start + i + 1] = value + + def build_response_as_excel(self): + filename = frappe.generate_hash("", 10) + with open(filename, 'wb') as f: + f.write(cstr(self.writer.getvalue()).encode("utf-8")) + f = open(filename) + reader = csv.reader(f) + + from frappe.utils.xlsxutils import make_xlsx + xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else 'Data Export') + + f.close() + os.remove(filename) + + # write out response as a xlsx type + frappe.response['filename'] = self.doctype + '.xlsx' + frappe.response['filecontent'] = xlsx_file.getvalue() + frappe.response['type'] = 'binary' + diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index 02b5adc54e..e03be837ff 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -22,7 +22,7 @@ frappe.ui.form.on('Data Import', { let progress_bar = $(frm.dashboard.progress_area).find(".progress-bar"); if (progress_bar) { $(progress_bar).removeClass("progress-bar-danger").addClass("progress-bar-success progress-bar-striped"); - $(progress_bar).css("width", data.progress+"%"); + $(progress_bar).css("width", data.progress + "%"); } } } @@ -38,10 +38,10 @@ frappe.ui.form.on('Data Import', { if (frm.doc.import_status) { const listview_settings = frappe.listview_settings['Data Import']; const indicator = listview_settings.get_indicator(frm.doc); - + frm.page.set_indicator(indicator[0], indicator[1]); - if (frm.doc.import_status==="In Progress") { + if (frm.doc.import_status === "In Progress") { frm.dashboard.add_progress("Data Import Progress", "0"); frm.set_read_only(); frm.refresh_fields(); @@ -57,14 +57,14 @@ frappe.ui.form.on('Data Import', { frappe.help.show_video("6wiriRKPhmg"); }); - if(frm.doc.reference_doctype && frm.doc.docstatus === 0) { + if (frm.doc.reference_doctype && frm.doc.docstatus === 0) { frm.add_custom_button(__("Download template"), function() { frappe.data_import.download_dialog(frm).show(); }); } if (frm.doc.reference_doctype && frm.doc.import_file && frm.doc.total_rows && - frm.doc.docstatus === 0 && (!frm.doc.import_status || frm.doc.import_status=="Failed")) { + frm.doc.docstatus === 0 && (!frm.doc.import_status || frm.doc.import_status == "Failed")) { frm.page.set_primary_action(__("Start Import"), function() { frappe.call({ method: "frappe.core.doctype.data_import.data_import.import_data", @@ -88,10 +88,6 @@ frappe.ui.form.on('Data Import', { } }, - // import_file: function(frm) { - // frm.save(); - // }, - overwrite: function(frm) { if (frm.doc.overwrite === 0) { frm.doc.only_update = 0; @@ -126,8 +122,11 @@ frappe.ui.form.on('Data Import', { create_log_table: function(frm) { let msg = JSON.parse(frm.doc.log_details); var $log_wrapper = $(frm.fields_dict.import_log.wrapper).empty(); - $(frappe.render_template("log_details", {data: msg.messages, show_only_errors: frm.doc.show_only_errors, - import_status: frm.doc.import_status})).appendTo($log_wrapper); + $(frappe.render_template("log_details", { + data: msg.messages, + import_status: frm.doc.import_status, + show_only_errors: frm.doc.show_only_errors, + })).appendTo($log_wrapper); } }); @@ -146,7 +145,7 @@ frappe.data_import.download_dialog = function(frm) { const get_doctype_checkbox_fields = () => { return dialog.fields.filter(df => df.fieldname.endsWith('_fields')) .map(df => dialog.fields_dict[df.fieldname]); - } + }; const doctype_fields = get_fields(frm.doc.reference_doctype) .map(df => ({ @@ -245,13 +244,13 @@ frappe.data_import.download_dialog = function(frm) { doctype: frm.doc.reference_doctype, parent_doctype: frm.doc.reference_doctype, select_columns: JSON.stringify(columns), - with_data: data.with_data ? 'Yes' : 'No', - all_doctypes: 'Yes', - from_data_import: 'Yes', - excel_format: data.file_type === 'Excel' ? 'Yes' : 'No' + with_data: data.with_data, + all_doctypes: true, + file_type: data.file_type, + template: true }; }; - let get_template_url = '/api/method/frappe.core.doctype.data_import.exporter.get_template'; + let get_template_url = '/api/method/frappe.core.doctype.data_export.exporter.export_data'; open_url_post(get_template_url, export_params()); } else { frappe.msgprint(__("Please select the Document Type.")); diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index 96429a415b..687db5e358 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -99,9 +99,9 @@ def export_json(doctype, path, filters=None, or_filters=None, name=None, order_b def export_csv(doctype, path): - from frappe.core.doctype.data_import.exporter import get_template + from frappe.core.doctype.data_export.exporter import export_data with open(path, "wb") as csvfile: - get_template(doctype=doctype, all_doctypes="Yes", with_data="Yes") + export_data(doctype=doctype, all_doctypes=True, template=True, with_data=True) csvfile.write(frappe.response.result.encode("utf-8")) diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py deleted file mode 100644 index 9a8ca42b24..0000000000 --- a/frappe/core/doctype/data_import/exporter.py +++ /dev/null @@ -1,309 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals - -import frappe, json -from frappe import _ -import frappe.permissions -import re, csv, os -from frappe.utils.csvutils import UnicodeWriter -from frappe.utils import cstr, formatdate, format_datetime -from frappe.core.doctype.data_import.importer import get_data_keys -from six import string_types - -reflags = { - "I":re.I, - "L":re.L, - "M":re.M, - "U":re.U, - "S":re.S, - "X":re.X, - "D": re.DEBUG -} - -@frappe.whitelist() -def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data="No", select_columns=None, - from_data_import="No", excel_format="No"): - all_doctypes = all_doctypes=="Yes" - if select_columns: - select_columns = json.loads(select_columns); - docs_to_export = {} - if doctype: - if isinstance(doctype, string_types): - doctype = [doctype]; - if len(doctype) > 1: - docs_to_export = doctype[1] - doctype = doctype[0] - - if not parent_doctype: - parent_doctype = doctype - - column_start_end = {} - - if all_doctypes: - child_doctypes = [] - for df in frappe.get_meta(doctype).get_table_fields(): - child_doctypes.append(dict(doctype=df.options, parentfield=df.fieldname)) - - def get_data_keys_definition(): - return get_data_keys() - - def add_main_header(): - w.writerow([_('Data Import Template')]) - w.writerow([get_data_keys_definition().main_table, doctype]) - - if parent_doctype != doctype: - w.writerow([get_data_keys_definition().parent_table, parent_doctype]) - else: - w.writerow(['']) - - w.writerow(['']) - w.writerow([_('Notes:')]) - w.writerow([_('Please do not change the template headings.')]) - w.writerow([_('First data column must be blank.')]) - w.writerow([_('If you are uploading new records, leave the "name" (ID) column blank.')]) - w.writerow([_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')]) - w.writerow([_('Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish.')]) - w.writerow([_('For updating, you can update only selective columns.')]) - w.writerow([_('You can only upload upto 5000 records in one go. (may be less in some cases)')]) - if key == "parent": - w.writerow([_('"Parent" signifies the parent table in which this row must be added')]) - w.writerow([_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')]) - - def build_field_columns(dt, parentfield=None): - meta = frappe.get_meta(dt) - - # build list of valid docfields - tablecolumns = [] - for f in frappe.db.sql('desc `tab%s`' % dt): - field = meta.get_field(f[0]) - if field and ((select_columns and f[0] in select_columns[dt]) or not select_columns): - tablecolumns.append(field) - - tablecolumns.sort(key = lambda a: int(a.idx)) - - _column_start_end = frappe._dict(start=0) - - if dt==doctype: - _column_start_end = frappe._dict(start=0) - else: - _column_start_end = frappe._dict(start=len(columns)) - - append_field_column(frappe._dict({ - "fieldname": "name", - "parent": dt, - "label": "ID", - "fieldtype": "Data", - "reqd": 1, - "idx": 0, - "info": _("Leave blank for new records") - }), True) - - for docfield in tablecolumns: - append_field_column(docfield, True) - - # all non mandatory fields - for docfield in tablecolumns: - append_field_column(docfield, False) - - # if there is one column, add a blank column (?) - if len(columns)-_column_start_end.start == 1: - append_empty_field_column() - - # append DocType name - tablerow[_column_start_end.start + 1] = dt - - if parentfield: - tablerow[_column_start_end.start + 2] = parentfield - - _column_start_end.end = len(columns) + 1 - - column_start_end[(dt, parentfield)] = _column_start_end - - def append_field_column(docfield, for_mandatory): - if not docfield: - return - if for_mandatory and not docfield.reqd: - return - if not for_mandatory and docfield.reqd: - return - if docfield.fieldname in ('parenttype', 'trash_reason'): - return - if docfield.hidden: - return - if select_columns and docfield.fieldname not in select_columns.get(docfield.parent, []): - return - - tablerow.append("") - fieldrow.append(docfield.fieldname) - labelrow.append(_(docfield.label)) - mandatoryrow.append(docfield.reqd and 'Yes' or 'No') - typerow.append(docfield.fieldtype) - inforow.append(getinforow(docfield)) - columns.append(docfield.fieldname) - - def append_empty_field_column(): - tablerow.append("~") - fieldrow.append("~") - labelrow.append("") - mandatoryrow.append("") - typerow.append("") - inforow.append("") - columns.append("") - - def getinforow(docfield): - """make info comment for options, links etc.""" - if docfield.fieldtype == 'Select': - if not docfield.options: - return '' - else: - return _("One of") + ': %s' % ', '.join(filter(None, docfield.options.split('\n'))) - elif docfield.fieldtype == 'Link': - return 'Valid %s' % docfield.options - elif docfield.fieldtype == 'Int': - return 'Integer' - elif docfield.fieldtype == "Check": - return "0 or 1" - elif docfield.fieldtype in ["Date", "Datetime"]: - return cstr(frappe.defaults.get_defaults().date_format) - elif hasattr(docfield, "info"): - return docfield.info - else: - return '' - - def add_field_headings(): - w.writerow(tablerow) - w.writerow(labelrow) - w.writerow(fieldrow) - w.writerow(mandatoryrow) - w.writerow(typerow) - w.writerow(inforow) - w.writerow([get_data_keys_definition().data_separator]) - - def add_data(): - def add_data_row(row_group, dt, parentfield, doc, rowidx): - d = doc.copy() - meta = frappe.get_meta(dt) - if all_doctypes: - d.name = '"'+ d.name+'"' - - if len(row_group) < rowidx + 1: - row_group.append([""] * (len(columns) + 1)) - row = row_group[rowidx] - - _column_start_end = column_start_end.get((dt, parentfield)) - - if _column_start_end: - for i, c in enumerate(columns[_column_start_end.start:_column_start_end.end]): - df = meta.get_field(c) - fieldtype = df.fieldtype if df else "Data" - value = d.get(c, "") - if value: - if fieldtype == "Date": - value = formatdate(value) - elif fieldtype == "Datetime": - value = format_datetime(value) - - row[_column_start_end.start + i + 1] = value - - if with_data=='Yes': - frappe.permissions.can_export(parent_doctype, raise_exception=True) - - # sort nested set doctypes by `lft asc` - order_by = None - table_columns = frappe.db.get_table_columns(parent_doctype) - if 'lft' in table_columns and 'rgt' in table_columns: - order_by = '`tab{doctype}`.`lft` asc'.format(doctype=parent_doctype) - - # get permitted data only - data = frappe.get_list(doctype, fields=["*"], limit_page_length=None, order_by=order_by) - - for doc in data: - op = docs_to_export.get("op") - names = docs_to_export.get("name") - - if names and op: - if op == '=' and doc.name not in names: - continue - elif op == '!=' and doc.name in names: - continue - elif names: - try: - sflags = docs_to_export.get("flags", "I,U").upper() - flags = 0 - for a in re.split('\W+',sflags): - flags = flags | reflags.get(a,0) - - c = re.compile(names, flags) - m = c.match(doc.name) - if not m: - continue - except: - if doc.name not in names: - continue - # add main table - row_group = [] - - add_data_row(row_group, doctype, None, doc, 0) - - if all_doctypes: - # add child tables - for c in child_doctypes: - for ci, child in enumerate(frappe.db.sql("""select * from `tab{0}` - where parent=%s and parentfield=%s order by idx""".format(c['doctype']), - (doc.name, c['parentfield']), as_dict=1)): - add_data_row(row_group, c['doctype'], c['parentfield'], child, ci) - - for row in row_group: - w.writerow(row) - - w = UnicodeWriter() - key = 'parent' if parent_doctype != doctype else 'name' - - add_main_header() - - w.writerow(['']) - tablerow = [get_data_keys_definition().doctype, ""] - labelrow = [_("Column Labels:"), "ID"] - fieldrow = [get_data_keys_definition().columns, key] - mandatoryrow = [_("Mandatory:"), _("Yes")] - typerow = [_('Type:'), 'Data (text)'] - inforow = [_('Info:'), ''] - columns = [key] - - build_field_columns(doctype) - - if all_doctypes: - for d in child_doctypes: - append_empty_field_column() - if (select_columns and select_columns.get(d['doctype'], None)) or not select_columns: - # if atleast one column is selected for this doctype - build_field_columns(d['doctype'], d['parentfield']) - - add_field_headings() - add_data() - - if from_data_import == "Yes" and excel_format == "Yes": - filename = frappe.generate_hash("", 10) - with open(filename, 'wb') as f: - f.write(cstr(w.getvalue()).encode("utf-8")) - f = open(filename) - reader = csv.reader(f) - - from frappe.utils.xlsxutils import make_xlsx - xlsx_file = make_xlsx(reader, "Data Import Template") - - f.close() - os.remove(filename) - - # write out response as a xlsx type - frappe.response['filename'] = doctype + '.xlsx' - frappe.response['filecontent'] = xlsx_file.getvalue() - frappe.response['type'] = 'binary' - - else: - # write out response as a type csv - frappe.response['result'] = cstr(w.getvalue()) - frappe.response['type'] = 'csv' - frappe.response['doctype'] = doctype diff --git a/frappe/core/doctype/data_import/test_data_import.py b/frappe/core/doctype/data_import/test_data_import.py index 413d4edcfd..d181d49457 100644 --- a/frappe/core/doctype/data_import/test_data_import.py +++ b/frappe/core/doctype/data_import/test_data_import.py @@ -4,24 +4,24 @@ from __future__ import unicode_literals import frappe, unittest -from frappe.core.doctype.data_import import exporter +from frappe.core.doctype.data_export import exporter from frappe.core.doctype.data_import import importer from frappe.utils.csvutils import read_csv_content class TestDataImport(unittest.TestCase): def test_export(self): - exporter.get_template("User", all_doctypes="No", with_data="No") + exporter.export_data("User", all_doctypes=True, template=True) content = read_csv_content(frappe.response.result) self.assertTrue(content[1][1], "User") def test_export_with_data(self): - exporter.get_template("User", all_doctypes="No", with_data="Yes") + exporter.export_data("User", all_doctypes=True, template=True, with_data=True) content = read_csv_content(frappe.response.result) self.assertTrue(content[1][1], "User") - self.assertTrue("Administrator" in [c[1] for c in content if len(c)>1]) + self.assertTrue('"Administrator"' in [c[1] for c in content if len(c)>1]) def test_export_with_all_doctypes(self): - exporter.get_template("User", all_doctypes="Yes", with_data="Yes") + exporter.export_data("User", all_doctypes="Yes", template=True, with_data=True) content = read_csv_content(frappe.response.result) self.assertTrue(content[1][1], "User") self.assertTrue('"Administrator"' in [c[1] for c in content if len(c)>1]) @@ -33,14 +33,14 @@ class TestDataImport(unittest.TestCase): if frappe.db.exists("Blog Category", "test-category"): frappe.delete_doc("Blog Category", "test-category") - exporter.get_template("Blog Category", all_doctypes="No", with_data="No") + exporter.export_data("Blog Category", all_doctypes=True, template=True) content = read_csv_content(frappe.response.result) content.append(["", "", "test-category", "Test Cateogry"]) importer.upload(content) self.assertTrue(frappe.db.get_value("Blog Category", "test-category", "title"), "Test Category") # export with data - exporter.get_template("Blog Category", all_doctypes="No", with_data="Yes") + exporter.export_data("Blog Category", all_doctypes=True, template=True, with_data=True) content = read_csv_content(frappe.response.result) # overwrite @@ -55,7 +55,7 @@ class TestDataImport(unittest.TestCase): frappe.get_doc({"doctype": "User", "email": user_email, "first_name": "Test Import UserRole"}).insert() - exporter.get_template("Has Role", "User", all_doctypes="No", with_data="No") + exporter.export_data("Has Role", "User", all_doctypes=True, template=True) content = read_csv_content(frappe.response.result) content.append(["", "test_import_userrole@example.com", "Blogger"]) importer.upload(content) @@ -65,7 +65,7 @@ class TestDataImport(unittest.TestCase): self.assertTrue(user.get("roles")[0].role, "Blogger") # overwrite - exporter.get_template("Has Role", "User", all_doctypes="No", with_data="No") + exporter.export_data("Has Role", "User", all_doctypes=True, template=True) content = read_csv_content(frappe.response.result) content.append(["", "test_import_userrole@example.com", "Website Manager"]) importer.upload(content, overwrite=True) @@ -77,7 +77,7 @@ class TestDataImport(unittest.TestCase): def test_import_with_children(self): #pylint: disable=R0201 if frappe.db.exists("Event", "EV00001"): frappe.delete_doc("Event", "EV00001") - exporter.get_template("Event", all_doctypes="Yes", with_data="No") + exporter.export_data("Event", all_doctypes="Yes", template=True) content = read_csv_content(frappe.response.result) content.append([None] * len(content[-2])) @@ -93,7 +93,7 @@ class TestDataImport(unittest.TestCase): if frappe.db.exists("Event", "EV00001"): frappe.delete_doc("Event", "EV00001") - exporter.get_template("Event", all_doctypes="No", with_data="No", from_data_import="Yes", excel_format="Yes") + exporter.export_data("Event", all_doctypes=True, template=True, file_type="Excel") from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file content = read_xlsx_file_from_attached_file(fcontent=frappe.response.filecontent) content.append(["", "EV00001", "_test", "Private", "05-11-2017 13:51:48", "0", "0", "", "1", "blue"]) diff --git a/frappe/public/build.json b/frappe/public/build.json index 04e200dbee..163f08548b 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -97,7 +97,8 @@ "public/less/mobile.less", "public/less/kanban.less", "public/less/controls.less", - "public/less/chat.less" + "public/less/chat.less", + "public/less/filters.less" ], "css/frappe-rtl.css": [ "public/css/bootstrap-rtl.css", diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index d37e93a6b3..9cc5c43fee 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -65,6 +65,7 @@ frappe.ui.Filter = class { this.filter_edit_area.find(".set-filter-and-run").on("click", () => { this.filter_edit_area.removeClass("new-filter"); this.on_change(); + this.update_filter_tag(); }); this.filter_edit_area.find('.condition').change(() => { diff --git a/frappe/public/js/frappe/ui/filters/filters.js b/frappe/public/js/frappe/ui/filters/filters.js deleted file mode 100644 index df161892f7..0000000000 --- a/frappe/public/js/frappe/ui/filters/filters.js +++ /dev/null @@ -1,678 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// MIT License. See license.txt - -frappe.ui.FilterList = Class.extend({ - init: function(opts) { - $.extend(this, opts); - this.filters = []; - this.wrapper = this.parent; - this.stats = []; - this.make(); - this.set_events(); - }, - make: function() { - this.wrapper.find('.show_filters, .filter_area').remove(); - this.wrapper.append(` -