[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:
committed by
Aurélien Warnon
co-authored by
Aurélien Warnon
parent
d1dda79c2f
commit
409ccc833d
@@ -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' }
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user