1 openerp.web.data_import = function(openerp) {
2 var QWeb = openerp.web.qweb,
5 * Safari does not deal well at all with raw JSON data being returned. As a
6 * result, we're going to cheat by using a pseudo-jsonp: instead of getting
7 * JSON data in the iframe, we're getting a ``script`` tag which consists of a
8 * function call and the returned data (the json dump).
10 * The function is an auto-generated name bound to ``window``, which calls
11 * back into the callback provided here.
13 * @param {Object} form the form element (DOM or jQuery) to use in the call
14 * @param {Object} attributes jquery.form attributes object
15 * @param {Function} callback function to call with the returned data
17 function jsonp(form, attributes, callback) {
18 attributes = attributes || {};
19 var options = {jsonp: _.uniqueId('import_callback_')};
20 window[options.jsonp] = function () {
21 delete window[options.jsonp];
22 callback.apply(null, arguments);
24 if ('data' in attributes) {
25 _.extend(attributes.data, options);
27 _.extend(attributes, {data: options});
29 $(form).ajaxSubmit(attributes);
32 openerp.web.DataImport = openerp.web.Dialog.extend({
33 template: 'ImportDataView',
34 dialog_title: {toString: function () { return _t("Import Data"); }},
35 init: function(parent, dataset){
37 this._super(parent, {});
38 this.model = parent.model;
41 this.fields_with_defaults = [];
42 this.required_fields = null;
44 var convert_fields = function (root, prefix) {
45 prefix = prefix || '';
46 _(root.fields).each(function (f) {
47 self.all_fields.push(prefix + f.name);
49 convert_fields(f, prefix + f.id + '/');
53 this.ready = $.Deferred.queue().then(function () {
54 self.required_fields = _(self.fields).chain()
55 .filter(function (field) {
56 return field.required &&
57 !_.include(self.fields_with_defaults, field.id); })
61 self.all_fields.sort();
69 {text: _t("Close"), click: function() { self.stop(); }},
70 {text: _t("Import File"), click: function() { self.do_import(); }, 'class': 'oe-dialog-import-button'}
72 close: function(event, ui) {
76 this.toggle_import_button(false);
77 this.$element.find('#csvfile').change(this.on_autodetect_data);
78 this.$element.find('fieldset').change(this.on_autodetect_data);
79 this.$element.delegate('fieldset legend', 'click', function() {
80 $(this).parent().toggleClass('oe-closed');
82 this.ready.push(new openerp.web.DataSet(this, this.model).call(
83 'fields_get', [], function (fields) {
84 self.graft_fields(fields);
85 self.ready.push(new openerp.web.DataSet(self, self.model)
86 .default_get(_.pluck(self.fields, 'id')).then(function (fields) {
87 _.each(fields, function(val, key) {
89 self.fields_with_defaults.push(key);
96 graft_fields: function (fields, parent, level) {
97 parent = parent || this;
105 string: _t('External ID'),
109 _(fields).each(function (field, field_name) {
110 // Ignore spec for id field
111 // Don't import function fields (function and related)
112 if (field_name === 'id' || 'function' in field) {
118 string: field.string,
119 required: field.required
122 switch (field.type) {
130 // only fetch sub-fields to a depth of 2 levels
132 self.ready.push(new openerp.web.DataSet(self, field.relation).call(
133 'fields_get', [], function (fields) {
134 self.graft_fields(fields, f, level+1);
139 parent.fields.push(f);
142 toggle_import_button: function (newstate) {
143 this.$element.dialog('widget')
144 .find('.oe-dialog-import-button')
145 .button('option', 'disabled', !newstate);
147 do_import: function() {
148 if(!this.$element.find('#csvfile').val()) { return; }
149 var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10);
150 var with_headers = this.$element.find('#file_has_headers').prop('checked');
151 if (!lines_to_skip && with_headers) {
154 var indices = [], fields = [];
155 this.$element.find(".sel_fields").each(function(index, element) {
156 var val = element.value;
164 jsonp(this.$element.find('#import_data'), {
165 url: '/web/import/import_data',
168 meta: JSON.stringify({
174 }, this.on_import_results);
176 on_autodetect_data: function() {
177 if(!this.$element.find('#csvfile').val()) { return; }
178 jsonp(this.$element.find('#import_data'), {
179 url: '/web/import/detect_data'
180 }, this.on_import_results);
182 on_import_results: function(results) {
183 this.$element.find('#result').empty();
184 var headers, result_node = this.$element.find("#result");
186 if (results['error']) {
187 result_node.append(QWeb.render('ImportView.error', {
188 'error': results['error']}));
189 this.$element.find('fieldset').removeClass('oe-closed');
192 if (results['success']) {
193 if (this.widget_parent.widget_parent.active_view == "list") {
194 this.widget_parent.reload_content();
200 if (results['records']) {
201 var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10),
202 with_headers = this.$element.find('#file_has_headers').prop('checked');
203 headers = with_headers ? results.records[0] : null;
205 result_node.append(QWeb.render('ImportView.result', {
207 'records': lines_to_skip ? results.records.slice(lines_to_skip)
208 : with_headers ? results.records.slice(1)
211 this.$element.find('fieldset').addClass('oe-closed');
213 this.$element.find('form').removeClass('oe-import-no-result');
215 this.$element.delegate('.oe-m2o-drop-down-button', 'click', function () {
216 $(this).prev('input').focus();
220 this.ready.then(function () {
221 var $fields = self.$element.find('.sel_fields').bind('blur', function () {
222 if (this.value && !_(self.all_fields).contains(this.value)) {
227 source: self.all_fields,
228 change: self.on_check_field_values
229 }).focus(function () {
230 $(this).autocomplete('search');
232 // Column auto-detection
233 _(headers).each(function (header, index) {
234 var field_name = self.match_column_to_field(header);
236 $fields.eq(index).val(field_name);
239 self.on_check_field_values();
243 * Returns the name of the field (nested) matching the provided column name
245 * @param {String} name column name to look for
246 * @param {Array} [fields] fields to look into for the provided name
247 * @returns {String|undefined}
249 match_column_to_field: function (name, fields) {
250 fields = fields || this.fields;
252 f = _(fields).detect(function (field) {
253 // TODO: levenshtein between header and field.string
254 return field.name === name
255 || field.string.toLowerCase() === name.toLowerCase();
257 if (f) { return f.name; }
259 // if ``name`` is a path (o2m), we need to recurse through its .fields
260 var index = name.indexOf('/');
261 if (index === -1) { return undefined; }
262 // Get the first path section, try to find the matching field
263 var column_name = name.substring(0, index);
264 f = _(fields).detect(function (field) {
265 // field.name for o2m is $foo/id, so we want to match on id
266 return field.id === column_name
267 || field.string.toLowerCase() === column_name.toLowerCase()
269 if (!f) { return undefined; }
271 // if we found a matching field for the first path section, recurse in
272 // its own .fields to try and get the rest of the path matched
273 var rest = this.match_column_to_field(
274 name.substring(index+1), f.fields);
275 if (!rest) { return undefined; }
276 return f.id + '/' + rest;
279 * Looks through all the field selections, and tries to find if two
280 * (or more) columns were matched to the same model field.
282 * Returns a map of the multiply-mapped fields to an array of offending
283 * columns (not actually columns, but the inputs containing the same field
286 * Also has the side-effect of marking the discovered inputs with the class
289 * @returns {Object<String, Array<String>>} map of duplicate field matches to same-valued inputs
291 find_duplicate_fields: function() {
292 // Maps values to DOM nodes, in order to discover duplicates
293 var values = {}, duplicates = {};
294 this.$element.find(".sel_fields").each(function(index, element) {
295 var value = element.value;
296 var $element = $(element).removeClass('duplicate_fld');
297 if (!value) { return; }
299 if (!(value in values)) {
300 values[value] = element;
302 var same_valued_field = values[value];
303 if (value in duplicates) {
304 duplicates[value].push(element);
306 duplicates[value] = [same_valued_field, element];
308 $element.add(same_valued_field).addClass('duplicate_fld');
313 on_check_field_values: function () {
314 this.$element.find("#message, #msg").remove();
316 var required_valid = this.check_required();
318 var duplicates = this.find_duplicate_fields();
319 if (_.isEmpty(duplicates)) {
320 this.toggle_import_button(required_valid);
322 var $err = $('<div id="msg" style="color: red;">Destination fields should only be selected once, some fields are selected more than once:</div>').insertBefore(this.$element.find('#result'));
323 var $dupes = $('<dl>').appendTo($err);
324 _(duplicates).each(function(elements, value) {
325 $('<dt>').text(value).appendTo($dupes);
326 _(elements).each(function(element) {
327 var cell = $(element).closest('td');
328 $('<dd>').text(cell.parent().children().index(cell)).appendTo($dupes);
331 this.toggle_import_button(false);
335 check_required: function() {
336 if (!this.required_fields.length) { return true; }
338 var selected_fields = _(this.$element.find('.sel_fields').get()).chain()
343 var missing_fields = _.difference(this.required_fields, selected_fields);
344 if (missing_fields.length) {
345 this.$element.find("#result").before('<div id="message" style="color:red">*Required Fields are not selected : ' + missing_fields + '.</div>');
351 this.$element.remove();