diff --git a/addons/base_import/static/src/js/import_action.js b/addons/base_import/static/src/js/import_action.js index f5374be0571..c2fb9e1a362 100644 --- a/addons/base_import/static/src/js/import_action.js +++ b/addons/base_import/static/src/js/import_action.js @@ -4,8 +4,10 @@ odoo.define('base_import.import', function (require) { var AbstractAction = require('web.AbstractAction'); var config = require('web.config'); var core = require('web.core'); +var Dialog = require('web.Dialog'); var session = require('web.session'); var time = require('web.time'); +var fieldUtils = require('web.field_utils'); var QWeb = core.qweb; var _t = core._t; @@ -93,7 +95,7 @@ var DataImport = AbstractAction.extend({ ], events: { 'change .oe_import_file': 'loaded_file', - 'change input.oe_import_has_header, .js_import_options input': 'settings_changed', + 'change input.oe_import_has_header, .oe_import_sheet': 'settings_changed', 'change input.oe_import_advanced_mode': function (e) { this.do_not_change_match = true; this['settings_changed'](); @@ -360,7 +362,18 @@ var DataImport = AbstractAction.extend({ onfile_loaded: function (event, from, to, arg) { // arg is null if reload -> don't reset partial import if (arg != null ) { + var savedSkipLines = 0; + var isPartialEnabled = this.$('.oe_import').hasClass('o_import_partial_mode'); + if (isPartialEnabled) { + savedSkipLines = this.$('#oe_import_row_start').val(); + } this.toggle_partial(null); + if (isPartialEnabled && savedSkipLines) { + // If partial mode was already enabled, we want to keep the 'start at line' parameter + // This can help the end user when he has partially imported a file and wants to make + // some modifications into it before re-uploading and resuming the upload. + this.$('#oe_import_row_start').val(savedSkipLines); + } } this.$buttons.filter('.o_import_import, .o_import_validate').addClass('d-none'); @@ -434,7 +447,7 @@ var DataImport = AbstractAction.extend({ onpreview_success: function (event, from, to, result) { var self = this; this.$buttons.filter('.oe_import_file') - .text(_t('Load New File')) + .text(_t('Load File')) .removeClass('btn-primary').addClass('btn-secondary') .blur(); this.$buttons.filter('.o_import_import, .o_import_validate').removeClass('d-none'); @@ -445,7 +458,8 @@ var DataImport = AbstractAction.extend({ this.$('.oe_import_grid').html(QWeb.render('ImportView.preview', result)); // Activate the batch configuration panel only of the file length > 100. (In order to let the user choose // the batch size even for medium size file. Could be useful to reduce the batch size for complex models). - this.$('.o_import_batch').toggleClass('d-none', !(result.file_length > 100)); + this.fileLength = result.file_length; + this.$('.o_import_batch').toggleClass('d-none', !(this.fileLength > 100)); this.$('.o_import_batch_alert').toggleClass('d-none', !result.batch); var messages = []; @@ -642,6 +656,50 @@ var DataImport = AbstractAction.extend({ this.$form.removeClass('oe_import_error'); }, + /** + * Called at the start of every batch to update the progress bar display. + * It also computes an estimated time left based on: starting time of the whole process, + * percentage done so far and remaining records. + */ + _onBatchStart: function () { + var recordsDone = this.batchSize * (this.currentBatchNumber - 1); + var percentage = parseInt(recordsDone / this.totalToImport * 100); + $('.o_import_progress_dialog') + .find('.progress-bar') + .text(percentage + "%") + .attr('aria-valuenow', percentage) + .css('width', percentage + '%'); + $('.o_import_progress_dialog') + .find('.o_import_progress_dialog_batch_count') + .text(this.currentBatchNumber); + if (percentage !== 0) { + // e.g: it took 1 seconds (1000 millis) to import 33% + // -> there is (1000) * ((100 - 33) / 33) / 60000 minutes left + // -> 1000 * (66 / 33) / 60000 -> 2000 / 60000 -> 0.03 minute left (2 seconds) left + var estimatedTimeLeftMinutes = ((Date.now() - this.importStartTime) * ((100 - percentage) / percentage)) / 60000; + $('.o_import_progress_dialog_time_left').removeClass('d-none'); + $('.o_import_progress_dialog_time_left_text') + .text(fieldUtils.format.float_time(estimatedTimeLeftMinutes)); + } + }, + + /** + * Called when the user manually interrupts the import during a batched import. + * Sets 'stopImport' to true, which will stop the process when the current batch is done. + * + * @private + * @param {MouseEvent} event + */ + _onStopImport: function (event) { + var $currentTarget = $(event.currentTarget); + $currentTarget.enable(false); + $currentTarget + .closest('.o_import_progress_dialog') + .find('.o_import_progress_dialog_stop, .o_import_progress_dialog_batch') + .toggleClass('d-none'); + this.stopImport = true; + }, + /** * This method is called when selecting a new mapping field, or when changing/removing an already selected mapping * field. It will check if multiple field columns are/were mapped on the same field. @@ -797,21 +855,31 @@ var DataImport = AbstractAction.extend({ {}, this.parent_context, {tracking_disable: tracking_disable} ); - var self = this; - $.blockUI({message: QWeb.render('Throbber')}); - $(document.body).addClass('o_ui_blocked'); - var opts = this.import_options(); - var $el = $('.oe_throbber_message'); - var msg = kwargs.dryrun ? _t("%d records tested...") - : _t("%d records successfully imported..."); - opts.callback = function (count) { - if (count) { - $el.text(_.str.sprintf(msg, count)); - } else { - $el.text(kwargs.dryrun ? _t("Testing...") : _t("Importing...")); - } - }; + this.importStartTime = Date.now(); + this.stopImport = false; + this.totalToImport = this.fileLength - parseInt(this.$('#oe_import_row_start').val()); + this.batchSize = parseInt(this.$('#oe_import_batch_limit').val() || 0); + var isBatch = this.batchSize !== 0 && this.totalToImport > this.batchSize; + var totalSteps = isBatch ? Math.floor(this.totalToImport / this.batchSize) + 1 : 1; + this.currentBatchNumber = 1; + + $.blockUI({ + message: QWeb.render( + 'base_import.progressDialog', { + importMode: kwargs.dryrun ? _t('Testing') : _t('Importing'), + isBatch: isBatch, + totalSteps: totalSteps, + } + ) + }); + $(document.body).addClass('o_ui_blocked'); + + $('.o_import_progress_dialog') + .find('.o_progress_stop_import') + .on('click', this._onStopImport.bind(this)); + + var opts = this.import_options(); return this._batchedImport(opts, [this.id, fields, columns], kwargs, {done: 0, prev: 0}) .then(null, function (reason) { @@ -862,8 +930,17 @@ var DataImport = AbstractAction.extend({ * @private */ _batchedImport: function (opts, args, kwargs, rec) { - opts.callback && opts.callback(rec.done || 0); var self = this; + opts.callback && opts.callback(this); + this._onBatchStart(); + this.currentBatchNumber += 1; + + if (this.stopImport) { + $(document.body).removeClass('o_ui_blocked'); + $.unblockUI(); + return Promise.resolve({}); + } + return this._rpc({ model: 'base_import.import', method: 'execute_import', @@ -906,7 +983,7 @@ var DataImport = AbstractAction.extend({ return names[0] || names[1]; }), ids: (results.ids || []).concat(r2.ids || []), - messages: results.messages.concat(r2.messages), + messages: r2.messages ? results.messages.concat(r2.messages) : results.messages, skip: r2.skip || results.nextrow, nextrow: r2.nextrow } @@ -922,8 +999,17 @@ var DataImport = AbstractAction.extend({ var self = this; var prom = this.call_import({ dryrun: false }); prom.then(function (results) { - var message = results.messages; - if (!_.any(message, function (message) { + if (self.stopImport) { + var recordsImported = results.ids ? results.ids.length : 0; + if (recordsImported) { + self.$('#oe_import_row_start').val((results.skip || 0) + 1); + self.displayNotification({ message: _.str.sprintf( + _t("%d records successfully imported"), + recordsImported + )}); + } + self['import_interrupted'](results); + } else if (!_.any(results.messages, function (message) { return message.type === 'error'; })) { self['import_succeeded'](results); return; @@ -950,13 +1036,16 @@ var DataImport = AbstractAction.extend({ var error_type = "warning"; var errorMessages = results.messages; - var no_messages = _.isEmpty(errorMessages); - if (no_messages) { + + if (_.isEmpty(errorMessages) && event !== 'import_interrupted') { errorMessages.push({ type: 'info', message: _t("Everything seems valid.") }); error_type = false; + } else if (event === 'import_interrupted' && results.ids) { + this.toggle_partial(results); + error_type = false; } else if (event === 'import_failed' && results.ids) { // both ids in a failed import -> partial import this.toggle_partial(results); @@ -1162,6 +1251,7 @@ StateMachine.create({ { name: 'validated', from: 'validating', to: 'results' }, { name: 'import', from: ['preview_success', 'results'], to: 'importing' }, { name: 'import_succeeded', from: 'importing', to: 'imported'}, + { name: 'import_interrupted', from: 'importing', to: 'results' }, { name: 'import_failed', from: 'importing', to: 'results' } ], }); diff --git a/addons/base_import/static/src/scss/base_import.scss b/addons/base_import/static/src/scss/base_import.scss index c108fcabe77..d7de1ace8bc 100644 --- a/addons/base_import/static/src/scss/base_import.scss +++ b/addons/base_import/static/src/scss/base_import.scss @@ -68,6 +68,9 @@ overflow: hidden; text-overflow: ellipsis; } + .o_import_batch_alert { + color: #46646d; + } } &.oe_import_preview .oe_import_grid { @@ -234,3 +237,24 @@ } } } + +/* Import Progress Bar */ +.o_import_progress_dialog { + .progress { + height: 1.5rem; + border-radius: 4px; + font-size: 1rem; + + .progress-bar { + width: 0%; + min-width: 3%; + } + } + + .o_progress_stop_import { + cursor: pointer; + &:hover { + transform: scale(1.2); + } + } +} diff --git a/addons/base_import/static/src/xml/base_import.xml b/addons/base_import/static/src/xml/base_import.xml index 222ff129fcf..5198eac0d8f 100644 --- a/addons/base_import/static/src/xml/base_import.xml +++ b/addons/base_import/static/src/xml/base_import.xml @@ -18,12 +18,6 @@ -
- Click 'Resume' to proceed with the import, resuming at line - 0.
- You can test or reload your file before resuming the import. -
-

@@ -225,6 +219,11 @@ field corresponding to the column. This makes imports simpler especially when the file has many columns.

+
+ Click 'Resume' to proceed with the import, resuming at line + 0.
+ You can test or reload your file before resuming the import. +
@@ -244,7 +243,7 @@ -
+
@@ -270,15 +269,15 @@

Batch Import

-
- The file will be imported by batches. +
+ The file will be imported by batches
-
+
-
@@ -315,4 +314,33 @@ lines composed only of empty cells">
+ +
+ +
+
+ batch 1 out of ... +
+ Estimated time left: + + minutes +
+
+ + Finalizing current batch before interrupting... + +
+
+
+ 0% +
+
+ +
+
+
+ ... +
+