From 779084991ee95ef210ca76910cce2e6ebf0b2ccc Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Sun, 29 Sep 2019 19:16:12 +0530 Subject: [PATCH] refactor - Parse template and build self.rows and self.columns - Store header_row data in columns along with df and skip_import - Use self.columns and self.rows without passing them explicitly - Remove the ability to edit rows - Show only first 10 rows as preview - Build doc with default values set - Show Dashboard progress when coming back from another view - Better ETA Message inspired from Apple - Action buttons "Export Errored Rows" and "Go to DocType List" - Import status "Imported x out of y records" - Success / Failure column in import log --- .../core/doctype/data_import/importer_new.py | 273 +++++++++--------- .../data_import_beta/data_import_beta.js | 124 +++++--- .../data_import_beta/data_import_beta.py | 34 +-- .../js/frappe/data_import/import_preview.js | 75 ++--- frappe/public/js/frappe/form/dashboard.js | 9 +- 5 files changed, 264 insertions(+), 251 deletions(-) diff --git a/frappe/core/doctype/data_import/importer_new.py b/frappe/core/doctype/data_import/importer_new.py index 934c7b8727..ca56dce16a 100644 --- a/frappe/core/doctype/data_import/importer_new.py +++ b/frappe/core/doctype/data_import/importer_new.py @@ -26,9 +26,7 @@ MAX_ROWS_IN_PREVIEW = 10 class Importer: def __init__(self, doctype, data_import=None, file_path=None, content=None): self.doctype = doctype - self.template_options = frappe._dict( - {"remap_column": {}} - ) + self.template_options = frappe._dict({"remap_column": {}}) if data_import: self.data_import = data_import @@ -42,9 +40,14 @@ class Importer: self.data = None # used to store date formats guessed from data rows per column self._guessed_date_formats = {} + # used to store eta during import self.last_eta = 0 + # used to collect warnings during template parsing + # and show them to user + self.warnings = [] self.meta = frappe.get_meta(doctype) self.prepare_content(file_path, content) + self.parse_data_from_template() def prepare_content(self, file_path, content): if self.data_import: @@ -128,91 +131,89 @@ class Importer: self.header_row = header_row def get_data_for_import_preview(self): - out = self.get_parsed_data_from_template() - - # prepare fields - fields = [] - for df in out.fields: - header_title = df.header_title - skip_import = df.skip_import - if isinstance(df, DocField): - field = df.as_dict() - else: - field = df - field.update({"header_title": header_title, "skip_import": skip_import}) - fields.append(field) - out.fields = fields - + out = frappe._dict() + out.data = list(self.rows) + out.columns = self.columns + out.warnings = self.warnings if len(out.data) > MAX_ROWS_IN_PREVIEW: out.data = out.data[:MAX_ROWS_IN_PREVIEW] out.max_rows_exceeded = True out.max_rows_in_preview = MAX_ROWS_IN_PREVIEW return out - def get_parsed_data_from_template(self): - fields, fields_warnings = self.parse_fields_from_header_row() - formats, formats_warnings = self.parse_formats_from_first_10_rows() - fields, data = self.add_serial_no_column(fields, self.data) + def parse_data_from_template(self): + columns = self.parse_columns_from_header_row() + columns, data = self.add_serial_no_column(columns, self.data) - warnings = fields_warnings + formats_warnings + self.columns = columns + self.rows = data - return frappe._dict( - header_row=self.header_row, fields=fields, data=data, warnings=warnings - ) - - def parse_fields_from_header_row(self): + def parse_columns_from_header_row(self): remap_column = self.template_options.remap_column - fields = [] - warnings = [] + columns = [] df_by_labels_and_fieldnames = self.build_fields_dict_for_column_matching() for i, header_title in enumerate(self.header_row): header_row_index = str(i) column_number = str(i + 1) + skip_import = False fieldname = remap_column.get(header_row_index) + if fieldname and fieldname != "Don't Import": df = df_by_labels_and_fieldnames.get(fieldname) - warnings.append( + self.warnings.append( { "col": column_number, "message": _("Mapping column {0} to field {1}").format( frappe.bold(header_title or "Untitled Column"), frappe.bold(df.label) ), + "type": "info", } ) else: df = df_by_labels_and_fieldnames.get(header_title) if not df: - field = frappe._dict(header_title=header_title, skip_import=True) + skip_import = True else: - field = df - field.header_title = header_title - field.skip_import = False + skip_import = False if fieldname == "Don't Import": - field.skip_import = True - warnings.append( + skip_import = True + self.warnings.append( { "col": column_number, "message": _("Skipping column {0}").format(frappe.bold(header_title)), + "type": "info", } ) elif header_title and not df: - warnings.append( + self.warnings.append( { "col": column_number, "message": _("Cannot match column {0} with any field").format( frappe.bold(header_title) ), + "type": "info", } ) elif not header_title and not df: - warnings.append({"col": column_number, "message": _("Skipping Untitled Column")}) - fields.append(field) + self.warnings.append( + {"col": column_number, "message": _("Skipping Untitled Column"), "type": "info"} + ) - return fields, warnings + columns.append( + frappe._dict( + df=df, + skip_import=skip_import, + header_title=header_title, + column_number=column_number, + index=i, + ) + ) + + return columns def build_fields_dict_for_column_matching(self): """ @@ -301,30 +302,20 @@ class Importer: out.append(df) return out - def parse_formats_from_first_10_rows(self): - """ - Returns a list of column descriptors for columns that might need parsing. - For e.g if it is a Date column return the Date format - [ - [['Data']], - [['Date', '%m/%d/%y']], - [['Currency', '#,###.##']], - ... - ] - """ - formats = [] - return formats, [] + def add_serial_no_column(self, columns, data): + columns_with_serial_no = [ + frappe._dict({"header_title": "Sr. No", "skip_import": True}) + ] + columns - def add_serial_no_column(self, fields, data): - fields_with_serial_no = [ - frappe._dict({"label": "Sr. No", "skip_import": True, "parent": None}) - ] + fields + # update index for each column + for i, col in enumerate(columns_with_serial_no): + col.index = i data_with_serial_no = [] for i, row in enumerate(data): data_with_serial_no.append([self.row_index_map[i] + 1] + row) - return fields_with_serial_no, data_with_serial_no + return columns_with_serial_no, data_with_serial_no def parse_value(self, value, df): # convert boolean values to 0 or 1 @@ -385,24 +376,19 @@ class Importer: frappe.flags.in_import = True frappe.flags.mute_emails = self.data_import.mute_emails - out = self.get_parsed_data_from_template() - fields = out["fields"] - data = out["data"] - warnings = [] - # prepare a map for missing link field values - self.prepare_missing_link_field_values(fields, data) + self.prepare_missing_link_field_values() - # parse import data - payloads = self.get_payloads_for_import(fields, data) - - # collect warnings - for payload in payloads: - warnings += payload.warnings + # parse docs from rows + payloads = self.get_payloads_for_import() + # dont import if there are non-ignorable warnings + warnings = [w for w in self.warnings if w.get("type") != "info"] if warnings: self.data_import.db_set("template_warnings", json.dumps(warnings)) - frappe.publish_realtime("data_import_refresh") + frappe.publish_realtime( + "data_import_refresh", {"data_import": self.data_import.name} + ) return # setup import log @@ -422,7 +408,7 @@ class Importer: imported_rows += log.row_indexes # start import - print("Importing {0} rows...".format(len(data))) + print("Importing {0} rows...".format(len(self.rows))) total_payload_count = len(payloads) batch_size = frappe.conf.data_import_batch_size or 1000 @@ -440,7 +426,12 @@ class Importer: if total_payload_count > 5: frappe.publish_realtime( "data_import_progress", - {"current": current_index, "total": total_payload_count, "skipping": True}, + { + "current": current_index, + "total": total_payload_count, + "skipping": True, + "data_import": self.data_import.name, + }, ) continue @@ -458,6 +449,7 @@ class Importer: "current": current_index, "total": total_payload_count, "docname": doc.name, + "data_import": self.data_import.name, "success": True, "row_indexes": row_indexes, "eta": eta, @@ -496,24 +488,25 @@ class Importer: frappe.flags.in_import = False frappe.flags.mute_emails = False - frappe.publish_realtime("data_import_refresh") + frappe.publish_realtime("data_import_refresh", {"data_import": self.data_import.name}) - def get_payloads_for_import(self, fields, data): + def get_payloads_for_import(self): payloads = [] + # make a copy + data = list(self.rows) while data: - doc, rows, data, warnings = self.parse_next_row_for_import(fields, data) - payloads.append(frappe._dict(doc=doc, rows=rows, warnings=warnings)) + doc, rows, data = self.parse_next_row_for_import(data) + payloads.append(frappe._dict(doc=doc, rows=rows)) return payloads - def parse_next_row_for_import(self, fields, data): + def parse_next_row_for_import(self, data): """ Parses rows that make up a doc. A doc maybe built from a single row or multiple rows. - Returns the doc, rows, data without the rows and warnings. + Returns the doc, rows, and data without the rows. """ doc = {} - warnings = [] mandatory_fields = [] - doctypes = set([df.parent for df in fields if df.parent]) + doctypes = set([col.df.parent for col in self.columns if col.df and col.df.parent]) # first row is included by default first_row = data[0] @@ -524,7 +517,7 @@ class Importer: # subsequent rows either dont have any parent value set # or have the same value as the parent # we include a row if either of conditions match - parent_column_index = self.get_first_parent_column_index(fields) + parent_column_index = self.get_first_parent_column_index() parent_value = first_row[parent_column_index] data_without_first_row = data[1:] for d in data_without_first_row: @@ -537,16 +530,26 @@ class Importer: rows.append(d) def get_column_indexes(doctype): - return [i for i, df in enumerate(fields) if df.parent == doctype] + return [ + col.index + for col in self.columns + if not col.skip_import and col.df and col.df.parent == doctype + ] def validate_value(value, df): - if df.fieldtype == "Select" and value not in df.get_select_options(): - options_string = ", ".join([frappe.bold(d) for d in df.get_select_options()]) - msg = _("Value must be one of {0}").format(options_string) - warnings.append( - {"row": row_number, "field": df.as_dict(convert_dates_to_str=True), "message": msg} - ) - return False + if df.fieldtype == "Select": + select_options = df.get_select_options() + if select_options and value not in select_options: + options_string = ", ".join([frappe.bold(d) for d in select_options]) + msg = _("Value must be one of {0}").format(options_string) + self.warnings.append( + { + "row": row_number, + "field": df.as_dict(convert_dates_to_str=True), + "message": msg, + } + ) + return False elif df.fieldtype == "Link": d = self.get_missing_link_field_values(df.options) @@ -554,7 +557,7 @@ class Importer: msg = _("Value {0} missing for {1}").format( frappe.bold(value), frappe.bold(df.options) ) - warnings.append( + self.warnings.append( { "row": row_number, "field": df.as_dict(convert_dates_to_str=True), @@ -566,19 +569,21 @@ class Importer: return value def parse_doc(doctype, docfields, values, row_number): - doc = {} - for index, (df, value) in enumerate(zip(docfields, values)): - if df.get("skip_import", False): - continue + # new_doc returns a dict with default values set + doc = frappe.new_doc(doctype, as_dict=True) + # remove standard fields and __islocal + for key in frappe.model.default_fields + ('__islocal',): + doc.pop(key, None) + for index, (df, value) in enumerate(zip(docfields, values)): if value in INVALID_VALUES: value = None - if validate_value(value, df): + value = validate_value(value, df) + if value: doc[df.fieldname] = self.parse_value(value, df) check_mandatory_fields(doctype, doc, row_number) - return doc def check_mandatory_fields(doctype, doc, row_number): @@ -591,7 +596,7 @@ class Importer: return if len(fields) == 1: - warnings.append( + self.warnings.append( { "row": row_number, "message": _("{0} is a mandatory field").format(fields[0].label), @@ -599,7 +604,7 @@ class Importer: ) else: fields_string = ", ".join([df.label for df in fields]) - warnings.append( + self.warnings.append( {"row": row_number, "message": _("{0} are mandatory fields").format(fields_string)} ) @@ -619,7 +624,8 @@ class Importer: # skip values if all of them are empty continue - docfields = [fields[i] for i in column_indexes] + columns = [self.columns[i] for i in column_indexes] + docfields = [col.df for col in columns] doc = parse_doc(doctype, docfields, values, row_number) parsed_docs[doctype] = parsed_docs.get(doctype, []) parsed_docs[doctype].append(doc) @@ -635,17 +641,17 @@ class Importer: table_field = table_dfs[0] doc[table_field.fieldname] = docs - return doc, rows, data[len(rows) :], warnings + return doc, rows, data[len(rows) :] - def get_first_parent_column_index(self, fields): + def get_first_parent_column_index(self): """ Returns the first column's index which must be one of the parent columns """ # find a parent column parent_column_index = -1 - for i, df in enumerate(fields): - if not df.get("skip_import", False) and df.parent == self.doctype: - parent_column_index = i + for col in self.columns: + if not col.skip_import and col.df and col.df.parent == self.doctype: + parent_column_index = col.index break return parent_column_index @@ -659,9 +665,11 @@ class Importer: def insert_record(self, doc): self.create_missing_linked_records(doc) + + new_doc = frappe.new_doc(self.doctype) + new_doc.update(doc) # name shouldn't be set when inserting a new record - doc.update({"doctype": self.doctype, "name": None}) - new_doc = frappe.get_doc(doc) + new_doc.set("name", None) new_doc.insert() if self.meta.is_submittable and self.data_import.submit_after_import: new_doc.submit() @@ -673,15 +681,17 @@ class Importer: document automatically if it has only one mandatory field """ link_values = [] + def get_link_fields(doc, doctype): for fieldname, value in doc.items(): meta = frappe.get_meta(doctype) df = meta.get_field(fieldname) - if df.fieldtype == 'Link': + if df.fieldtype == "Link": link_values.append([df.options, value]) elif df.fieldtype in table_fields: for row in value: get_link_fields(row, df.options) + get_link_fields(doc, self.doctype) for link_doctype, link_value in link_values: @@ -701,7 +711,7 @@ class Importer: def update_record(self, doc): id_fieldname = self.get_id_fieldname() id_value = doc[id_fieldname] - existing_doc = frappe.get_doc(self.doctype, {id_fieldname: id_value}) + existing_doc = frappe.get_doc(self.doctype, id_value) existing_doc.flags.via_data_import = self.data_import.name existing_doc.update(doc) existing_doc.save() @@ -723,43 +733,37 @@ class Importer: row_indexes = list(set(row_indexes)) row_indexes.sort() - out = self.get_parsed_data_from_template() - header_row = out["header_row"] - data = out["data"] - + header_row = [col.header_title for col in self.columns[1:]] rows = [header_row] - rows += [row[1:] for row in data if row[0] in row_indexes] + rows += [row[1:] for row in self.rows if row[0] in row_indexes] build_csv_response(rows, self.doctype) def get_missing_link_field_values(self, doctype): return self.missing_link_values.get(doctype, {}) - def prepare_missing_link_field_values(self, fields, data): - link_column_indexes = [i for i, df in enumerate(fields) if df.fieldtype == "Link"] - - def has_one_mandatory_field(doctype): - meta = frappe.get_meta(doctype) - # get mandatory fields with default not set - mandatory_fields = [df for df in meta.fields if df.reqd and not df.default] - mandatory_fields_count = len(mandatory_fields) - if meta.autoname and meta.autoname.lower() == "prompt": - mandatory_fields_count += 1 - return mandatory_fields_count == 1 + def prepare_missing_link_field_values(self): + columns = self.columns + rows = self.rows + link_column_indexes = [ + col.index for col in columns if col.df and col.df.fieldtype == "Link" + ] self.missing_link_values = {} for index in link_column_indexes: - df = fields[index] - column_values = [row[index] for row in data] + col = columns[index] + column_values = [row[index] for row in rows] values = set([v for v in column_values if v not in INVALID_VALUES]) - doctype = df.options + doctype = col.df.options missing_values = [value for value in values if not frappe.db.exists(doctype, value)] if self.missing_link_values.get(doctype): self.missing_link_values[doctype].missing_values += missing_values else: self.missing_link_values[doctype] = frappe._dict( - missing_values=missing_values, one_mandatory=has_one_mandatory_field(doctype), df=df + missing_values=missing_values, + one_mandatory=self.has_one_mandatory_field(doctype), + df=col.df, ) def get_id_fieldname(self): @@ -778,6 +782,15 @@ class Importer: self.last_eta = eta return self.last_eta + def has_one_mandatory_field(self, doctype): + meta = frappe.get_meta(doctype) + # get mandatory fields with default not set + mandatory_fields = [df for df in meta.fields if df.reqd and not df.default] + mandatory_fields_count = len(mandatory_fields) + if meta.autoname and meta.autoname.lower() == "prompt": + mandatory_fields_count += 1 + return mandatory_fields_count == 1 + DATE_FORMATS = [ r"%d-%m-%Y", diff --git a/frappe/core/doctype/data_import_beta/data_import_beta.js b/frappe/core/doctype/data_import_beta/data_import_beta.js index 03086f1a1a..f2ff1eb11b 100644 --- a/frappe/core/doctype/data_import_beta/data_import_beta.js +++ b/frappe/core/doctype/data_import_beta/data_import_beta.js @@ -3,31 +3,40 @@ frappe.ui.form.on('Data Import Beta', { setup(frm) { - frappe.realtime.on('data_import_refresh', () => { + frappe.realtime.on('data_import_refresh', ({ data_import }) => { + if (data_import !== frm.doc.name) return; frappe.model.clear_doc('Data Import Beta', frm.doc.name); frappe.model.with_doc('Data Import Beta', frm.doc.name).then(() => { frm.refresh(); }); }); frappe.realtime.on('data_import_progress', data => { + if (data.data_import !== frm.doc.name) { + return; + } let percent = Math.floor((data.current * 100) / data.total); + let seconds = Math.floor(data.eta); + let minutes = Math.floor(data.eta / 60); let eta_message = - data.eta < 60 - ? __('ETA {0} seconds', [Math.floor(data.eta)]) - : __('ETA {0} minutes', [Math.floor(data.eta / 60)]); + seconds < 60 + ? __('About {0} seconds remaining', [seconds]) + : minutes === 1 + ? __('About {0} minute remaining', [minutes]) + : __('About {0} minutes remaining', [minutes]); + let message; if (data.success) { - let message_args = [data.docname, data.current, data.total]; + let message_args = [data.current, data.total, eta_message]; message = frm.doc.import_type === 'Insert New Records' - ? __('Importing {0} ({1} of {2})', message_args) - : __('Updating {0} ({1} of {2})', message_args); + ? __('Importing {0} of {1}, {2}', message_args) + : __('Updating {0} of {1}, {2}', message_args); } if (data.skipping) { - message = __('Skipping ({1} of {2})', [data.current, data.total]); + message = __('Skipping {0} of {1}, {2}', [data.current, data.total, eta_message]); } frm.dashboard.show_progress(__('Import Progress'), percent, message); - frm.page.set_indicator(eta_message, 'orange'); + frm.page.set_indicator(__('In Progress'), 'orange'); // hide progress when complete if (data.current === data.total) { @@ -59,14 +68,19 @@ frappe.ui.form.on('Data Import Beta', { frm.trigger('show_import_log'); frm.trigger('show_import_warnings'); frm.trigger('toggle_submit_after_import'); + frm.trigger('show_import_status'); - if (frm.doc.import_log && frm.doc.import_log !== '[]') { - frm.disable_save(); + if (frm.doc.status === 'Partial Success') { + frm.add_custom_button(__('Export Errored Rows'), + () => frm.trigger('export_errored_rows')); } - if (frm.doc.status === 'Success') { - frm.events.show_success_message(frm); - } else { + if (frm.doc.status.includes('Success')) { + frm.add_custom_button(__('Go to {0} List', [frm.doc.reference_doctype]), + () => frappe.set_route('List', frm.doc.reference_doctype)); + } + + if (frm.doc.status !== 'Success') { if (!frm.is_new() && frm.doc.import_file) { let label = frm.doc.status === 'Pending' ? __('Start Import') : __('Retry'); frm.page.set_primary_action(label, () => frm.events.start_import(frm)); @@ -74,30 +88,40 @@ frappe.ui.form.on('Data Import Beta', { frm.page.set_primary_action(__('Save'), () => frm.save()); } } - frm.page.set_indicator( - __(frm.doc.status), - frm.doc.status === 'Success' ? 'green' : 'grey' - ); }, - - show_success_message(frm) { + show_import_status(frm) { let import_log = JSON.parse(frm.doc.import_log || '[]'); let successful_records = import_log.filter(log => log.success); - let link = ` - ${__('{0} List', [frm.doc.reference_doctype])} - `; - let message_args = [successful_records.length, link]; + let failed_records = import_log.filter(log => !log.success); + if (successful_records.length === 0) return; + let message; - if (frm.doc.import_type === 'Insert New Records') { - message = - successful_records.length > 1 - ? __('Successfully imported {0} records. Go to {1}', message_args) - : __('Successfully imported {0} record. Go to {1}', message_args); + if (failed_records.length === 0) { + let message_args = [successful_records.length]; + if (frm.doc.import_type === 'Insert New Records') { + message = + successful_records.length > 1 + ? __('Successfully imported {0} records.', message_args) + : __('Successfully imported {0} record.', message_args); + } else { + message = + successful_records.length > 1 + ? __('Successfully updated {0} records.', message_args) + : __('Successfully updated {0} record.', message_args); + } } else { - message = - successful_records.length > 1 - ? __('Successfully updated {0} records. Go to {1}', message_args) - : __('Successfully updated {0} record. Go to {1}', message_args); + let message_args = [successful_records.length, import_log.length]; + if (frm.doc.import_type === 'Insert New Records') { + message = + successful_records.length > 1 + ? __('Successfully imported {0} records out of {1}.', message_args) + : __('Successfully imported {0} record out of {1}.', message_args); + } else { + message = + successful_records.length > 1 + ? __('Successfully updated {0} records out of {1}.', message_args) + : __('Successfully updated {0} record out of {1}.', message_args); + } } frm.dashboard.set_headline(message); }, @@ -196,21 +220,17 @@ frappe.ui.form.on('Data Import Beta', { frm.set_value('template_options', JSON.stringify(template_options)); frm.save().then(() => frm.trigger('import_file')); }, - - export_errored_rows() { - open_url_post('/api/method/frappe.core.doctype.data_import_beta.data_import_beta.download_errored_template', { - data_import_name: frm.doc.name - }); - }, - - show_warnings() { - frm.scroll_to_field('import_warnings'); - } } }); }); }, + export_errored_rows(frm) { + open_url_post('/api/method/frappe.core.doctype.data_import_beta.data_import_beta.download_errored_template', { + data_import_name: frm.doc.name + }); + }, + show_import_warnings(frm, preview_data) { let warnings = JSON.parse(frm.doc.template_warnings || '[]'); warnings = warnings.concat(preview_data.warnings || []); @@ -299,13 +319,13 @@ frappe.ui.form.on('Data Import Beta', { .map(JSON.parse) .map(m => { let title = m.title ? `${m.title}` : ''; - let message = m.message ? `

${m.message}

` : ''; + let message = m.message ? `
${m.message}
` : ''; return title + message; }) .join(''); let id = frappe.dom.get_unique_id(); html = `${messages} -
@@ -314,9 +334,16 @@ frappe.ui.form.on('Data Import Beta', {
`; } + let indicator_color = log.success ? 'green' : 'red'; + let title = log.success ? __('Success') : __('Failure'); return ` ${log.row_indexes.join(', ')} - ${html} + +
${title}
+ + + ${html} + `; }) .join(''); @@ -324,8 +351,9 @@ frappe.ui.form.on('Data Import Beta', { frm.get_field('import_log_preview').$wrapper.html(` - - + + + ${rows}
${__('Row Number')}${__('Message')}${__('Row Number')}${__('Status')}${__('Message')}
diff --git a/frappe/core/doctype/data_import_beta/data_import_beta.py b/frappe/core/doctype/data_import_beta/data_import_beta.py index e169ee06ac..705013d9c5 100644 --- a/frappe/core/doctype/data_import_beta/data_import_beta.py +++ b/frappe/core/doctype/data_import_beta/data_import_beta.py @@ -34,7 +34,9 @@ class DataImportBeta(Document): def start_import(self): if frappe.utils.scheduler.is_scheduler_inactive(): - frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) + frappe.throw( + _("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive") + ) enqueued_jobs = [d.get("job_name") for d in get_info()] @@ -46,37 +48,15 @@ class DataImportBeta(Document): event="data_import", job_name=self.name, data_import=self.name, - now=True + now=True, ) - def get_importer(self): - return Importer(self.reference_doctype, data_import=self) - - def create_missing_link_values(self, missing_link_values): - docs = [] - for d in missing_link_values: - d = frappe._dict(d) - if not d.has_one_mandatory_field: - continue - - doctype = d.doctype - values = d.missing_values - meta = frappe.get_meta(doctype) - # find the autoname field - if meta.autoname and meta.autoname.startswith("field:"): - autoname_field = meta.autoname[len("field:") :] - else: - autoname_field = "name" - - for value in values: - new_doc = frappe.new_doc(doctype) - new_doc.set(autoname_field, value) - docs.append(new_doc.insert()) - return docs - def export_errored_rows(self): return self.get_importer().export_errored_rows() + def get_importer(self): + return Importer(self.reference_doctype, data_import=self) + def start_import(data_import): """This method runs in background job""" diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js index fd512172db..89efb2c69e 100644 --- a/frappe/public/js/frappe/data_import/import_preview.js +++ b/frappe/public/js/frappe/data_import/import_preview.js @@ -27,8 +27,6 @@ frappe.data_import.ImportPreview = class ImportPreview { } refresh() { - this.header_row = this.preview_data.header_row; - this.fields = this.preview_data.fields; this.data = this.preview_data.data; this.make_wrapper(); this.prepare_columns(); @@ -57,62 +55,52 @@ frappe.data_import.ImportPreview = class ImportPreview { } prepare_columns() { - this.columns = this.fields.map((df, i) => { + this.columns = this.preview_data.columns.map((col, i) => { + let df = col.df; let column_width = 120; - let header_row_index = i - 1; - if (df.skip_import) { - let is_sr = df.label === 'Sr. No'; + if (col.header_title === 'Sr. No') { + return { + id: 'srno', + name: 'Sr. No', + content: 'Sr. No', + editable: false, + focusable: false, + align: 'left', + width: 60 + } + } + + if (col.skip_import) { let show_warnings_button = ``; - if (!df.parent) { + if (!col.df) { // increase column width for unidentified columns column_width += 50 } - let column_title = is_sr - ? df.label - : ` - ${df.header_title || `${__('Untitled Column')}`} - ${!df.parent ? show_warnings_button : ''} - `; + let column_title = ` + ${col.header_title || `${__('Untitled Column')}`} + ${!col.df ? show_warnings_button : ''} + `; return { id: frappe.utils.get_random(6), - name: df.label, + name: col.header_title || df.label, content: column_title, skip_import: true, editable: false, focusable: false, align: 'left', - header_row_index, - width: is_sr ? 60 : column_width, - format: (value, row, column, data) => { - let html = `
${value}
`; - if (is_sr && this.is_row_imported(row)) { - html = ` -
${SVG_ICONS['checkbox-circle-line'] + - html}
- `; - } - return html; - } + width: column_width, + format: value => `
${value}
` }; } - let column_title = df.label; - if (this.doctype !== df.parent) { - column_title = `${df.label} (${df.parent})`; - } - let meta = frappe.get_meta(this.doctype); - if (meta.autoname === `field:${df.fieldname}`) { - column_title = `ID (${df.label})`; - } return { id: df.fieldname, - name: column_title, - content: `${df.header_title || df.label}`, + name: col.header_title, + content: `${col.header_title || df.label}`, df: df, editable: false, align: 'left', - header_row_index, width: column_width }; }); @@ -215,11 +203,11 @@ frappe.data_import.ImportPreview = class ImportPreview { } export_errored_rows() { - this.events.export_errored_rows(); + this.frm.trigger('export_errored_rows'); } show_warnings() { - this.events.show_warnings(); + this.frm.scroll_to_field('import_warnings'); } show_column_warning(_, $target) { @@ -234,11 +222,12 @@ frappe.data_import.ImportPreview = class ImportPreview { doctype: this.doctype }); let changed = []; - let fields = this.fields.map((df, i) => { - if (df.label === 'Sr. No') return []; + let fields = this.preview_data.columns.map((col, i) => { + let df = col.df; + if (col.header_title === 'Sr. No') return []; let fieldname; - if (df.skip_import) { + if (!df) { fieldname = null; } else { fieldname = df.parent === this.doctype @@ -249,7 +238,7 @@ frappe.data_import.ImportPreview = class ImportPreview { { label: '', fieldtype: 'Data', - default: df.header_title, + default: col.header_title, fieldname: `Column ${i}`, read_only: 1 }, diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 3e9dacbd05..153bbadc07 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -92,11 +92,14 @@ frappe.ui.form.Dashboard = Class.extend({ show_progress: function(title, percent, message) { this._progress_map = this._progress_map || {}; - if (!this._progress_map[title]) { - const progress_chart = this.add_progress(title, percent, message); + let progress_chart = this._progress_map[title]; + // create a new progress chart if it doesnt exist + // or the previous one got detached from the DOM + if (!progress_chart || progress_chart.parent().length == 0) { + progress_chart = this.add_progress(title, percent, message); this._progress_map[title] = progress_chart; } - let progress_chart = this._progress_map[title]; + if (!$.isArray(percent)) { percent = this.format_percent(title, percent); }