[IMP] base_import: add import progress bar for batch mode

This commit adds a progress bar that is displayed during the import (or import
test). It is only displayed when importing in batch mode.

The user can decide to stop the import. If it's the case, the current
batch is finalized and the import is interrupted afterwards.

The user can easily resume the import later starting from the line where the
batch was interrupted
e.g: interrupted after 300 / 500 lines, when you resume, it will start from
line 301.
(The above behavior is not applied in test mode, we never alter the starting
line when testing).

Side note: This commit also removes the alert-info box from batch import
disclaimer, for design beauty purpose.

Task-2567591

closes odoo/odoo#73243

Related: odoo/enterprise#19467
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
Co-authored-by: Aurélien Warnon <awa@odoo.com>
This commit is contained in:
David Beguin
2021-08-23 09:02:08 +00:00
committed by Aurélien Warnon
co-authored by Aurélien Warnon
parent d1dda79c2f
commit 409ccc833d
3 changed files with 176 additions and 34 deletions
+113 -23
View File
@@ -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' }
],
});
+24
View File
@@ -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);
}
}
}
@@ -18,12 +18,6 @@
<t t-call="ImportView.data_matching"/>
</div>
<div class="o_import_partial_alert alert alert-warning d-none">
Click 'Resume' to proceed with the import, resuming at line
<span class="o_import_partial_count">0</span>.<br/>
You can test or reload your file before resuming the import.
</div>
<div class="o_view_nocontent">
<div class="o_nocontent_help">
<p class="o_view_nocontent_smiling_face">
@@ -225,6 +219,11 @@
field corresponding to the column. This makes imports
simpler especially when the file has many columns.</p>
</div>
<div class="o_import_partial_alert alert alert-warning d-none mx-2 my-2 font-weight-bold">
Click 'Resume' to proceed with the import, resuming at line
<span class="o_import_partial_count">0</span>.<br/>
You can test or reload your file before resuming the import.
</div>
<div class="oe_import_error_report px-3 py-2"></div>
<div class="table-responsive">
<table class="table-striped oe_import_grid w-100 overflow-hidden" />
@@ -244,7 +243,7 @@
<span id="oe_imported_file_extension" class="font-italic"></span>
</div>
<div class="oe_import_has_multiple_sheets js_import_options flex-column mt-2">
<div class="oe_import_has_multiple_sheets flex-column mt-2">
<label for="oe_import_sheet">Sheet:</label>
<input class="oe_import_sheet oe_import_dropdown" id="oe_import_sheet"/>
</div>
@@ -270,15 +269,15 @@
<div class="o_import_batch">
<h4 class="mt-3">Batch Import</h4>
<div class="o_import_batch_alert alert alert-info d-none">
The file will be imported by batches.
<div class="o_import_batch_alert font-weight-bold d-none pb-2">
The file will be imported by batches
</div>
<div class="d-flex">
<div class="js_import_options oe_import_options oe_import_batch_limit w-50 pr-1">
<div class="oe_import_options oe_import_batch_limit w-50 pr-1">
<label class="mb-1" for="oe_import_batch_limit">Batch limit</label>
<input class="w-100" id="oe_import_batch_limit" value="2000"/>
</div>
<div class="js_import_options oe_import_options w-50 pl-1" title="Warning: ignores the labels line, empty lines and
<div class="oe_import_options w-50 pl-1" title="Warning: ignores the labels line, empty lines and
lines composed only of empty cells">
<label class="mb-1" for="oe_import_row_start">Start at line</label>
<input class="w-100" id="oe_import_row_start" value="1"/>
@@ -315,4 +314,33 @@ lines composed only of empty cells">
</div>
</div>
</t>
<div t-name="base_import.progressDialog" class="o_import_progress_dialog text-white">
<span class="fa fa-spin fa-circle-o-notch fa-2x mb-2"/>
<div t-if="isBatch">
<div class="o_import_progress_dialog_batch">
<span t-esc="importMode"/> batch <span class="o_import_progress_dialog_batch_count">1</span> out of <span t-esc="totalSteps"/>...
<div class="o_import_progress_dialog_time_left d-none">
<span>Estimated time left:</span>
<span class="o_import_progress_dialog_time_left_text"/>
<span>minutes</span>
</div>
</div>
<span class="o_import_progress_dialog_stop d-none">
Finalizing current batch before interrupting...
</span>
<div class="d-flex align-items-center mt-2">
<div class="progress flex-grow-1">
<div class="progress-bar progress-bar-striped" role="progressbar"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<span>0%</span>
</div>
</div>
<a class="o_progress_stop_import ml-2" role="button"><i class="fa fa-close" aria-label="Stop Import"/></a>
</div>
</div>
<div t-else="">
<span t-esc="task"/>...
</div>
</div>
</templates>