From b60de0db41bbe4af3c2f6e2369eda5d483702d76 Mon Sep 17 00:00:00 2001 From: Jigar Patel Date: Tue, 17 Jul 2018 18:49:05 +0530 Subject: [IMP] import/export UIs Task 40692 Various changes to import/export (mainly) UIs: * default to excel & "full" (non-import-compatible) export * auto-detect encoding of CSV using chardet * remember column -> field mapping after having imported a file (useful for repeated imports where auto-matching failed) * better handle localised booleans & column names * automatically select source list view's fields when exporting * better integrate import templates feature and add a number of templates --- addons/web/controllers/main.py | 33 ++++++----- .../static/src/js/views/list/list_controller.js | 5 +- addons/web/static/src/js/widgets/data_export.js | 65 +++++++++++++++------- addons/web/static/src/scss/data_export.scss | 20 +------ addons/web/static/src/xml/base.xml | 50 +++++++++-------- 5 files changed, 97 insertions(+), 76 deletions(-) (limited to 'addons/web') diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index cd0f2ff0873..bf27842d55e 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1238,8 +1238,8 @@ class Export(http.Controller): :rtype: [(str, str)] """ return [ - {'tag': 'csv', 'label': 'CSV'}, {'tag': 'xls', 'label': 'Excel', 'error': None if xlwt else "XLWT 1.3.0 required"}, + {'tag': 'csv', 'label': 'CSV'}, ] def fields_get(self, model): @@ -1250,24 +1250,29 @@ class Export(http.Controller): @http.route('/web/export/get_fields', type='json', auth="user") def get_fields(self, model, prefix='', parent_name= '', import_compat=True, parent_field_type=None, - exclude=None): + parent_field=None, exclude=None): - if import_compat and parent_field_type == "many2one": - fields = {} + if import_compat and parent_field_type in ['many2one', 'many2many']: + fields = self.fields_get(model) + fields = {k: v for k, v in fields.items() if k in ['id', 'name']} else: fields = self.fields_get(model) - if import_compat: - fields.pop('id', None) - else: + if not import_compat: fields['.id'] = fields.pop('id', {'string': 'ID'}) + else: + fields['id']['string'] = _('External ID') + + if parent_field: + parent_field['string'] = _('External ID') + fields['id'] = parent_field fields_sequence = sorted(fields.items(), - key=lambda field: odoo.tools.ustr(field[1].get('string', ''))) + key=lambda field: (field[0] not in ['id', '.id', 'display_name', 'name'], odoo.tools.ustr(field[1].get('string', '')))) records = [] for field_name, field in fields_sequence: - if import_compat: + if import_compat and not field_name == 'id': if exclude and field_name in exclude: continue if field.get('readonly'): @@ -1279,6 +1284,9 @@ class Export(http.Controller): continue id = prefix + (prefix and '/'or '') + field_name + if field_name == 'name' and import_compat and parent_field_type in ['many2one', 'many2many']: + # Add name field when expand m2o and m2m fields in import-compatible mode + id = prefix name = parent_name + (parent_name and '/' or '') + field['string'] record = {'id': id, 'string': name, 'value': id, 'children': False, @@ -1290,11 +1298,8 @@ class Export(http.Controller): if len(name.split('/')) < 3 and 'relation' in field: ref = field.pop('relation') record['value'] += '/id' - record['params'] = {'model': ref, 'prefix': id, 'name': name} - - if not import_compat or field['type'] == 'one2many': - # m2m field in import_compat is childless - record['children'] = True + record['params'] = {'model': ref, 'prefix': id, 'name': name, 'parent_field': field} + record['children'] = True return records diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index a210c07a885..ce0554a8fe6 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -446,7 +446,10 @@ var ListController = BasicController.extend({ */ _onExportData: function () { var record = this.model.get(this.handle); - new DataExport(this, record).open(); + var defaultExportFields = _.map(this.renderer.columns, function (field) { + return field.attrs.name; + }); + new DataExport(this, record, defaultExportFields).open(); }, /** * Called when the renderer displays an editable row and the user tries to diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js index 8a09f7fa034..30e17604077 100644 --- a/addons/web/static/src/js/widgets/data_export.js +++ b/addons/web/static/src/js/widgets/data_export.js @@ -14,7 +14,7 @@ var _t = core._t; var DataExport = Dialog.extend({ template: 'ExportDialog', events: { - 'click .o_expand_parent': function(e) { + 'click .o_expand': function(e) { this.on_expand_action(this.records[$(e.target).closest('.o_export_tree_item').data('id')]); }, 'click .o_export_tree_item': function(e) { @@ -56,10 +56,19 @@ var DataExport = Dialog.extend({ } } }, - 'dblclick .o_export_tree_item': function(e) { - var $elem = $(e.currentTarget); - $elem.removeClass('o_selected'); - this.add_field($elem.data('id'), $elem.find('.o_tree_column').first().text()); + 'dblclick .o_export_tree_item:not(.haschild)': function(e) { + var self = this; + function addElement(el) { + self.add_field(el.getAttribute('data-id'), el.querySelector('.o_tree_column').textContent); + } + var target = e.currentTarget; + target.classList.remove('o_selected'); + // Add parent fields to export + [].reverse.call($(target).parents('.o_export_tree_item')).each(function () { + addElement(this); + }); + // add field itself + addElement(target); }, 'keydown .o_export_tree_item': function(e) { e.stopPropagation(); @@ -195,7 +204,7 @@ var DataExport = Dialog.extend({ }); }, }, - init: function(parent, record) { + init: function(parent, record, defaultExportFields) { var options = { title: _t("Export Data"), buttons: [ @@ -206,6 +215,7 @@ var DataExport = Dialog.extend({ this._super(parent, options); this.records = {}; this.record = record; + this.defaultExportFields = defaultExportFields; this.exports = new data.DataSetSearch(this, 'ir.exports', this.record.getContext()); this.row_index = 0; @@ -228,13 +238,14 @@ var DataExport = Dialog.extend({ var got_fields = new $.Deferred(); this.$import_compat_radios.change(function(e) { + self.isCompatibleMode = !!$(e.target).val(); self.$('.o_field_tree_structure').remove(); self._rpc({ route: '/web/export/get_fields', params: { model: self.record.model, - import_compat: !!$(e.target).val(), + import_compat: self.isCompatibleMode, }, }) .done(function (records) { @@ -250,6 +261,15 @@ var DataExport = Dialog.extend({ .remove(); got_fields.resolve(); self.on_show_data(records); + // In compatible mode add ID field as first field to export + if (self.isCompatibleMode) { + self.$('.o_fields_list').prepend(new Option(_('External ID'), 'id')); + } + _.each(records, function (record) { + if (_.contains(self.defaultExportFields, record.id)) { + self.add_field(record.id, record.string); + } + }); }); }).eq(0).change(); waitFor.push(got_fields); @@ -262,10 +282,15 @@ var DataExport = Dialog.extend({ self.ids_to_export = false; self.domain = domain; } - self.on_show_domain(); })); - waitFor.push(this.show_exports_list()); + waitFor.push(this.show_exports_list().then(function () { + _.each(self.records, function (record, key) { + if (_.contains(self.defaultExportFields, key)) { + self.add_field(key, record.string); + } + }); + })); return $.when.apply($, waitFor); @@ -274,14 +299,15 @@ var DataExport = Dialog.extend({ _.each(formats, function(format, i) { var $radio = $('', {type: 'radio', value: format.tag, name: 'o_export_format_name'}); - var $label = $('