summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/widgets/data_export.js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/widgets/data_export.js')
-rw-r--r--addons/web/static/src/js/widgets/data_export.js688
1 files changed, 688 insertions, 0 deletions
diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js
new file mode 100644
index 00000000..f6354920
--- /dev/null
+++ b/addons/web/static/src/js/widgets/data_export.js
@@ -0,0 +1,688 @@
+odoo.define('web.DataExport', function (require) {
+"use strict";
+
+var config = require('web.config');
+var core = require('web.core');
+var Dialog = require('web.Dialog');
+var data = require('web.data');
+var framework = require('web.framework');
+var pyUtils = require('web.py_utils');
+
+var QWeb = core.qweb;
+var _t = core._t;
+
+var DataExport = Dialog.extend({
+ template: 'ExportDialog',
+ events: {
+ 'change .o_exported_lists_select': '_onChangeExportList',
+ 'change .o_import_compat input': '_onChangeCompatibleInput',
+ 'click .o_add_field': '_onClickAddField',
+ 'click .o_delete_exported_list': '_onClickDeleteExportListBtn',
+ 'click .o_expand': '_onClickExpand',
+ 'click .o_remove_field': '_onClickRemoveField',
+ 'click .o_save_list .o_save_list_btn': '_onClickSaveListBtn',
+ 'click .o_save_list .o_cancel_list_btn': '_resetTemplateField',
+ 'click .o_export_tree_item': '_onClickTreeItem',
+ 'dblclick .o_export_tree_item:not(.haschild)': '_onDblclickTreeItem',
+ 'keydown .o_export_tree_item': '_onKeydownTreeItem',
+ 'keydown .o_save_list_name': '_onKeydownSaveList',
+ 'input .o_export_search_input': '_onSearchInput',
+ },
+ /**
+ * @constructor
+ * @param {Widget} parent
+ * @param {Object} record
+ * @param {string[]} defaultExportFields
+ */
+ init: function (parent, record, defaultExportFields, groupedBy, activeDomain, idsToExport) {
+ var options = {
+ title: _t("Export Data"),
+ buttons: [
+ {text: _t("Export"), click: this._onExportData, classes: 'btn-primary'},
+ {text: _t("Close"), close: true},
+ ],
+ };
+ this._super(parent, options);
+ this.records = {};
+ this.record = record;
+ this.defaultExportFields = defaultExportFields;
+ this.groupby = groupedBy;
+ this.exports = new data.DataSetSearch(this, 'ir.exports', this.record.getContext());
+ this.rowIndex = 0;
+ this.rowIndexLevel = 0;
+ this.isCompatibleMode = false;
+ this.domain = activeDomain || this.record.domain;
+ this.idsToExport = activeDomain ? false: idsToExport;
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ var self = this;
+ var proms = [this._super.apply(this, arguments)];
+
+ // The default for the ".modal_content" element is "max-height: 100%;"
+ // but we want it to always expand to "height: 100%;" for this modal.
+ // This can be achieved thanks to CSS modification without touching
+ // the ".modal-content" rules... but not with Internet explorer (11).
+ this.$modal.find('.modal-content').css('height', '100%');
+
+ this.$fieldsList = this.$('.o_fields_list');
+
+ proms.push(this._rpc({route: '/web/export/formats'}).then(doSetupExportFormats));
+ proms.push(this._onChangeCompatibleInput().then(function () {
+ _.each(self.defaultExportFields, function (field) {
+ var record = self.records[field];
+ self._addField(record.id, record.string);
+ });
+ }));
+
+ proms.push(this._showExportsList());
+
+ // Bind sortable events after Dialog is open
+ this.opened().then(function () {
+ self.$('.o_fields_list').sortable({
+ axis: 'y',
+ handle: '.o_short_field',
+ forcePlaceholderSize: true,
+ placeholder: 'o-field-placeholder',
+ update: self.proxy('_resetTemplateField'),
+ });
+ });
+ return Promise.all(proms);
+
+ function doSetupExportFormats(formats) {
+ var $fmts = self.$('.o_export_format');
+
+ _.each(formats, function (format) {
+ var $radio = $('<input/>', {type: 'radio', value: format.tag, name: 'o_export_format_name', class: 'form-check-input', id: 'o_radio' + format.label});
+ var $label = $('<label/>', {html: format.label, class: 'form-check-label', for: 'o_radio' + format.label});
+
+ if (format.error) {
+ $radio.prop('disabled', true);
+ $label.html(_.str.sprintf("%s — %s", format.label, format.error));
+ }
+
+ $fmts.append($("<div class='radio form-check form-check-inline pl-4'></div>").append($radio, $label));
+ });
+
+ self.$exportFormatInputs = $fmts.find('input');
+ self.$exportFormatInputs.filter(':enabled').first().prop('checked', true);
+ }
+ },
+
+ /**
+ * Export all data with default values (fields, domain)
+ */
+ export() {
+ let exportedFields = this.defaultExportFields.map(field => ({
+ name: field,
+ label: this.record.fields[field].string,
+ store: this.record.fields[field].store,
+ type: this.record.fields[field].type,
+ }));
+ this._exportData(exportedFields, 'xlsx', false);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Add the field in the export list
+ *
+ * @private
+ * @param {string} fieldID
+ * @param {string} label
+ */
+ _addField: function (fieldID, label) {
+ var $fieldList = this.$('.o_fields_list');
+ if (!$fieldList.find(".o_export_field[data-field_id='" + fieldID + "']").length) {
+ $fieldList.append(
+ $('<li>', {'class': 'o_export_field', 'data-field_id': fieldID}).append(
+ $('<span>', {'class': "fa fa-arrows o_short_field mx-1"}),
+ label.trim(),
+ $('<span>', {'class': 'fa fa-trash m-1 pull-right o_remove_field', 'title': _t("Remove field")})
+ )
+ );
+ }
+ },
+
+ /**
+ * Submit the user data and export the file
+ *
+ * @private
+ */
+ _exportData(exportedFields, exportFormat, idsToExport) {
+
+ if (_.isEmpty(exportedFields)) {
+ Dialog.alert(this, _t("Please select fields to export..."));
+ return;
+ }
+ if (this.isCompatibleMode) {
+ exportedFields.unshift({ name: 'id', label: _t('External ID') });
+ }
+
+ framework.blockUI();
+ this.getSession().get_file({
+ url: '/web/export/' + exportFormat,
+ data: {
+ data: JSON.stringify({
+ model: this.record.model,
+ fields: exportedFields,
+ ids: idsToExport,
+ domain: this.domain,
+ groupby: this.groupby,
+ context: pyUtils.eval('contexts', [this.record.getContext()]),
+ import_compat: this.isCompatibleMode,
+ })
+ },
+ complete: framework.unblockUI,
+ error: (error) => this.call('crash_manager', 'rpc_error', error),
+ });
+ },
+ /**
+ * @private
+ * @returns {string[]} exportFields
+ */
+ _getFields: function () {
+ var exportFields = this.$('.o_export_field').map(function () {
+ return $(this).data('field_id');
+ }).get();
+ if (exportFields.length === 0) {
+ Dialog.alert(this, _t("Please select fields to save export list..."));
+ }
+ return exportFields;
+ },
+ /**
+ * Fetch the field info for the relational field. This method will be
+ * invoked when the user expands the relational field from keyboard/mouse.
+ *
+ * @private
+ * @param {Object} record
+ */
+ _onExpandAction: function (record) {
+ var self = this;
+ if (!record.children) {
+ return;
+ }
+
+ var model = record.params.model;
+ var prefix = record.params.prefix;
+ var name = record.params.name;
+ var excludeFields = [];
+ if (record.relation_field) {
+ excludeFields.push(record.relation_field);
+ }
+
+ if (!record.loaded) {
+ this._rpc({
+ route: '/web/export/get_fields',
+ params: {
+ model: model,
+ prefix: prefix,
+ parent_name: name,
+ import_compat: this.isCompatibleMode,
+ parent_field_type: record.field_type,
+ parent_field: record.params.parent_field,
+ exclude: excludeFields,
+ },
+ }).then(function (results) {
+ record.loaded = true;
+ self._onShowData(results, record.id);
+ });
+ } else {
+ this._showContent(record.id);
+ }
+ },
+ /**
+ * After the fetching the fields info for the relational field, this method
+ * will render a list of a field for expanded relational field.
+ *
+ * @private
+ * @param {Object[]} records
+ * @param {string} expansion
+ */
+ _onShowData: function (records, expansion) {
+ var self = this;
+ if (expansion) {
+ this.$('.o_export_tree_item[data-id="' + expansion + '"]')
+ .addClass('show')
+ .find('.o_expand_parent')
+ .toggleClass('fa-chevron-right fa-chevron-down')
+ .next()
+ .after(QWeb.render('Export.TreeItems', {fields: records, debug: config.isDebug()}));
+ } else {
+ this.$('.o_left_field_panel').empty().append(
+ $('<div/>').addClass('o_field_tree_structure')
+ .append(QWeb.render('Export.TreeItems', {fields: records, debug: config.isDebug()}))
+ );
+ }
+
+ _.extend(this.records, _.object(_.pluck(records, 'id'), records));
+ this.$records = this.$('.o_export_tree_item');
+ this.$records.each(function (i, el) {
+ var $el = $(el);
+ $el.find('.o_tree_column').first().toggleClass('o_required', !!self.records[$el.data('id')].required);
+ });
+ },
+ /**
+ * @private
+ */
+ _addNewTemplate: function () {
+ this.$('.o_exported_lists').addClass('d-none');
+
+ this.$(".o_save_list")
+ .show()
+ .find(".o_save_list_name")
+ .val("")
+ .focus();
+ },
+ /**
+ * @private
+ */
+ _resetTemplateField: function () {
+ this.$('.o_exported_lists_select').val("");
+ this.$('.o_delete_exported_list').addClass('d-none');
+ this.$('.o_exported_lists').removeClass('d-none');
+
+ this.$(".o_save_list")
+ .hide()
+ .find(".o_save_list_name").val("");
+ },
+ /**
+ * If relational fields info is already fetched then this method is
+ * used to display fields.
+ *
+ * @private
+ * @param {string} fieldID
+ */
+ _showContent: function (fieldID) {
+ var $item = this.$('.o_export_tree_item[data-id="' + fieldID + '"]');
+ $item.toggleClass('show');
+ var isOpen = $item.hasClass('show');
+
+ $item.children('.o_expand_parent').toggleClass('fa-chevron-down', !!isOpen).toggleClass('fa-chevron-right', !isOpen);
+
+ var $childField = $item.find('.o_export_tree_item');
+ var childLength = (fieldID.split('/')).length + 1;
+ for (var i = 0 ; i < $childField.length ; i++) {
+ var $child = $childField.eq(i);
+ if (!isOpen) {
+ $child.hide();
+ } else if (childLength === $childField.eq(i).data('id').split('/').length) {
+ if ($child.hasClass('show')) {
+ $child.removeClass('show');
+ $child.children('.o_expand_parent').removeClass('fa-chevron-down').addClass('fa-chevron-right');
+ }
+ $child.show();
+ }
+ }
+ },
+ /**
+ * Fetches the saved export list for the current model
+ *
+ * @private
+ * @returns {Deferred}
+ */
+ _showExportsList: function () {
+ var self = this;
+ if (this.$('.o_exported_lists_select').is(':hidden')) {
+ this.$('.o_exported_lists').show();
+ return Promise.resolve();
+ }
+
+ return this._rpc({
+ model: 'ir.exports',
+ method: 'search_read',
+ fields: ['name'],
+ domain: [['resource', '=', this.record.model]]
+ }).then(function (exportList) {
+ self.$('.o_exported_lists').append(QWeb.render('Export.SavedList', {
+ existing_exports: exportList,
+ }));
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * This method will fill fields to export when user change exported field list
+ *
+ * @private
+ */
+ _onChangeExportList: function () {
+ var self = this;
+ var exportID = this.$('.o_exported_lists_select option:selected').val();
+ this.$('.o_delete_exported_list').toggleClass('d-none', !exportID);
+ if (exportID && exportID !== 'new_template') {
+ this.$('.o_fields_list').empty();
+ this._rpc({
+ route: '/web/export/namelist',
+ params: {
+ model: this.record.model,
+ export_id: parseInt(exportID, 10),
+ },
+ }).then(function (fieldList) {
+ _.each(fieldList, function (field) {
+ self._addField(field.name, field.label);
+ });
+ });
+ } else if (exportID === 'new_template') {
+ self._addNewTemplate();
+ }
+ },
+ /**
+ * @private
+ * @returns {Deferred}
+ */
+ _onChangeCompatibleInput: function () {
+ var self = this;
+ this.isCompatibleMode = this.$('.o_import_compat input').is(':checked');
+
+ this.$('.o_field_tree_structure').remove();
+ this._resetTemplateField();
+ return this._rpc({
+ route: '/web/export/get_fields',
+ params: {
+ model: this.record.model,
+ import_compat: this.isCompatibleMode,
+ },
+ }).then(function (records) {
+ var compatibleFields = _.map(records, function (record) { return record.id; });
+ self._onShowData(records);
+ self.$('.o_fields_list').empty();
+
+ _.chain(self.$fieldsList.find('.o_export_field'))
+ .map(function (field) { return $(field).data('field_id'); })
+ .union(self.defaultExportFields)
+ .intersection(compatibleFields)
+ .each(function (field) {
+ var record = _.find(records, function (rec) {
+ return rec.id === field;
+ });
+ self._addField(record.id, record.string);
+ });
+ self.$('#o-export-search-filter').val('');
+ });
+ },
+ /**
+ * Add a field to export list
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onClickAddField: function(ev) {
+ ev.stopPropagation();
+ var $field = $(ev.currentTarget);
+ this._resetTemplateField();
+ this._addField($field.closest('.o_export_tree_item').data('id'), $field.closest('.o_tree_column').text());
+ },
+ /**
+ * Delete selected export list item from the saved export list
+ *
+ * @private
+ */
+ _onClickDeleteExportListBtn: function () {
+ var self = this;
+ var selectExp = this.$('.o_exported_lists_select option:selected');
+ var options = {
+ confirm_callback: function () {
+ if (selectExp.val()) {
+ self.exports.unlink([parseInt(selectExp.val(), 10)]);
+ selectExp.remove();
+ if (self.$('.o_exported_lists_select option').length <= 1) {
+ self.$('.o_exported_lists').hide();
+ }
+ }
+ }
+ };
+ Dialog.confirm(this, _t("Do you really want to delete this export template?"), options);
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onClickExpand: function (ev) {
+ this._onExpandAction(this.records[$(ev.target).closest('.o_export_tree_item').data('id')]);
+ },
+ /**
+ * Remove selected field from export field list
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onClickRemoveField: function (ev) {
+ $(ev.currentTarget).closest('.o_export_field').remove();
+ this._resetTemplateField();
+ },
+ /**
+ * This method will create a record in 'ir.exports' model with list of
+ * selected fields.
+ *
+ * @private
+ */
+ _onClickSaveListBtn: function () {
+ var self = this;
+ var $saveList = this.$('.o_save_list');
+
+ var value = $saveList.find('input').val();
+ if (!value) {
+ Dialog.alert(this, _t("Please enter save field list name"));
+ return;
+ }
+
+ var fields = this._getFields();
+ if (fields.length === 0) {
+ return;
+ }
+
+ $saveList.hide();
+
+ this.exports.create({
+ name: value,
+ resource: this.record.model,
+ export_fields: _.map(fields, function (field) {
+ return [0, 0, { name: field }];
+ }),
+ }).then(function (exportListID) {
+ if (!exportListID) {
+ return;
+ }
+ var $select = self.$('.o_exported_lists_select');
+ if ($select.length === 0 || $select.is(':hidden')) {
+ self._showExportsList();
+ }
+ $select.append(new Option(value, exportListID));
+ self.$('.o_exported_lists').removeClass('d-none');
+ $select.val(exportListID);
+ });
+ },
+ /**
+ * @private
+ * @param ev
+ */
+ _onClickTreeItem: function (ev) {
+ ev.stopPropagation();
+ var $elem = $(ev.currentTarget);
+
+ var rowIndex = $elem.prevAll('.o_export_tree_item').length;
+ var rowIndexLevel = $elem.parents('.o_export_tree_item').length;
+
+ if (ev.shiftKey && rowIndexLevel === this.rowIndexLevel) {
+ var minIndex = Math.min(rowIndex, this.rowIndex);
+ var maxIndex = Math.max(rowIndex, this.rowIndex);
+
+ this.$records.filter(function () { return ($elem.parent()[0] === $(this).parent()[0]); })
+ .slice(minIndex, maxIndex + 1)
+ .addClass('o_selected')
+ .filter(':not(:last)')
+ .each(processChildren);
+ }
+
+ this.rowIndex = rowIndex;
+ this.rowIndexLevel = rowIndexLevel;
+
+ if (ev.ctrlKey) {
+ $elem.toggleClass('o_selected').focus();
+ } else if (ev.shiftKey) {
+ $elem.addClass('o_selected').focus();
+ } else {
+ this.$('.o_selected').removeClass('o_selected');
+ $elem.addClass('o_selected').focus();
+ }
+
+ function processChildren() {
+ var $child = $(this);
+ if ($child.hasClass('show')) {
+ $child.children('.o_export_tree_item')
+ .addClass('o_selected')
+ .each(processChildren);
+ }
+ }
+ },
+ /**
+ * Submit the user data and export the file
+ *
+ * @private
+ */
+ _onExportData() {
+ let exportedFields = this.$('.o_export_field').map((i, field) => ({
+ name: $(field).data('field_id'),
+ label: field.textContent,
+ }
+ )).get();
+ let exportFormat = this.$exportFormatInputs.filter(':checked').val();
+ this._exportData(exportedFields, exportFormat, this.idsToExport);
+ },
+ /**
+ * Add a field to export field list on double click
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onDblclickTreeItem: function (ev) {
+ var self = this;
+ this._resetTemplateField();
+ function addElement(el) {
+ self._addField(el.getAttribute('data-id'), el.querySelector('.o_tree_column').textContent);
+ }
+ var target = ev.currentTarget;
+ target.classList.remove('o_selected');
+ // Add parent fields to export
+ [].reverse.call($(target).parents('.o_export_tree_item')).each(function () {
+ addElement(this);
+ });
+ // add field itself
+ addElement(target);
+ },
+ /**
+ * @private
+ * @param ev
+ */
+ _onKeydownSaveList: function (ev) {
+ if (ev.keyCode === $.ui.keyCode.ENTER) {
+ this._onClickSaveListBtn();
+ }
+ },
+ /**
+ * Handles the keyboard navigation for the fields
+ *
+ * @private
+ * @param ev
+ */
+ _onKeydownTreeItem: function (ev) {
+ ev.stopPropagation();
+ var $el = $(ev.currentTarget);
+ var record = this.records[$el.data('id')];
+
+ switch (ev.keyCode || ev.which) {
+ case $.ui.keyCode.LEFT:
+ if ($el.hasClass('show')) {
+ this._onExpandAction(record);
+ }
+ break;
+ case $.ui.keyCode.RIGHT:
+ if (!$el.hasClass('show')) {
+ this._onExpandAction(record);
+ }
+ break;
+ case $.ui.keyCode.UP:
+ var $prev = $el.prev('.o_export_tree_item');
+ if ($prev.length === 1) {
+ while ($prev.hasClass('show')) {
+ $prev = $prev.children('.o_export_tree_item').last();
+ }
+ } else {
+ $prev = $el.parent('.o_export_tree_item');
+ if ($prev.length === 0) {
+ break;
+ }
+ }
+
+ $el.removeClass('o_selected').blur();
+ $prev.addClass("o_selected").focus();
+ break;
+ case $.ui.keyCode.DOWN:
+ var $next;
+ if ($el.hasClass('show')) {
+ $next = $el.children('.o_export_tree_item').first();
+ } else {
+ $next = $el.next('.o_export_tree_item');
+ if ($next.length === 0) {
+ $next = $el.parent('.o_export_tree_item').next('.o_export_tree_item');
+ if ($next.length === 0) {
+ break;
+ }
+ }
+ }
+
+ $el.removeClass('o_selected').blur();
+ $next.addClass('o_selected').focus();
+ break;
+ }
+ },
+ /**
+ * Search fields from a field list.
+ *
+ * @private
+ */
+ _onSearchInput: function (ev) {
+ var searchText = $(ev.currentTarget).val().trim().toUpperCase();
+ if (!searchText) {
+ this.$('.o_no_match').remove();
+ this.$(".o_export_tree_item").show();
+ this.$(".o_export_tree_item.haschild:not(.show) .o_export_tree_item").hide();
+ return;
+ }
+
+ var matchItems = this.$(".o_tree_column").filter(function () {
+ var title = this.getAttribute('title');
+ return this.innerText.toUpperCase().indexOf(searchText) >= 0
+ || title && title.toUpperCase().indexOf(searchText) >= 0;
+ }).parent();
+ this.$(".o_export_tree_item").hide();
+ if (matchItems.length) {
+ this.$('.o_no_match').remove();
+ _.each(matchItems, function (col) {
+ var $col = $(col);
+ $col.show();
+ $col.parents('.haschild.show').show();
+ if (!$col.parent().hasClass('show') && !$col.parent().hasClass('o_field_tree_structure')) {
+ $col.hide();
+ }
+ });
+ } else if (!this.$('.o_no_match').length) {
+ this.$(".o_field_tree_structure").append($("<h3/>", {
+ class: 'text-center text-muted mt-5 o_no_match',
+ text: _t("No match found.")
+ }));
+ }
+ },
+});
+
+return DataExport;
+
+});