1 openerp.web.data_import = function(openerp) {
2 var QWeb = openerp.web.qweb;
4 * Safari does not deal well at all with raw JSON data being returned. As a
5 * result, we're going to cheat by using a pseudo-jsonp: instead of getting
6 * JSON data in the iframe, we're getting a ``script`` tag which consists of a
7 * function call and the returned data (the json dump).
9 * The function is an auto-generated name bound to ``window``, which calls
10 * back into the callback provided here.
12 * @param {Object} form the form element (DOM or jQuery) to use in the call
13 * @param {Object} attributes jquery.form attributes object
14 * @param {Function} callback function to call with the returned data
16 function jsonp(form, attributes, callback) {
17 attributes = attributes || {};
18 var options = {jsonp: _.uniqueId('import_callback_')};
19 window[options.jsonp] = function () {
20 delete window[options.jsonp];
21 callback.apply(null, arguments);
23 if ('data' in attributes) {
24 _.extend(attributes.data, options);
26 _.extend(attributes, {data: options});
28 $(form).ajaxSubmit(attributes);
31 openerp.web.DataImport = openerp.web.Dialog.extend({
32 template: 'ImportDataView',
33 dialog_title: "Import Data",
34 init: function(parent, dataset){
36 this._super(parent, {});
37 this.model = parent.model;
40 this.required_fields = null;
42 var convert_fields = function (root, prefix) {
43 prefix = prefix || '';
44 _(root.fields).each(function (f) {
45 self.all_fields.push(prefix + f.name);
47 convert_fields(f, prefix + f.id + '/');
51 this.ready = $.Deferred.queue().then(function () {
52 self.required_fields = _(self.fields).chain()
53 .filter(function (field) { return field.required; })
57 self.all_fields.sort();
69 {text: "Close", click: function() { self.stop(); }},
70 {text: "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.find('fieldset legend').click(function() {
80 $(this).next().toggle();
82 this.ready.push(new openerp.web.DataSet(this, this.model).call(
83 'fields_get', [], function (fields) {
84 self.graft_fields(fields);
87 graft_fields: function (fields, parent, level) {
88 parent = parent || this;
92 _(fields).each(function (field, field_name) {
97 required: field.required
100 switch (field.type) {
108 // only fetch sub-fields to a depth of 2 levels
110 self.ready.push(new openerp.web.DataSet(self, field.relation).call(
111 'fields_get', [], function (fields) {
112 self.graft_fields(fields, f, level+1);
117 parent.fields.push(f);
120 toggle_import_button: function (newstate) {
121 this.$dialog.dialog('widget')
122 .find('.oe-dialog-import-button')
123 .button('option', 'disabled', !newstate);
125 do_import: function() {
126 if(!this.$element.find('#csvfile').val()) { return; }
127 var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10);
128 var with_headers = this.$element.find('#file_has_headers').prop('checked');
129 if (!lines_to_skip && with_headers) {
132 var indices = [], fields = [];
133 this.$element.find(".sel_fields").each(function(index, element) {
134 var val = element.value;
142 jsonp(this.$element.find('#import_data'), {
143 url: '/web/import/import_data',
146 meta: JSON.stringify({
152 }, this.on_import_results);
154 on_autodetect_data: function() {
155 if(!this.$element.find('#csvfile').val()) { return; }
156 jsonp(this.$element.find('#import_data'), {
157 url: '/web/import/detect_data'
158 }, this.on_import_results);
160 on_import_results: function(results) {
161 this.$element.find('#result').empty();
162 var headers, result_node = this.$element.find("#result");
164 if (results['records']) {
165 var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10),
166 with_headers = this.$element.find('#file_has_headers').prop('checked');
167 headers = with_headers ? results.records[0] : null;
169 result_node.append(QWeb.render('ImportView.result', {
171 'records': lines_to_skip ? results.records.slice(lines_to_skip)
172 : with_headers ? results.records.slice(1)
175 } else if (results['error']) {
176 result_node.append(QWeb.render('ImportView.error', {
177 'error': results['error']}));
178 } else if (results['success']) {
179 if (this.widget_parent.widget_parent.active_view == "list") {
180 this.widget_parent.reload_content();
187 this.ready.then(function () {
188 var $fields = self.$element.find('.sel_fields').bind('blur', function () {
189 if (this.value && !_(self.all_fields).contains(this.value)) {
194 source: self.all_fields,
195 change: self.on_check_field_values
196 }).focus(function () {
197 $(this).autocomplete('search');
199 // Column auto-detection
200 _(headers).each(function (header, index) {
201 var f =_(self.fields).detect(function (field) {
202 // TODO: levenshtein between header and field.string
203 return field.name === header || field.string.toLowerCase() === header;
206 $fields.eq(index).val(f.name);
209 self.on_check_field_values();
213 * Looks through all the field selections, and tries to find if two
214 * (or more) columns were matched to the same model field.
216 * Returns a map of the multiply-mapped fields to an array of offending
217 * columns (not actually columns, but the inputs containing the same field
220 * Also has the side-effect of marking the discovered inputs with the class
223 * @returns {Object<String, Array<String>>} map of duplicate field matches to same-valued inputs
225 find_duplicate_fields: function() {
226 // Maps values to DOM nodes, in order to discover duplicates
227 var values = {}, duplicates = {};
228 this.$element.find(".sel_fields").each(function(index, element) {
229 var value = element.value;
230 var $element = $(element).removeClass('duplicate_fld');
231 if (!value) { return; }
233 if (!(value in values)) {
234 values[value] = element;
236 var same_valued_field = values[value];
237 if (value in duplicates) {
238 duplicates[value].push(element);
240 duplicates[value] = [same_valued_field, element];
242 $element.add(same_valued_field).addClass('duplicate_fld');
247 on_check_field_values: function () {
248 this.$element.find("#message, #msg").remove();
250 var required_valid = this.check_required();
252 var duplicates = this.find_duplicate_fields();
253 if (_.isEmpty(duplicates)) {
254 this.toggle_import_button(required_valid);
256 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'));
257 var $dupes = $('<dl>').appendTo($err);
258 _(duplicates).each(function(elements, value) {
259 $('<dt>').text(value).appendTo($dupes);
260 _(elements).each(function(element) {
261 var cell = $(element).closest('td');
262 $('<dd>').text(cell.parent().children().index(cell)).appendTo($dupes);
265 this.toggle_import_button(false);
269 check_required: function() {
270 if (!this.required_fields.length) { return true; }
272 var selected_fields = _(this.$element.find('.sel_fields').get()).chain()
277 var missing_fields = _.difference(this.required_fields, selected_fields);
278 if (missing_fields.length) {
279 this.$element.find("#result").before('<div id="message" style="color:red">*Required Fields are not selected : ' + missing_fields + '.</div>');
285 $(this.$dialog).remove();