[FIX]base_import: context was not considered while importing. Thus, default value...
[odoo/odoo.git] / addons / base_import / static / src / js / import.js
1 openerp.base_import = function (instance) {
2     var QWeb = instance.web.qweb;
3     var _t = instance.web._t;
4     var _lt = instance.web._lt;
5
6     /**
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).
12      *
13      * The function is an auto-generated name bound to ``window``,
14      * which calls back into the callback provided here.
15      *
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
19      */
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);
26         };
27         if ('data' in attributes) {
28             _.extend(attributes.data, options);
29         } else {
30             _.extend(attributes, {data: options});
31         }
32         _.extend(attributes, {
33             dataType: 'script',
34         });
35         $(form).ajaxSubmit(attributes);
36     }
37
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 () {
42             var self = this;
43             var add_button = false;
44             if (!this.$buttons) {
45                 add_button = true;
46             }
47             this._super.apply(this, arguments);
48             if(add_button) {
49                 this.$buttons.on('click', '.oe_list_button_import', function() {
50                     self.do_action({
51                         type: 'ir.actions.client',
52                         tag: 'import',
53                         params: {
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
58                             // though
59                             context: self.getParent().action.context,
60                         }
61                     }, {
62                         on_reverse_breadcrumb: function () {
63                             self.reload();
64                         },
65                     });
66                     return false;
67                 });
68             }
69         }
70     });
71
72     instance.web.client_actions.add(
73         'import', 'instance.web.DataImport');
74     instance.web.DataImport = instance.web.Widget.extend({
75         template: 'ImportView',
76         opts: [
77             {name: 'encoding', label: _lt("Encoding:"), value: 'utf-8'},
78             {name: 'separator', label: _lt("Separator:"), value: ','},
79             {name: 'quoting', label: _lt("Quoting:"), value: '"'}
80         ],
81         events: {
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) {
87                 e.preventDefault();
88                 var $el = $(e.target);
89                 ($el.next().length
90                         ? $el.next()
91                         : $el.parent().next())
92                     .toggle();
93             },
94             'click .oe_import_report a.oe_import_report_count': function (e) {
95                 e.preventDefault();
96                 $(e.target).parent().toggleClass('oe_import_report_showmore');
97             },
98             'click .oe_import_moreinfo_action a': function (e) {
99                 e.preventDefault();
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];
106                     return [
107                         id,
108                         type !== 'tree' ? type
109                           : action.view_type === 'form' ? 'list'
110                           : 'tree'
111                     ];
112                 });
113                 this.do_action(_.extend(action, {
114                     target: 'new',
115                     flags: {
116                         search_view: true,
117                         display_title: true,
118                         pager: true,
119                         list: {selectable: false}
120                     }
121                 }));
122             },
123             // buttons
124             'click .oe_import_validate': 'validate',
125             'click .oe_import_import': 'import',
126             'click .oe_import_cancel': function (e) {
127                 e.preventDefault();
128                 this.exit();
129             }
130         },
131         init: function (parent, action) {
132             var self = this;
133             this._super.apply(this, arguments);
134             this.res_model = action.params.model;
135             this.parent_context = action.params.context || {};
136             // import object id
137             this.id = null;
138             this.Import = new instance.web.Model('base_import.import');
139         },
140         start: function () {
141             var self = this;
142             this.setup_encoding_picker();
143             this.setup_separator_picker();
144
145             return $.when(
146                 this._super(),
147                 this.Import.call('create', [{
148                     'res_model': this.res_model
149                 }]).done(function (id) {
150                     self.id = id;
151                     self.$('input[name=import_id]').val(id);
152                 })
153             )
154         },
155         setup_encoding_picker: function () {
156             this.$('input.oe_import_encoding').select2({
157                 width: '160px',
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+/),
163                         make);
164                     if (q.term) {
165                         suggestions.unshift(make(q.term));
166                     }
167                     q.callback({results: suggestions});
168                 },
169                 initSelection: function (e, c) {
170                     return c({id: 'utf-8', text: 'utf-8'});
171                 }
172             }).select2('val', 'utf-8');
173         },
174         setup_separator_picker: function () {
175             this.$('input.oe_import_separator').select2({
176                 width: '160px',
177                 query: function (q) {
178                     var suggestions = [
179                         {id: ',', text: _t("Comma")},
180                         {id: ';', text: _t("Semicolon")},
181                         {id: '\t', text: _t("Tab")},
182                         {id: ' ', text: _t("Space")}
183                     ];
184                     if (q.term) {
185                         suggestions.unshift({id: q.term, text: q.term});
186                     }
187                     q.callback({results: suggestions});
188                 },
189                 initSelection: function (e, c) {
190                     return c({id: ',', text: _t("Comma")});
191                 },
192             });
193         },
194
195         import_options: function () {
196             var self = this;
197             var options = {
198                 headers: this.$('input.oe_import_has_header').prop('checked')
199             };
200             _(this.opts).each(function (opt) {
201                 options[opt.name] =
202                     self.$('input.oe_import_' + opt.name).val();
203             });
204             return options;
205         },
206
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; }
212
213             this.$el.removeClass('oe_import_preview oe_import_error');
214             jsonp(this.$el, {
215                 url: '/base_import/set_file'
216             }, this.proxy('settings_changed'));
217         },
218         onpreviewing: function () {
219             var self = this;
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'));
228             this.Import.call(
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);
233                 });
234         },
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));
241         },
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));
249
250             if (result.headers.length === 1) {
251                 this.$('.oe_import_options').show();
252                 this.onresults(null, null, null, [{
253                     type: 'warning',
254                     message: _t("A single column was found in the file, this often means the file separator is incorrect")
255                 }]);
256             }
257
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) {
264                     var item = items[i];
265                     if (item.id === id) {
266                         return item;
267                     }
268                     var val;
269                     if (item.children && (val = item_finder(id, item.children))) {
270                         return val;
271                     }
272                 }
273                 return '';
274             };
275             $fields.select2({
276                 allowClear: true,
277                 minimumInputLength: 0,
278                 data: data,
279                 initSelection: function (element, callback) {
280                     var default_value = element.val();
281                     if (!default_value) {
282                         callback('');
283                         return;
284                     }
285
286                     callback(item_finder(default_value));
287                 },
288
289                 width: 'resolve',
290                 dropdownCssClass: 'oe_import_selector'
291             });
292         },
293         generate_fields_completion: function (root) {
294             var basic = [];
295             var regulars = [];
296             var o2m = [];
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('/');
302
303                 // If non-relational, m2o or m2m, collection is regulars
304                 if (!collection) {
305                     if (field.name === 'id') {
306                         collection = basic
307                     } else if (_.isEmpty(subfields)
308                             || _.isEqual(_.pluck(subfields, 'name'), ['id', '.id'])) {
309                         collection = regulars;
310                     } else {
311                         collection = o2m;
312                     }
313                 }
314
315                 collection.push({
316                     id: id,
317                     text: label,
318                     required: field.required
319                 });
320
321                 for(var i=0, end=subfields.length; i<end; ++i) {
322                     traverse(subfields[i], field_path, collection);
323                 }
324             }
325             _(root.fields).each(function (field) {
326                 traverse(field, []);
327             });
328
329             var cmp = function (field1, field2) {
330                 return field1.text.localeCompare(field2.text);
331
332             };
333             regulars.sort(cmp);
334             o2m.sort(cmp);
335             return basic.concat([
336                 { text: _t("Normal Fields"), children: regulars },
337                 { text: _t("Relation Fields"), children: o2m }
338             ]);
339         },
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; }
345
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;
353                         });
354                     })
355                     .pluck('name')
356                     .value()
357                     .join('/');
358             });
359         },
360
361         //- import itself
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;
365             }).get();
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(); }
373                     return $.when([{
374                         type: 'error',
375                         record: false,
376                         message: error.data.fault_code,
377                     }]);
378                 }) ;
379         },
380         onvalidate: function () {
381             return this.call_import({ dryrun: true })
382                 .done(this.proxy('validated'));
383         },
384         onimport: function () {
385             var self = this;
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']();
390                     return;
391                 }
392                 self['import_failed'](message);
393             });
394         },
395         onimported: function () {
396             this.exit();
397         },
398         exit: function () {
399             this.do_action({
400                 type: 'ir.actions.client',
401                 tag: 'history_back'
402             });
403         },
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);
408             if (no_messages) {
409                 message.push({
410                     type: 'info',
411                     message: _t("Everything seems valid.")
412                 });
413             }
414             // row indexes come back 0-indexed, spreadsheets
415             // display 1-indexed.
416             var offset = 1;
417             // offset more if header
418             if (this.import_options().headers) { offset += 1; }
419
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;
427                         if (from === to) {
428                             return _.str.sprintf(_t("at row %d"), from);
429                         }
430                         return _.str.sprintf(_t("between rows %d and %d"),
431                                              from, to);
432                     },
433                     more: function (n) {
434                         return _.str.sprintf(_t("(%d more)"), n);
435                     },
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));
441                         }
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) {
447                                     return '<li>'
448                                         + _.str.escapeHTML(msg)
449                                     + '</li>';
450                                 }).join(''));
451                         }
452                         // Final should be object, action descriptor
453                         return [
454                             '<div class="oe_import_moreinfo oe_import_moreinfo_action">',
455                                 _.str.sprintf('<a href="#" data-action="%s">',
456                                         _.str.escapeHTML(JSON.stringify(msg))),
457                                     _.str.escapeHTML(
458                                         _t("Get all possible values")),
459                                 '</a>',
460                             '</div>'
461                         ].join('')
462                     },
463                 }));
464         },
465     });
466     // FSM-ize DataImport
467     StateMachine.create({
468         target: instance.web.DataImport.prototype,
469         events: [
470             { name: 'loaded_file',
471               from: ['none', 'file_loaded', 'preview_error', 'preview_success', 'results'],
472               to: 'file_loaded' },
473             { name: 'settings_changed',
474               from: ['file_loaded', 'preview_error', 'preview_success', 'results'],
475               to: 'previewing' },
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' }
484         ]
485     })
486 };