diff options
Diffstat (limited to 'addons/base_import/static/src/js/base_import.js')
| -rw-r--r-- | addons/base_import/static/src/js/base_import.js | 654 |
1 files changed, 654 insertions, 0 deletions
diff --git a/addons/base_import/static/src/js/base_import.js b/addons/base_import/static/src/js/base_import.js new file mode 100644 index 00000000000..d12e9292fde --- /dev/null +++ b/addons/base_import/static/src/js/base_import.js @@ -0,0 +1,654 @@ +odoo.define('base_import.import', function (require) { +"use strict"; + +var ControlPanelMixin = require('web.ControlPanelMixin'); +var core = require('web.core'); +var time = require('web.time'); +var ListView = require('web.ListView'); +var KanbanView = require('web_kanban.KanbanView'); +var Model = require('web.DataModel'); +var session = require('web.session'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; +var _t = core._t; +var _lt = core._lt; +var StateMachine = window.StateMachine; + +/** + * Safari does not deal well at all with raw JSON data being + * returned. As a result, we're going to cheat by using a + * pseudo-jsonp: instead of getting JSON data in the iframe, we're + * getting a ``script`` tag which consists of a function call and + * the returned data (the json dump). + * + * The function is an auto-generated name bound to ``window``, + * which calls back into the callback provided here. + * + * @param {Object} form the form element (DOM or jQuery) to use in the call + * @param {Object} attributes jquery.form attributes object + * @param {Function} callback function to call with the returned data + */ +function jsonp(form, attributes, callback) { + attributes = attributes || {}; + var options = {jsonp: _.uniqueId('import_callback_')}; + window[options.jsonp] = function () { + delete window[options.jsonp]; + callback.apply(null, arguments); + }; + if ('data' in attributes) { + _.extend(attributes.data, options); + } else { + _.extend(attributes, {data: options}); + } + _.extend(attributes, { + dataType: 'script', + }); + $(form).ajaxSubmit(attributes); +} + +function execute_import_action () { + var self = this; + this.do_action({ + type: 'ir.actions.client', + tag: 'import', + params: { + model: this.dataset.model, + // this.dataset.get_context() could be a compound? + // not sure. action's context should be evaluated + // so safer bet. Odd that timezone & al in it + // though + context: this.getParent().action.context, + } + }, { + on_reverse_breadcrumb: function () { + return self.reload(); + }, + }); + return false; +} + +// if true, the 'Import', 'Export', etc... buttons will be shown +ListView.prototype.defaults.import_enabled = true; +ListView.include({ + /** + * Extend the render_buttons function of ListView by adding an event listener + * on the import button. + * @return {jQuery} the rendered buttons + */ + render_buttons: function() { + var self = this; + var add_button = false; + if (!this.$buttons) { // Ensures that this is only done once + add_button = true; + } + this._super.apply(this, arguments); // Sets this.$buttons + if(add_button) { + this.$buttons.on('click', '.o_button_import', execute_import_action.bind(this)); + } + } +}); + +KanbanView.include({ + /** + * Extend the render_buttons function of KanbanView by adding an event listener + * on the import button. + * @return {jQuery} the rendered buttons + */ + render_buttons: function() { + var self = this; + var add_button = false; + if (!this.$buttons) { // Ensures that this is only done once + add_button = true; + } + this._super.apply(this, arguments); // Sets this.$buttons + if(add_button && this.$buttons) { + this.$buttons.on('click', '.o_button_import', execute_import_action.bind(this)); + } + }, + + set_default_options: function (options) { + this._super(_.defaults(options || {}, { + import_enabled: true, + })); + }, +}); + +var DataImport = Widget.extend(ControlPanelMixin, { + template: 'ImportView', + opts: [ + {name: 'encoding', label: _lt("Encoding:"), value: 'utf-8'}, + {name: 'separator', label: _lt("Separator:"), value: ','}, + {name: 'quoting', label: _lt("Quoting:"), value: '"'} + ], + parse_opts: [ + {name: 'date_format', label: _lt("Date Format:"), value: ''}, + {name: 'float_thousand_separator', label: _lt("Thousand Separator:"), value: ','}, + {name: 'float_decimal_separator', label: _lt("Decimal Separator:"), value: '.'} + ], + events: { + // 'change .oe_import_grid input': 'import_dryrun', + 'change .oe_import_file': 'loaded_file', + 'click .oe_import_file_reload': 'loaded_file', + 'change input.oe_import_has_header, .js_import_options input': 'settings_changed', + 'change input.oe_import_advanced_mode': function (e) { + this.do_not_change_match = true; + this['settings_changed'](); + }, + 'click a.oe_import_toggle': function (e) { + e.preventDefault(); + this.$('.oe_import_options').toggle(); + }, + 'click .oe_import_report a.oe_import_report_count': function (e) { + e.preventDefault(); + $(e.target).parent().toggleClass('oe_import_report_showmore'); + }, + 'click .oe_import_moreinfo_action a': function (e) { + e.preventDefault(); + // #data will parse the attribute on its own, we don't like + // that sort of things + var action = JSON.parse($(e.target).attr('data-action')); + // FIXME: when JS-side clean_action + action.views = _(action.views).map(function (view) { + var id = view[0], type = view[1]; + return [ + id, + type !== 'tree' ? type + : action.view_type === 'form' ? 'list' + : 'tree' + ]; + }); + this.do_action(_.extend(action, { + target: 'new', + flags: { + search_view: true, + display_title: true, + pager: true, + list: {selectable: false} + } + })); + }, + }, + init: function (parent, action) { + this._super.apply(this, arguments); + this.action_manager = parent; + this.res_model = action.params.model; + this.parent_context = action.params.context || {}; + // import object id + this.id = null; + this.Import = new Model('base_import.import'); + this.session = session; + action.display_name = _t('Import a File'); // Displayed in the breadcrumbs + this.do_not_change_match = false; + }, + start: function () { + var self = this; + this.setup_encoding_picker(); + this.setup_separator_picker(); + this.setup_float_format_picker(); + + return $.when( + this._super(), + self.create_model().done(function (id) { + self.id = id; + self.$('input[name=import_id]').val(id); + + self.render_buttons(); + var status = { + breadcrumbs: self.action_manager.get_breadcrumbs(), + cp_content: {$buttons: self.$buttons}, + }; + self.update_control_panel(status); + }) + ); + }, + create_model: function() { + return this.Import.call('create', [{ + 'res_model': this.res_model + }]); + }, + render_buttons: function() { + var self = this; + this.$buttons = $(QWeb.render("ImportView.buttons", this)); + this.$buttons.filter('.o_import_validate').on('click', this.validate.bind(this)); + this.$buttons.filter('.o_import_import').on('click', this.import.bind(this)); + this.$buttons.filter('.o_import_cancel').on('click', function(e) { + e.preventDefault(); + self.exit(); + }); + }, + setup_encoding_picker: function () { + this.$('input.oe_import_encoding').select2({ + width: '160px', + query: function (q) { + var make = function (term) { return {id: term, text: term}; }; + var suggestions = _.map( + ('utf-8 utf-16 windows-1252 latin1 latin2 big5 ' + + 'gb18030 shift_jis windows-1251 koir8_r').split(/\s+/), + make); + if (q.term) { + suggestions.unshift(make(q.term)); + } + q.callback({results: suggestions}); + }, + initSelection: function (e, c) { + return c({id: 'utf-8', text: 'utf-8'}); + } + }).select2('val', 'utf-8'); + }, + setup_separator_picker: function () { + this.$('input.oe_import_separator').select2({ + width: '160px', + query: function (q) { + var suggestions = [ + {id: ',', text: _t("Comma")}, + {id: ';', text: _t("Semicolon")}, + {id: '\t', text: _t("Tab")}, + {id: ' ', text: _t("Space")} + ]; + if (q.term) { + suggestions.unshift({id: q.term, text: q.term}); + } + q.callback({results: suggestions}); + }, + initSelection: function (e, c) { + return c({id: ',', text: _t("Comma")}); + }, + }); + }, + setup_float_format_picker: function () { + var sug_query = function (q) { + var suggestions = [ + {id: ',', text: _t("Comma")}, + {id: '.', text: _t("Dot")}, + ]; + if (q.term) { + suggestions.unshift({id: q.term, text: q.term}); + } + q.callback({results: suggestions}); + }; + this.$('input.oe_import_float_thousand_separator').select2({ + width: '160px', + query: sug_query, + initSelection: function (e, c) { + return c({id: ',', text: _t("Comma")}); + }, + }); + this.$('input.oe_import_float_decimal_separator').select2({ + width: '160px', + query: sug_query, + initSelection: function (e, c) { + return c({id: ',', text: _t("Dot")}); + }, + }); + }, + + import_options: function () { + var self = this; + var options = { + headers: this.$('input.oe_import_has_header').prop('checked'), + advanced: this.$('input.oe_import_advanced_mode').prop('checked'), + keep_matches: this.do_not_change_match + }; + _(this.opts).each(function (opt) { + options[opt.name] = + self.$('input.oe_import_' + opt.name).val(); + }); + _(this.parse_opts).each(function (opt) { + if (opt.name === 'date_format') { + options[opt.name] = time.moment_to_strftime_format(self.$('input.oe_import_date_format').val()); + } + else { + options[opt.name] = self.$('input.oe_import_' + opt.name).val(); + } + }); + options['fields'] = []; + if (this.do_not_change_match) { + options['fields'] = this.$('.oe_import_fields input.oe_import_match_field').map(function (index, el) { + return $(el).select2('val') || false; + }).get(); + } + this.do_not_change_match = false; + return options; + }, + + //- File & settings change section + onfile_loaded: function () { + var file = this.$('.oe_import_file')[0].files[0]; + this.$('.oe_import_file_show').val(file !== undefined && file.name || ''); + this.$buttons.filter('.o_import_button').add(this.$('.oe_import_file_reload')) + .prop('disabled', true); + if (!this.$('input.oe_import_file').val()) { return this['settings_changed'](); } + this.$('.oe_import_date_format').val(''); + + this.$el.removeClass('oe_import_preview oe_import_error'); + var import_toggle = false; + var file = this.$('input.oe_import_file')[0].files[0]; + // some platforms send text/csv, application/csv, or other things if Excel is prevent + if ((file.type && _.last(file.type.split('/')) === "csv") || ( _.last(file.name.split('.')) === "csv")) { + import_toggle = true; + } + this.$el.find('.oe_import_toggle').toggle(import_toggle); + jsonp(this.$el, { + url: '/base_import/set_file' + }, this.proxy('settings_changed')); + }, + onpreviewing: function () { + var self = this; + this.$buttons.filter('.o_import_button').add(this.$('.oe_import_file_reload')) + .prop('disabled', true); + this.$el.addClass('oe_import_with_file'); + // TODO: test that write // succeeded? + this.$el.removeClass('oe_import_preview_error oe_import_error'); + this.$el.toggleClass( + 'oe_import_noheaders', + !this.$('input.oe_import_has_header').prop('checked')); + this.Import.call( + 'parse_preview', [this.id, this.import_options()]) + .done(function (result) { + var signal = result.error ? 'preview_failed' : 'preview_succeeded'; + self[signal](result); + }); + }, + onpreview_error: function (event, from, to, result) { + this.$('.oe_import_options').show(); + this.$('.oe_import_file_reload').prop('disabled', false); + this.$el.addClass('oe_import_preview_error oe_import_error'); + this.$('.oe_import_error_report').html( + QWeb.render('ImportView.preview.error', result)); + }, + onpreview_success: function (event, from, to, result) { + var self = this; + this.$buttons.filter('.o_import_import').removeClass('btn-primary'); + this.$buttons.filter('.o_import_validate').addClass('btn-primary'); + this.$buttons.filter('.o_import_button').add(this.$('.oe_import_file_reload')) + .prop('disabled', false); + this.$el.addClass('oe_import_preview'); + this.$('.oe_import_grid').html(QWeb.render('ImportView.preview', result)); + + if (result.headers.length === 1) { + this.$('.oe_import_options').show(); + this.onresults(null, null, null, [{ + type: 'warning', + message: _t("A single column was found in the file, this often means the file separator is incorrect") + }]); + } + + this.$('.oe_import_date_format').val(time.strftime_to_moment_format(result.options.date_format)); + this.$('.oe_import_float_thousand_separator').val(result.options.float_thousand_separator).change(); + this.$('.oe_import_float_decimal_separator').val(result.options.float_decimal_separator).change(); + if (result.debug === false){ + this.$('.oe_import_tracking').hide(); + } + + var $fields = this.$('.oe_import_fields input'); + this.render_fields_matches(result, $fields); + var data = this.generate_fields_completion(result); + var item_finder = function (id, items) { + items = items || data; + for (var i=0; i < items.length; ++i) { + var item = items[i]; + if (item.id === id) { + return item; + } + var val; + if (item.children && (val = item_finder(id, item.children))) { + return val; + } + } + return ''; + }; + $fields.each(function (k,v) { + var filtered_data = self.generate_fields_completion(result, k); + $(v).select2({ + allowClear: true, + minimumInputLength: 0, + data: filtered_data, + initSelection: function (element, callback) { + var default_value = element.val(); + if (!default_value) { + callback(''); + return; + } + + callback(item_finder(default_value)); + }, + placeholder: _t('Don\'t import'), + width: 'resolve', + dropdownCssClass: 'oe_import_selector' + }); + }); + }, + generate_fields_completion: function (root, index) { + var basic = []; + var regulars = []; + var o2m = []; + var headers_type = root.headers_type; + function traverse(field, ancestors, collection, type) { + var subfields = field.fields; + var advanced_mode = self.$('input.oe_import_advanced_mode').prop('checked'); + var field_path = ancestors.concat(field); + var label = _(field_path).pluck('string').join(' / '); + var id = _(field_path).pluck('name').join('/'); + if (type === undefined || (type !== undefined && (type.indexOf('all') !== -1 || type.indexOf(field['type']) !== -1))){ + // If non-relational, m2o or m2m, collection is regulars + if (!collection) { + if (field.name === 'id') { + collection = basic; + } else if (_.isEmpty(subfields) + || _.isEqual(_.pluck(subfields, 'name'), ['id', '.id'])) { + collection = regulars; + } else { + collection = o2m; + } + } + + collection.push({ + id: id, + text: label, + required: field.required + }); + + } + if (advanced_mode){ + for(var i=0, end=subfields.length; i<end; ++i) { + traverse(subfields[i], field_path, collection, type); + } + } + + } + _(root.fields).each(function (field) { + if (index === undefined) { + traverse(field, []); + } + else { + if (self.$('input.oe_import_advanced_mode').prop('checked')){ + traverse(field, [], undefined, ['all']); + } + else { + traverse(field, [], undefined, headers_type[index]); + } + } + }); + + var cmp = function (field1, field2) { + return field1.text.localeCompare(field2.text); + + }; + regulars.sort(cmp); + o2m.sort(cmp); + if (!_.isEmpty(regulars) && !_.isEmpty(o2m)){ + basic = basic.concat([ + { text: _t("Normal Fields"), children: regulars }, + { text: _t("Relation Fields"), children: o2m }, + ]); + } + else if (!_.isEmpty(regulars)) { + basic = basic.concat(regulars); + } + else if (!_.isEmpty(o2m)) { + basic = basic.concat(o2m); + } + return basic; + }, + render_fields_matches: function (result, $fields) { + if (_(result.matches).isEmpty()) { return; } + $fields.each(function (index, input) { + var match = result.matches[index]; + if (!match) { return; } + + var current_field = result; + input.value = _(match).chain() + .map(function (name) { + // WARNING: does both mapping and folding (over the + // ``field`` iterator variable) + return current_field = _(current_field.fields).find(function (subfield) { + return subfield.name === name; + }); + }) + .pluck('name') + .value() + .join('/'); + }); + }, + + //- import itself + call_import: function (kwargs) { + var fields = this.$('.oe_import_fields input.oe_import_match_field').map(function (index, el) { + return $(el).select2('val') || false; + }).get(); + var tracking_disable = 'tracking_disable' in kwargs ? kwargs.tracking_disable : !this.$('#oe_import_tracking').prop('checked') + delete kwargs.tracking_disable + kwargs.context = _.extend( + {}, this.parent_context, + {tracking_disable: tracking_disable} + ); + return this.Import.call('do', [this.id, fields, this.import_options()], kwargs) + .then(undefined, function (error, event) { + // In case of unexpected exception, convert + // "JSON-RPC error" to an import failure, and + // prevent default handling (warning dialog) + if (event) { event.preventDefault(); } + return $.when([{ + type: 'error', + record: false, + message: error.data.arguments[1], + }]); + }) ; + }, + onvalidate: function () { + return this.call_import({ dryrun: true, tracking_disable: true }) + .done(this.proxy('validated')); + }, + onimport: function () { + var self = this; + return this.call_import({ dryrun: false }).done(function (message) { + if (!_.any(message, function (message) { + return message.type === 'error'; })) { + self['import_succeeded'](); + return; + } + self['import_failed'](message); + }); + }, + onimported: function () { + this.exit(); + }, + exit: function () { + this.do_action({ + type: 'ir.actions.client', + tag: 'history_back' + }); + }, + onresults: function (event, from, to, message) { + var no_messages = _.isEmpty(message); + this.$buttons.filter('.o_import_import').toggleClass('btn-primary', no_messages); + this.$buttons.filter('.o_import_import').toggleClass('btn-default', !no_messages); + this.$buttons.filter('.o_import_validate').toggleClass('btn-primary', !no_messages); + this.$buttons.filter('.o_import_validate').toggleClass('btn-default', no_messages); + if (no_messages) { + message.push({ + type: 'info', + message: _t("Everything seems valid.") + }); + } + // row indexes come back 0-indexed, spreadsheets + // display 1-indexed. + var offset = 1; + // offset more if header + if (this.import_options().headers) { offset += 1; } + + this.$el.addClass('oe_import_error'); + this.$('.oe_import_error_report').html( + QWeb.render('ImportView.error', { + errors: _(message).groupBy('message'), + at: function (rows) { + var from = rows.from + offset; + var to = rows.to + offset; + if (from === to) { + return _.str.sprintf(_t("at row %d"), from); + } + return _.str.sprintf(_t("between rows %d and %d"), + from, to); + }, + more: function (n) { + return _.str.sprintf(_t("(%d more)"), n); + }, + info: function (msg) { + if (typeof msg === 'string') { + return _.str.sprintf( + '<div class="oe_import_moreinfo oe_import_moreinfo_message">%s</div>', + _.str.escapeHTML(msg)); + } + if (msg instanceof Array) { + return _.str.sprintf( + '<div class="oe_import_moreinfo oe_import_moreinfo_choices">%s <ul>%s</ul></div>', + _.str.escapeHTML(_t("Here are the possible values:")), + _(msg).map(function (msg) { + return '<li>' + + _.str.escapeHTML(msg) + + '</li>'; + }).join('')); + } + // Final should be object, action descriptor + return [ + '<div class="oe_import_moreinfo oe_import_moreinfo_action">', + _.str.sprintf('<a href="#" data-action="%s">', + _.str.escapeHTML(JSON.stringify(msg))), + _.str.escapeHTML( + _t("Get all possible values")), + '</a>', + '</div>' + ].join(''); + }, + })); + }, +}); +core.action_registry.add('import', DataImport); + +// FSM-ize DataImport +StateMachine.create({ + target: DataImport.prototype, + events: [ + { name: 'loaded_file', + from: ['none', 'file_loaded', 'preview_error', 'preview_success', 'results'], + to: 'file_loaded' }, + { name: 'settings_changed', + from: ['file_loaded', 'preview_error', 'preview_success', 'results'], + to: 'previewing' }, + { name: 'preview_failed', from: 'previewing', to: 'preview_error' }, + { name: 'preview_succeeded', from: 'previewing', to: 'preview_success' }, + { name: 'validate', from: 'preview_success', to: 'validating' }, + { name: 'validate', from: 'results', to: 'validating' }, + { name: 'validated', from: 'validating', to: 'results' }, + { name: 'import', from: ['preview_success', 'results'], to: 'importing' }, + { name: 'import_succeeded', from: 'importing', to: 'imported'}, + { name: 'import_failed', from: 'importing', to: 'results' } + ] +}); + +return { + DataImport: DataImport, +} + +}); |
