odoo.define('base_accounting_kit.ReconciliationRenderer', function (require) {
"use strict";
var Widget = require('web.Widget');
var FieldManagerMixin = require('web.FieldManagerMixin');
var relational_fields = require('web.relational_fields');
var basic_fields = require('web.basic_fields');
var core = require('web.core');
var time = require('web.time');
var session = require('web.session');
var qweb = core.qweb;
var _t = core._t;
/**
* rendering of the bank statement action contains progress bar, title and
* auto reconciliation button
*/
var StatementRenderer = Widget.extend(FieldManagerMixin, {
template: 'reconciliation.statement',
events: {
'click *[rel="do_action"]': '_onDoAction',
'click button.js_load_more': '_onLoadMore',
},
/**
* @override
*/
init: function (parent, model, state) {
this._super(parent);
this.model = model;
this._initialState = state;
},
/**
* display iniial state and create the name statement field
*
* @override
*/
start: function () {
var self = this;
var defs = [this._super.apply(this, arguments)];
this.time = Date.now();
this.$progress = $('');
return Promise.all(defs);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/*
* hide the button to load more statement line
*/
hideLoadMoreButton: function (show) {
if (!show) {
this.$('.js_load_more').show();
}
else {
this.$('.js_load_more').hide();
}
},
showRainbowMan: function (state) {
if (this.model.display_context !== 'validate') {
return
}
var dt = Date.now()-this.time;
var $done = $(qweb.render("reconciliation.done", {
'duration': moment(dt).utc().format(time.getLangTimeFormat()),
'number': state.valuenow,
'timePerTransaction': Math.round(dt/1000/state.valuemax),
'context': state.context,
}));
$done.find('*').addClass('o_reward_subcontent');
$done.find('.button_close_statement').click(this._onCloseBankStatement.bind(this));
$done.find('.button_back_to_statement').click(this._onGoToBankStatement.bind(this));
// display rainbowman after full reconciliation
if (session.show_effect) {
this.trigger_up('show_effect', {
type: 'rainbow_man',
fadeout: 'no',
message: $done,
});
this.$el.css('min-height', '450px');
} else {
$done.appendTo(this.$el);
}
},
/**
* update the statement rendering
*
* @param {object} state - statement data
* @param {integer} state.valuenow - for the progress bar
* @param {integer} state.valuemax - for the progress bar
* @param {string} state.title - for the progress bar
* @param {[object]} [state.notifications]
*/
update: function (state) {
var self = this;
this._updateProgressBar(state);
if (state.valuenow === state.valuemax && !this.$('.done_message').length) {
this.showRainbowMan(state);
}
if (state.notifications) {
this._renderNotifications(state.notifications);
}
},
_updateProgressBar: function(state) {
this.$progress.find('.valuenow').text(state.valuenow);
this.$progress.find('.valuemax').text(state.valuemax);
this.$progress.find('.progress-bar')
.attr('aria-valuenow', state.valuenow)
.attr('aria-valuemax', state.valuemax)
.css('width', (state.valuenow/state.valuemax*100) + '%');
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* render the notifications
*
* @param {[object]} notifications
*/
_renderNotifications: function(notifications) {
this.$(".notification_area").empty();
for (var i=0; i')
.appendTo(this.$('thead .cell_info_popover'))
.attr("data-content", qweb.render('reconciliation.line.statement_line.details', {'state': this._initialState}));
this.$el.popover({
'selector': '.line_info_button',
'placement': 'left',
'container': this.$el,
'html': true,
// disable bootstrap sanitizer because we use a table that has been
// rendered using qweb.render so it is safe and also because sanitizer escape table by default.
'sanitize': false,
'trigger': 'hover',
'animation': false,
'toggle': 'popover'
});
var def2 = this._super.apply(this, arguments);
return Promise.all([def1, def2, def3, def4]);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* update the statement line rendering
*
* @param {object} state - statement line
*/
update: function (state) {
var self = this;
// isValid
var to_check_checked = !!(state.to_check);
this.$('caption .o_buttons button.o_validate').toggleClass('d-none', !!state.balance.type && !to_check_checked);
this.$('caption .o_buttons button.o_reconcile').toggleClass('d-none', state.balance.type <= 0 || to_check_checked);
this.$('caption .o_buttons .o_no_valid').toggleClass('d-none', state.balance.type >= 0 || to_check_checked);
self.$('caption .o_buttons button.o_validate').toggleClass('text-warning', to_check_checked);
// partner_id
this._makePartnerRecord(state.st_line.partner_id, state.st_line.partner_name).then(function (recordID) {
self.fields.partner_id.reset(self.model.get(recordID));
self.$el.attr('data-partner', state.st_line.partner_id);
});
// mode
this.$el.data('mode', state.mode).attr('data-mode', state.mode);
this.$('.o_notebook li a').attr('aria-selected', false);
this.$('.o_notebook li a').removeClass('active');
this.$('.o_notebook .tab-content .tab-pane').removeClass('active');
this.$('.o_notebook li a[href*="notebook_page_' + state.mode + '"]').attr('aria-selected', true);
this.$('.o_notebook li a[href*="notebook_page_' + state.mode + '"]').addClass('active');
this.$('.o_notebook .tab-content .tab-pane[id*="notebook_page_' + state.mode + '"]').addClass('active');
this.$('.create, .match').each(function () {
$(this).removeAttr('style');
});
// reconciliation_proposition
var $props = this.$('.accounting_view tbody').empty();
// Search propositions that could be a partial credit/debit.
var props = [];
var balance = state.balance.amount_currency;
_.each(state.reconciliation_proposition, function (prop) {
if (prop.display) {
props.push(prop);
}
});
_.each(props, function (line) {
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state, 'proposition': true}));
if (!isNaN(line.id)) {
$('')
.appendTo($line.find('.cell_info_popover'))
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line}));
}
$props.append($line);
});
// mv_lines
var matching_modes = self.model.modes.filter(x => x.startsWith('match'));
for (let i = 0; i < matching_modes.length; i++) {
var stateMvLines = state['mv_lines_'+matching_modes[i]] || [];
var recs_count = stateMvLines.length > 0 ? stateMvLines[0].recs_count : 0;
var remaining = state['remaining_' + matching_modes[i]];
var $mv_lines = this.$('div[id*="notebook_page_' + matching_modes[i] + '"] .match table tbody').empty();
this.$('.o_notebook li a[href*="notebook_page_' + matching_modes[i] + '"]').parent().toggleClass('d-none', stateMvLines.length === 0 && !state['filter_'+matching_modes[i]]);
_.each(stateMvLines, function (line) {
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
if (!isNaN(line.id)) {
$('')
.appendTo($line.find('.cell_info_popover'))
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line}));
}
$mv_lines.append($line);
});
this.$('div[id*="notebook_page_' + matching_modes[i] + '"] .match div.load-more').toggle(remaining > 0);
this.$('div[id*="notebook_page_' + matching_modes[i] + '"] .match div.load-more span').text(remaining);
}
// balance
this.$('.popover').remove();
this.$('table tfoot').html(qweb.render("reconciliation.line.balance", {'state': state}));
// create form
if (state.createForm) {
var createPromise;
if (!this.fields.account_id) {
createPromise = this._renderCreate(state);
}
Promise.resolve(createPromise).then(function(){
var data = self.model.get(self.handleCreateRecord).data;
return self.model.notifyChanges(self.handleCreateRecord, state.createForm)
.then(function () {
// FIXME can't it directly written REPLACE_WITH ids=state.createForm.analytic_tag_ids
return self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}})
})
.then(function (){
var defs = [];
_.each(state.createForm.analytic_tag_ids, function (tag) {
defs.push(self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}}));
});
return Promise.all(defs);
})
.then(function () {
return self.model.notifyChanges(self.handleCreateRecord, {tax_ids: {operation: 'REPLACE_WITH', ids: []}})
})
.then(function (){
var defs = [];
_.each(state.createForm.tax_ids, function (tag) {
defs.push(self.model.notifyChanges(self.handleCreateRecord, {tax_ids: {operation: 'ADD_M2M', ids: tag}}));
});
return Promise.all(defs);
})
.then(function () {
var record = self.model.get(self.handleCreateRecord);
_.each(self.fields, function (field, fieldName) {
if (self._avoidFieldUpdate[fieldName]) return;
if (fieldName === "partner_id") return;
if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) {
field.reset(record);
}
if (fieldName === 'tax_ids') {
if (!state.createForm[fieldName].length || state.createForm[fieldName].length > 1) {
$('.create_force_tax_included').addClass('d-none');
}
else {
$('.create_force_tax_included').removeClass('d-none');
var price_include = state.createForm[fieldName][0].price_include;
var force_tax_included = state.createForm[fieldName][0].force_tax_included;
self.$('.create_force_tax_included input').prop('checked', force_tax_included);
self.$('.create_force_tax_included input').prop('disabled', price_include);
}
}
});
if (state.to_check) {
// Set the to_check field to true if global to_check is set
self.$('.create_to_check input').prop('checked', state.to_check).change();
}
return true;
});
});
}
this.$('.create .add_line').toggle(!!state.balance.amount_currency);
},
updatePartialAmount: function(line_id, amount) {
var $line = this.$('.mv_line[data-line-id='+line_id+']');
$line.find('.edit_amount').addClass('d-none');
$line.find('.edit_amount_input').removeClass('d-none');
$line.find('.edit_amount_input').focus();
$line.find('.edit_amount_input').val(amount);
$line.find('.line_amount').addClass('d-none');
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {jQueryElement} $el
*/
_destroyPopover: function ($el) {
var popover = $el.data('bs.popover');
if (popover) {
popover.dispose();
}
},
/**
* @private
* @param {integer} partnerID
* @param {string} partnerName
* @returns {string} local id of the dataPoint
*/
_makePartnerRecord: function (partnerID, partnerName) {
var field = {
relation: 'res.partner',
type: 'many2one',
name: 'partner_id',
};
if (partnerID) {
field.value = [partnerID, partnerName];
}
return this.model.makeRecord('account.bank.statement.line', [field], {
partner_id: {
domain: ["|", ["is_company", "=", true], ["parent_id", "=", false]],
options: {
no_open: true
}
}
});
},
/**
* create account_id, tax_ids, analytic_account_id, analytic_tag_ids, label and amount fields
*
* @private
* @param {object} state - statement line
* @returns {Promise}
*/
_renderCreate: function (state) {
var self = this;
return this.model.makeRecord('account.bank.statement.line', [{
relation: 'account.account',
type: 'many2one',
name: 'account_id',
domain: [['company_id', '=', state.st_line.company_id], ['deprecated', '=', false]],
}, {
relation: 'account.journal',
type: 'many2one',
name: 'journal_id',
domain: [['company_id', '=', state.st_line.company_id]],
}, {
relation: 'account.tax',
type: 'many2many',
name: 'tax_ids',
domain: [['company_id', '=', state.st_line.company_id]],
}, {
relation: 'account.analytic.account',
type: 'many2one',
name: 'analytic_account_id',
}, {
relation: 'account.analytic.tag',
type: 'many2many',
name: 'analytic_tag_ids',
}, {
type: 'boolean',
name: 'force_tax_included',
}, {
type: 'char',
name: 'label',
}, {
type: 'float',
name: 'amount',
}, {
type: 'char', //TODO is it a bug or a feature when type date exists ?
name: 'date',
}, {
type: 'boolean',
name: 'to_check',
}], {
account_id: {
string: _t("Account"),
},
label: {string: _t("Label")},
amount: {string: _t("Account")},
}).then(function (recordID) {
self.handleCreateRecord = recordID;
var record = self.model.get(self.handleCreateRecord);
self.fields.account_id = new relational_fields.FieldMany2One(self,
'account_id', record, {mode: 'edit', attrs: {can_create:false}});
self.fields.journal_id = new relational_fields.FieldMany2One(self,
'journal_id', record, {mode: 'edit'});
self.fields.tax_ids = new relational_fields.FieldMany2ManyTags(self,
'tax_ids', record, {mode: 'edit', additionalContext: {append_type_to_tax_name: true}});
self.fields.analytic_account_id = new relational_fields.FieldMany2One(self,
'analytic_account_id', record, {mode: 'edit'});
self.fields.analytic_tag_ids = new relational_fields.FieldMany2ManyTags(self,
'analytic_tag_ids', record, {mode: 'edit'});
self.fields.force_tax_included = new basic_fields.FieldBoolean(self,
'force_tax_included', record, {mode: 'edit'});
self.fields.label = new basic_fields.FieldChar(self,
'label', record, {mode: 'edit'});
self.fields.amount = new basic_fields.FieldFloat(self,
'amount', record, {mode: 'edit'});
self.fields.date = new basic_fields.FieldDate(self,
'date', record, {mode: 'edit'});
self.fields.to_check = new basic_fields.FieldBoolean(self,
'to_check', record, {mode: 'edit'});
var $create = $(qweb.render("reconciliation.line.create", {'state': state, 'group_tags': self.group_tags, 'group_acc': self.group_acc}));
self.fields.account_id.appendTo($create.find('.create_account_id .o_td_field'))
.then(addRequiredStyle.bind(self, self.fields.account_id));
self.fields.journal_id.appendTo($create.find('.create_journal_id .o_td_field'));
self.fields.tax_ids.appendTo($create.find('.create_tax_id .o_td_field'));
self.fields.analytic_account_id.appendTo($create.find('.create_analytic_account_id .o_td_field'));
self.fields.analytic_tag_ids.appendTo($create.find('.create_analytic_tag_ids .o_td_field'));
self.fields.force_tax_included.appendTo($create.find('.create_force_tax_included .o_td_field'));
self.fields.label.appendTo($create.find('.create_label .o_td_field'))
.then(addRequiredStyle.bind(self, self.fields.label));
self.fields.amount.appendTo($create.find('.create_amount .o_td_field'))
.then(addRequiredStyle.bind(self, self.fields.amount));
self.fields.date.appendTo($create.find('.create_date .o_td_field'));
self.fields.to_check.appendTo($create.find('.create_to_check .o_td_field'));
self.$('.create').append($create);
function addRequiredStyle(widget) {
widget.$el.addClass('o_required_modifier');
}
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* The event on the partner m2o widget was propagated to the bank statement
* line widget, causing it to expand and the others to collapse. This caused
* the dropdown to be poorly placed and an unwanted update of this widget.
*
* @private
*/
_onStopPropagation: function(ev) {
ev.stopPropagation();
},
/**
* @private
* @param {MouseEvent} event
*/
_onCreateReconcileModel: function (event) {
event.preventDefault();
var self = this;
this.do_action({
type: 'ir.actions.act_window',
res_model: 'account.reconcile.model',
views: [[false, 'form']],
target: 'current'
},
{
on_reverse_breadcrumb: function() {self.trigger_up('reload');},
});
},
_editAmount: function (event) {
event.stopPropagation();
var $line = $(event.target);
var moveLineId = $line.closest('.mv_line').data('line-id');
this.trigger_up('partial_reconcile', {'data': {mvLineId: moveLineId, 'amount': $line.val()}});
},
_onEditAmount: function (event) {
event.preventDefault();
event.stopPropagation();
// Don't call when clicking inside the input field
if (! $(event.target).hasClass('edit_amount_input')){
var $line = $(event.target);
this.trigger_up('getPartialAmount', {'data': $line.closest('.mv_line').data('line-id')});
}
},
/**
* @private
* @param {MouseEvent} event
*/
_onEditReconcileModel: function (event) {
event.preventDefault();
var self = this;
this.do_action({
type: 'ir.actions.act_window',
res_model: 'account.reconcile.model',
views: [[false, 'list'], [false, 'form']],
view_mode: "list",
target: 'current'
},
{
on_reverse_breadcrumb: function() {self.trigger_up('reload');},
});
},
/**
* @private
* @param {OdooEvent} event
*/
_onFieldChanged: function (event) {
event.stopPropagation();
var fieldName = event.target.name;
if (fieldName === 'partner_id') {
var partner_id = event.data.changes.partner_id;
this.trigger_up('change_partner', {'data': partner_id});
} else {
if (event.data.changes.amount && isNaN(event.data.changes.amount)) {
return;
}
this.trigger_up('update_proposition', {'data': event.data.changes});
}
},
/**
* @private
*/
_onTogglePanel: function () {
if (this.$el[0].getAttribute('data-mode') == 'inactive')
this.trigger_up('change_mode', {'data': 'default'});
},
/**
* @private
*/
_onChangeTab: function(event) {
if (event.currentTarget.nodeName === 'TFOOT') {
this.trigger_up('change_mode', {'data': 'next'});
} else {
var modes = this.model.modes;
var selected_mode = modes.find(function(e) {return event.target.getAttribute('href').includes(e)});
if (selected_mode) {
this.trigger_up('change_mode', {'data': selected_mode});
}
}
},
/**
* @private
* @param {input event} event
*/
_onFilterChange: function (event) {
this.trigger_up('change_filter', {'data': _.str.strip($(event.target).val())});
},
/**
* @private
* @param {keyup event} event
*/
_onInputKeyup: function (event) {
var target_partner_id = $(event.target).parents('[name="partner_id"]');
if (target_partner_id.length === 1) {
return;
}
if(event.keyCode === 13) {
if ($(event.target).hasClass('edit_amount_input')) {
$(event.target).blur();
return;
}
var created_lines = _.findWhere(this.model.lines, {mode: 'create'});
if (created_lines && created_lines.balance.amount) {
this._onCreateProposition();
}
return;
}
if ($(event.target).hasClass('edit_amount_input')) {
if (event.type === 'keyup') {
return;
}
else {
return this._editAmount(event);
}
}
var self = this;
for (var fieldName in this.fields) {
var field = this.fields[fieldName];
if (!field.$el.is(event.target)) {
continue;
}
this._avoidFieldUpdate[field.name] = event.type !== 'focusout';
field.value = false;
field._setValue($(event.target).val()).then(function () {
self._avoidFieldUpdate[field.name] = false;
});
break;
}
},
/**
* @private
*/
_onLoadMore: function (ev) {
ev.preventDefault();
this.trigger_up('change_offset');
},
/**
* @private
* @param {MouseEvent} event
*/
_onSelectMoveLine: function (event) {
var $el = $(event.target);
$el.prop('disabled', true);
this._destroyPopover($el);
var moveLineId = $el.closest('.mv_line').data('line-id');
this.trigger_up('add_proposition', {'data': moveLineId});
},
/**
* @private
* @param {MouseEvent} event
*/
_onSelectProposition: function (event) {
var $el = $(event.target);
this._destroyPopover($el);
var moveLineId = $el.closest('.mv_line').data('line-id');
this.trigger_up('remove_proposition', {'data': moveLineId});
},
/**
* @private
* @param {MouseEvent} event
*/
_onQuickCreateProposition: function (event) {
document.activeElement && document.activeElement.blur();
this.trigger_up('quick_create_proposition', {'data': $(event.target).data('reconcile-model-id')});
},
/**
* @private
*/
_onCreateProposition: function () {
document.activeElement && document.activeElement.blur();
var invalid = [];
_.each(this.fields, function (field) {
if (!field.isValid()) {
invalid.push(field.string);
}
});
if (invalid.length) {
this.do_warn(_t("Some fields are undefined"), invalid.join(', '));
return;
}
this.trigger_up('create_proposition');
},
/**
* @private
*/
_onValidate: function () {
this.trigger_up('validate');
}
});
/**
* rendering of the manual reconciliation action contains progress bar, title
* and auto reconciliation button
*/
var ManualRenderer = StatementRenderer.extend({
template: "reconciliation.manual.statement",
});
/**
* rendering of the manual reconciliation, contains line data, proposition and
* view for 'match' mode
*/
var ManualLineRenderer = LineRenderer.extend({
template: "reconciliation.manual.line",
/**
* @override
* @param {string} handle
* @param {number} proposition id (move line id)
* @returns {Promise}
*/
removeProposition: function (handle, id) {
if (!id) {
return Promise.resolve();
}
return this._super(handle, id);
},
/**
* move the partner field
*
* @override
*/
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
return self.model.makeRecord('account.move.line', [{
relation: 'account.account',
type: 'many2one',
name: 'account_id',
value: [self._initialState.account_id.id, self._initialState.account_id.display_name],
}]).then(function (recordID) {
self.fields.title_account_id = new relational_fields.FieldMany2One(self,
'account_id',
self.model.get(recordID),
{mode: 'readonly'}
);
}).then(function () {
return self.fields.title_account_id.appendTo(self.$('.accounting_view thead td:eq(0) span:first'));
});
});
},
/**
* @override
*/
update: function (state) {
this._super(state);
var props = _.filter(state.reconciliation_proposition, {'display': true});
if (!props.length) {
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': {}, 'state': state}));
this.$('.accounting_view tbody').append($line);
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* display journal_id field
*
* @override
*/
_renderCreate: function (state) {
var self = this;
var parentPromise = this._super(state).then(function() {
self.$('.create .create_journal_id').show();
self.$('.create .create_date').removeClass('d-none');
self.$('.create .create_journal_id .o_input').addClass('o_required_modifier');
});
return parentPromise;
},
});
return {
StatementRenderer: StatementRenderer,
ManualRenderer: ManualRenderer,
LineRenderer: LineRenderer,
ManualLineRenderer: ManualLineRenderer,
};
});