1 openerp.base_import = function (instance) {
2 var QWeb = instance.web.qweb;
3 var _t = instance.web._t;
4 var _lt = instance.web._lt;
7 * Safari does not deal well at all with raw JSON data being
8 * returned. As a result, we're going to cheat by using a
9 * pseudo-jsonp: instead of getting JSON data in the iframe, we're
10 * getting a ``script`` tag which consists of a function call and
11 * the returned data (the json dump).
13 * The function is an auto-generated name bound to ``window``,
14 * which calls back into the callback provided here.
16 * @param {Object} form the form element (DOM or jQuery) to use in the call
17 * @param {Object} attributes jquery.form attributes object
18 * @param {Function} callback function to call with the returned data
20 function jsonp(form, attributes, callback) {
21 attributes = attributes || {};
22 var options = {jsonp: _.uniqueId('import_callback_')};
23 window[options.jsonp] = function () {
24 delete window[options.jsonp];
25 callback.apply(null, arguments);
27 if ('data' in attributes) {
28 _.extend(attributes.data, options);
30 _.extend(attributes, {data: options});
32 _.extend(attributes, {
35 $(form).ajaxSubmit(attributes);
38 // if true, the 'Import', 'Export', etc... buttons will be shown
39 instance.web.ListView.prototype.defaults.import_enabled = true;
40 instance.web.ListView.include({
41 load_list: function () {
43 var add_button = false;
47 this._super.apply(this, arguments);
49 this.$buttons.on('click', '.oe_list_button_import', function() {
51 type: 'ir.actions.client',
54 model: self.dataset.model,
55 // self.dataset.get_context() could be a compound?
56 // not sure. action's context should be evaluated
57 // so safer bet. Odd that timezone & al in it
59 context: self.getParent().action.context,
62 on_reverse_breadcrumb: function () {
72 instance.web.client_actions.add(
73 'import', 'instance.web.DataImport');
74 instance.web.DataImport = instance.web.Widget.extend({
75 template: 'ImportView',
77 {name: 'encoding', label: _lt("Encoding:"), value: 'utf-8'},
78 {name: 'separator', label: _lt("Separator:"), value: ','},
79 {name: 'quoting', label: _lt("Quoting:"), value: '"'}
82 // 'change .oe_import_grid input': 'import_dryrun',
83 'change .oe_import_file': 'loaded_file',
84 'click .oe_import_file_reload': 'loaded_file',
85 'change input.oe_import_has_header, .oe_import_options input': 'settings_changed',
86 'click a.oe_import_toggle': function (e) {
88 var $el = $(e.target);
91 : $el.parent().next())
94 'click .oe_import_report a.oe_import_report_count': function (e) {
96 $(e.target).parent().toggleClass('oe_import_report_showmore');
98 'click .oe_import_moreinfo_action a': function (e) {
100 // #data will parse the attribute on its own, we don't like
101 // that sort of things
102 var action = JSON.parse($(e.target).attr('data-action'));
103 // FIXME: when JS-side clean_action
104 action.views = _(action.views).map(function (view) {
105 var id = view[0], type = view[1];
108 type !== 'tree' ? type
109 : action.view_type === 'form' ? 'list'
113 this.do_action(_.extend(action, {
119 list: {selectable: false}
124 'click .oe_import_validate': 'validate',
125 'click .oe_import_import': 'import',
126 'click .oe_import_cancel': function (e) {
131 init: function (parent, action) {
133 this._super.apply(this, arguments);
134 this.res_model = action.params.model;
135 this.parent_context = action.params.context || {};
138 this.Import = new instance.web.Model('base_import.import');
142 this.setup_encoding_picker();
143 this.setup_separator_picker();
147 this.Import.call('create', [{
148 'res_model': this.res_model
149 }]).done(function (id) {
151 self.$('input[name=import_id]').val(id);
155 setup_encoding_picker: function () {
156 this.$('input.oe_import_encoding').select2({
158 query: function (q) {
159 var make = function (term) { return {id: term, text: term}; };
160 var suggestions = _.map(
161 ('utf-8 utf-16 windows-1252 latin1 latin2 big5 ' +
162 'gb18030 shift_jis windows-1251 koir8_r').split(/\s+/),
165 suggestions.unshift(make(q.term));
167 q.callback({results: suggestions});
169 initSelection: function (e, c) {
170 return c({id: 'utf-8', text: 'utf-8'});
172 }).select2('val', 'utf-8');
174 setup_separator_picker: function () {
175 this.$('input.oe_import_separator').select2({
177 query: function (q) {
179 {id: ',', text: _t("Comma")},
180 {id: ';', text: _t("Semicolon")},
181 {id: '\t', text: _t("Tab")},
182 {id: ' ', text: _t("Space")}
185 suggestions.unshift({id: q.term, text: q.term});
187 q.callback({results: suggestions});
189 initSelection: function (e, c) {
190 return c({id: ',', text: _t("Comma")});
195 import_options: function () {
198 headers: this.$('input.oe_import_has_header').prop('checked')
200 _(this.opts).each(function (opt) {
202 self.$('input.oe_import_' + opt.name).val();
207 //- File & settings change section
208 onfile_loaded: function () {
209 this.$('.oe_import_button, .oe_import_file_reload')
210 .prop('disabled', true);
211 if (!this.$('input.oe_import_file').val()) { return; }
213 this.$el.removeClass('oe_import_preview oe_import_error');
215 url: '/base_import/set_file'
216 }, this.proxy('settings_changed'));
218 onpreviewing: function () {
220 this.$('.oe_import_button, .oe_import_file_reload')
221 .prop('disabled', true);
222 this.$el.addClass('oe_import_with_file');
223 // TODO: test that write // succeeded?
224 this.$el.removeClass('oe_import_preview_error oe_import_error');
225 this.$el.toggleClass(
226 'oe_import_noheaders',
227 !this.$('input.oe_import_has_header').prop('checked'));
229 'parse_preview', [this.id, this.import_options()])
230 .done(function (result) {
231 var signal = result.error ? 'preview_failed' : 'preview_succeeded';
232 self[signal](result);
235 onpreview_error: function (event, from, to, result) {
236 this.$('.oe_import_options').show();
237 this.$('.oe_import_file_reload').prop('disabled', false);
238 this.$el.addClass('oe_import_preview_error oe_import_error');
239 this.$('.oe_import_error_report').html(
240 QWeb.render('ImportView.preview.error', result));
242 onpreview_success: function (event, from, to, result) {
243 this.$('.oe_import_import').removeClass('oe_highlight');
244 this.$('.oe_import_validate').addClass('oe_highlight');
245 this.$('.oe_import_button, .oe_import_file_reload')
246 .prop('disabled', false);
247 this.$el.addClass('oe_import_preview');
248 this.$('table').html(QWeb.render('ImportView.preview', result));
250 if (result.headers.length === 1) {
251 this.$('.oe_import_options').show();
252 this.onresults(null, null, null, [{
254 message: _t("A single column was found in the file, this often means the file separator is incorrect")
258 var $fields = this.$('.oe_import_fields input');
259 this.render_fields_matches(result, $fields);
260 var data = this.generate_fields_completion(result);
261 var item_finder = function (id, items) {
262 items = items || data;
263 for (var i=0; i < items.length; ++i) {
265 if (item.id === id) {
269 if (item.children && (val = item_finder(id, item.children))) {
277 minimumInputLength: 0,
279 initSelection: function (element, callback) {
280 var default_value = element.val();
281 if (!default_value) {
286 callback(item_finder(default_value));
290 dropdownCssClass: 'oe_import_selector'
293 generate_fields_completion: function (root) {
297 function traverse(field, ancestors, collection) {
298 var subfields = field.fields;
299 var field_path = ancestors.concat(field);
300 var label = _(field_path).pluck('string').join(' / ');
301 var id = _(field_path).pluck('name').join('/');
303 // If non-relational, m2o or m2m, collection is regulars
305 if (field.name === 'id') {
307 } else if (_.isEmpty(subfields)
308 || _.isEqual(_.pluck(subfields, 'name'), ['id', '.id'])) {
309 collection = regulars;
318 required: field.required
321 for(var i=0, end=subfields.length; i<end; ++i) {
322 traverse(subfields[i], field_path, collection);
325 _(root.fields).each(function (field) {
329 var cmp = function (field1, field2) {
330 return field1.text.localeCompare(field2.text);
335 return basic.concat([
336 { text: _t("Normal Fields"), children: regulars },
337 { text: _t("Relation Fields"), children: o2m }
340 render_fields_matches: function (result, $fields) {
341 if (_(result.matches).isEmpty()) { return; }
342 $fields.each(function (index, input) {
343 var match = result.matches[index];
344 if (!match) { return; }
346 var current_field = result;
347 input.value = _(match).chain()
348 .map(function (name) {
349 // WARNING: does both mapping and folding (over the
350 // ``field`` iterator variable)
351 return current_field = _(current_field.fields).find(function (subfield) {
352 return subfield.name === name;
362 call_import: function (kwargs) {
363 var fields = this.$('.oe_import_fields input.oe_import_match_field').map(function (index, el) {
364 return $(el).select2('val') || false;
366 kwargs.context = this.parent_context;
367 return this.Import.call('do', [this.id, fields, this.import_options()], kwargs)
368 .then(undefined, function (error, event) {
369 // In case of unexpected exception, convert
370 // "JSON-RPC error" to an import failure, and
371 // prevent default handling (warning dialog)
372 if (event) { event.preventDefault(); }
376 message: error.data.fault_code,
380 onvalidate: function () {
381 return this.call_import({ dryrun: true })
382 .done(this.proxy('validated'));
384 onimport: function () {
386 return this.call_import({ dryrun: false }).done(function (message) {
387 if (!_.any(message, function (message) {
388 return message.type === 'error' })) {
389 self['import_succeeded']();
392 self['import_failed'](message);
395 onimported: function () {
400 type: 'ir.actions.client',
404 onresults: function (event, from, to, message) {
405 var no_messages = _.isEmpty(message);
406 this.$('.oe_import_import').toggleClass('oe_highlight', no_messages);
407 this.$('.oe_import_validate').toggleClass('oe_highlight', !no_messages);
411 message: _t("Everything seems valid.")
414 // row indexes come back 0-indexed, spreadsheets
415 // display 1-indexed.
417 // offset more if header
418 if (this.import_options().headers) { offset += 1; }
420 this.$el.addClass('oe_import_error');
421 this.$('.oe_import_error_report').html(
422 QWeb.render('ImportView.error', {
423 errors: _(message).groupBy('message'),
424 at: function (rows) {
425 var from = rows.from + offset;
426 var to = rows.to + offset;
428 return _.str.sprintf(_t("at row %d"), from);
430 return _.str.sprintf(_t("between rows %d and %d"),
434 return _.str.sprintf(_t("(%d more)"), n);
436 info: function (msg) {
437 if (typeof msg === 'string') {
438 return _.str.sprintf(
439 '<div class="oe_import_moreinfo oe_import_moreinfo_message">%s</div>',
440 _.str.escapeHTML(msg));
442 if (msg instanceof Array) {
443 return _.str.sprintf(
444 '<div class="oe_import_moreinfo oe_import_moreinfo_choices">%s <ul>%s</ul></div>',
445 _.str.escapeHTML(_t("Here are the possible values:")),
446 _(msg).map(function (msg) {
448 + _.str.escapeHTML(msg)
452 // Final should be object, action descriptor
454 '<div class="oe_import_moreinfo oe_import_moreinfo_action">',
455 _.str.sprintf('<a href="#" data-action="%s">',
456 _.str.escapeHTML(JSON.stringify(msg))),
458 _t("Get all possible values")),
466 // FSM-ize DataImport
467 StateMachine.create({
468 target: instance.web.DataImport.prototype,
470 { name: 'loaded_file',
471 from: ['none', 'file_loaded', 'preview_error', 'preview_success', 'results'],
473 { name: 'settings_changed',
474 from: ['file_loaded', 'preview_error', 'preview_success', 'results'],
476 { name: 'preview_failed', from: 'previewing', to: 'preview_error' },
477 { name: 'preview_succeeded', from: 'previewing', to: 'preview_success' },
478 { name: 'validate', from: 'preview_success', to: 'validating' },
479 { name: 'validate', from: 'results', to: 'validating' },
480 { name: 'validated', from: 'validating', to: 'results' },
481 { name: 'import', from: ['preview_success', 'results'], to: 'importing' },
482 { name: 'import_succeeded', from: 'importing', to: 'imported'},
483 { name: 'import_failed', from: 'importing', to: 'results' }