flectra/addons/account/static/src/js/reconciliation/reconciliation_renderer.js

840 lines
29 KiB
JavaScript

flectra.define('account.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 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 div:first button.o_automatic_reconciliation': '_onAutoReconciliation',
'click div:first h1.statement_name': '_onClickStatementName',
'click div:first h1.statement_name_edition button': '_onValidateName',
"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 = this.$('.progress');
if (this._initialState.bank_statement_id) {
var def = this.model.makeRecord("account.bank.statement", [{
type: 'char',
name: 'name',
attrs: {string: ""},
value: this._initialState.bank_statement_id.display_name
}]).then(function (recordID) {
self.handleNameRecord = recordID;
self.name = new basic_fields.FieldChar(self,
'name', self.model.get(self.handleNameRecord),
{mode: 'edit'});
self.name.appendTo(self.$('.statement_name_edition')).then(function () {
self.name.$el.addClass('o_required_modifier');
});
self.$('.statement_name').text(self._initialState.bank_statement_id.display_name);
});
defs.push(def);
}
this.$('h1.statement_name').text(this._initialState.title);
delete this._initialState;
this.enterHandler = function (e) {
if ((e.which === 13 || e.which === 10) && (e.ctrlKey || e.metaKey)) {
this.trigger_up('validate_all_balanced');
}
}.bind(this);
$('body').on('keyup', this.enterHandler);
return $.when.apply($, defs);
},
/**
* @override
*/
destroy: function () {
this._super();
$('body').off('keyup', this.enterHandler);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/*
* hide the button to load more statement line
*/
hideLoadMoreButton: function () {
this.$('.js_load_more').hide();
},
/**
* 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.$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) + '%');
if (state.valuenow === state.valuemax && !this.$('.done_message').length) {
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('.button_close_statement').click(this._onCloseBankStatement.bind(this));
$done.find('.button_back_to_statement').click(this._onGoToBankStatement.bind(this));
this.$el.children().hide();
// display rainbowman after full reconciliation
this.trigger_up('show_effect', {
type: 'rainbow_man',
fadeout: 'no',
message: $done,
click_close: false,
});
this.$el.css('min-height', '450px');
}
if (state.notifications) {
this._renderNotifications(state.notifications);
}
this.$('h1.statement_name').text(state.title);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* render the notifications
*
* @param {[object]} notifications
*/
_renderNotifications: function(notifications) {
this.$(".notification_area").empty();
for (var i=0; i<notifications.length; i++) {
var $notification = $(qweb.render("reconciliation.notification", {
type: notifications[i].type,
message: notifications[i].message,
details: notifications[i].details,
})).hide();
$notification.appendTo(this.$(".notification_area")).slideDown(300);
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onAutoReconciliation: function () {
this.trigger_up('auto_reconciliation');
},
/**
* @private
*/
_onClickStatementName: function () {
this.$('.statement_name, .statement_name_edition').toggle();
},
/**
* @private
* Click on close bank statement button, this will
* close and then open form view of bank statement
* @param {MouseEvent} event
*/
_onCloseBankStatement: function (e) {
this.trigger_up('close_statement');
},
/**
* @private
* @param {MouseEvent} event
*/
_onDoAction: function(e) {
e.preventDefault();
var name = e.currentTarget.dataset.action_name;
var model = e.currentTarget.dataset.model;
var ids = e.currentTarget.dataset.ids.split(",").map(Number);
this.do_action({
name: name,
res_model: model,
domain: [['id', 'in', ids]],
views: [[false, 'list'], [false, 'form']],
type: 'ir.actions.act_window',
view_type: "list",
view_mode: "list"
});
},
/**
* Open the list view for account.bank.statement model
* @private
* @param {MouseEvent} event
*/
_onGoToBankStatement: function (e) {
var journalId = $(e.target).attr('data_journal_id');
if (journalId) {
journalId = parseInt(journalId);
}
this.do_action({
name: 'Bank Statements',
res_model: 'account.bank.statement',
views: [[false, 'list'], [false, 'form']],
type: 'ir.actions.act_window',
context: {search_default_journal_id: journalId},
view_type: 'list',
view_mode: 'form',
});
},
/**
* Load more statement lines for reconciliation
* @private
* @param {MouseEvent} event
*/
_onLoadMore: function (e) {
this.trigger_up('load_more');
},
/**
* @private
*/
_onValidateName: function () {
var name = this.model.get(this.handleNameRecord).data.name;
this.trigger_up('change_name', {'data': name});
this.$('.statement_name, .statement_name_edition').toggle();
},
});
/**
* rendering of the bank statement line, contains line data, proposition and
* view for 'match' and 'create' mode
*/
var LineRenderer = Widget.extend(FieldManagerMixin, {
template: "reconciliation.line",
events: {
'click .accounting_view caption .o_buttons button': '_onValidate',
'click .accounting_view thead td': '_onTogglePanel',
'click .accounting_view tfoot td:not(.cell_left,.cell_right)': '_onShowPanel',
'click tfoot .cell_left, tfoot .cell_right': '_onSearchBalanceAmount',
'input input.filter': '_onFilterChange',
'click .match_controls .fa-chevron-left:not(.disabled)': '_onPrevious',
'click .match_controls .fa-chevron-right:not(.disabled)': '_onNext',
'click .match .mv_line td': '_onSelectMoveLine',
'click .accounting_view tbody .mv_line td': '_onSelectProposition',
'click .o_reconcile_models button': '_onQuickCreateProposition',
'click .create .add_line': '_onCreateProposition',
'click .accounting_view .line_info_button.fa-exclamation-triangle': '_onTogglePartialReconcile',
'click .reconcile_model_create': '_onCreateReconcileModel',
'click .reconcile_model_edit': '_onEditReconcileModel',
'keyup input': '_onInputKeyup',
'blur input': '_onInputKeyup',
},
custom_events: _.extend({}, FieldManagerMixin.custom_events, {
'field_changed': '_onFieldChanged',
}),
_avoidFieldUpdate: {},
/**
* create partner_id field in editable mode
*
* @override
*/
init: function (parent, model, state) {
this._super(parent);
FieldManagerMixin.init.call(this);
this.model = model;
this._initialState = state;
},
/**
* @override
*/
start: function () {
var self = this;
var def1 = this._makePartnerRecord(this._initialState.st_line.partner_id, this._initialState.st_line.partner_name).then(function (recordID) {
self.fields = {
partner_id : new relational_fields.FieldMany2One(self,
'partner_id',
self.model.get(recordID),
{mode: 'edit'}
)
};
self.fields.partner_id.appendTo(self.$('.accounting_view caption'));
});
this.$('thead .line_info_button').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,
'trigger': 'hover',
'animation': false,
'toggle': 'popover'
});
var def2 = this._super.apply(this, arguments);
return $.when(def1, def2);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* update the statement line rendering
*
* @param {object} state - statement line
*/
update: function (state) {
var self = this;
// isValid
this.$('caption .o_buttons button.o_validate').toggleClass('hidden', !!state.balance.type);
this.$('caption .o_buttons button.o_reconcile').toggleClass('hidden', state.balance.type <= 0);
this.$('caption .o_buttons .o_no_valid').toggleClass('hidden', state.balance.type >= 0);
// 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.$('.create, .match').each(function () {
var $panel = $(this);
$panel.css('-webkit-transition', 'none');
$panel.css('-moz-transition', 'none');
$panel.css('-o-transition', 'none');
$panel.css('transition', 'none');
$panel.css('max-height', $panel.height());
$panel.css('-webkit-transition', '');
$panel.css('-moz-transition', '');
$panel.css('-o-transition', '');
$panel.css('transition', '');
});
this.$el.data('mode', state.mode).attr('data-mode', state.mode);
this.$('.create, .match').each(function () {
$(this).removeAttr('style');
});
// reconciliation_proposition
var $props = this.$('.accounting_view tbody').empty();
var props = _.filter(state.reconciliation_proposition, {'display': true});
_.each(props, function (line) {
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
if (!isNaN(line.id)) {
$('<span class="line_info_button fa fa-info-circle"/>')
.appendTo($line.find('.cell_info_popover'))
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line}));
}
if ((state.balance.amount_currency !== 0 || line.partial_reconcile) && props.length === 1 &&
line.already_paid === false &&
(
(state.st_line.amount > 0 && state.st_line.amount < props[0].amount) ||
(state.st_line.amount < 0 && state.st_line.amount > props[0].amount))
) {
var $cell = $line.find(line.amount > 0 ? '.cell_right' : '.cell_left');
var text;
if (line.partial_reconcile) {
text = _t("Undo the partial reconciliation.");
$cell.text(state.st_line.amount_str);
} else {
text = _t("This move's amount is higher than the transaction's amount. Click to register a partial payment and keep the payment balance open.");
}
$('<span class="do_partial_reconcile_'+(!line.partial_reconcile)+' line_info_button fa fa-exclamation-triangle"/>')
.prependTo($cell)
.attr("data-content", text);
}
$props.append($line);
});
// mv_lines
var $mv_lines = this.$('.match table tbody').empty();
_.each(state.mv_lines.slice(0, state.limitMoveLines), function (line) {
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
if (!isNaN(line.id)) {
$('<span class="line_info_button fa fa-info-circle"/>')
.appendTo($line.find('.cell_info_popover'))
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line}));
}
$mv_lines.append($line);
});
this.$('.match .fa-chevron-right').toggleClass('disabled', state.mv_lines.length <= state.limitMoveLines);
this.$('.match .fa-chevron-left').toggleClass('disabled', !state.offset);
this.$('.match').css('max-height', !state.mv_lines.length && !state.filter.length ? '0px' : '');
// balance
this.$('.popover').remove();
this.$('table tfoot').html(qweb.render("reconciliation.line.balance", {'state': state}));
// filter
if (_.str.strip(this.$('input.filter').val()) !== state.filter) {
this.$('input.filter').val(state.filter);
}
// create form
if (state.createForm) {
if (!this.fields.account_id) {
this._renderCreate(state);
}
var data = this.model.get(this.handleCreateRecord).data;
this.model.notifyChanges(this.handleCreateRecord, state.createForm);
var record = this.model.get(this.handleCreateRecord);
_.each(this.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);
}
});
}
this.$('.create .add_line').toggle(!!state.balance.amount_currency);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {jQueryElement} $el
*/
_destroyPopover: function ($el) {
var popover = $el.data('bs.popover');
if (popover) {
popover.destroy();
}
},
/**
* @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: [["parent_id", "=", false], "|", ["customer", "=", true], ["supplier", "=", true]],
options: {
no_open: true
}
}
});
},
/**
* create account_id, tax_id, analytic_account_id, label and amount field
*
* @private
* @param {object} state - statement line
*/
_renderCreate: function (state) {
var self = this;
this.model.makeRecord('account.bank.statement.line', [{
relation: 'account.account',
type: 'many2one',
name: 'account_id',
}, {
relation: 'account.journal',
type: 'many2one',
name: 'journal_id',
}, {
relation: 'account.tax',
type: 'many2one',
name: 'tax_id',
}, {
relation: 'account.analytic.account',
type: 'many2one',
name: 'analytic_account_id',
}, {
type: 'char',
name: 'label',
}, {
type: 'float',
name: 'amount',
}], {
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'});
self.fields.journal_id = new relational_fields.FieldMany2One(self,
'journal_id', record, {mode: 'edit'});
self.fields.tax_id = new relational_fields.FieldMany2One(self,
'tax_id', record, {mode: 'edit'});
self.fields.analytic_account_id = new relational_fields.FieldMany2One(self,
'analytic_account_id', 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'});
var $create = $(qweb.render("reconciliation.line.create", {'state': state}));
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_id.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.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.$('.create').append($create);
function addRequiredStyle(widget) {
widget.$el.addClass('o_required_modifier');
}
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} event
*/
_onCreateReconcileModel: function (event) {
event.preventDefault();
this.do_action({
type: 'ir.actions.act_window',
res_model: 'account.reconcile.model',
views: [[false, 'form']],
target: 'current'
});
},
/**
* @private
* @param {MouseEvent} event
*/
_onEditReconcileModel: function (event) {
event.preventDefault();
this.do_action({
type: 'ir.actions.act_window',
res_model: 'account.reconcile.model',
views: [[false, 'list'], [false, 'form']],
view_type: "list",
view_mode: "list",
target: 'current'
});
},
/**
* @private
* @param {FlectraEvent} 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 () {
var mode = this.$el.data('mode') === 'inactive' ? 'match' : 'inactive';
this.trigger_up('change_mode', {'data': mode});
},
/**
* @private
*/
_onSearchBalanceAmount: function () {
this.trigger_up('search_balance_amount');
},
/**
* @private
*/
_onShowPanel: function () {
var mode = (this.$el.data('mode') === 'inactive' || this.$el.data('mode') === 'match') ? 'create' : 'match';
this.trigger_up('change_mode', {'data': mode});
},
/**
* @private
*/
_onFilterChange: function () {
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 (_.findWhere(this.model.lines, {mode: 'create'}).balance.amount) {
this._onCreateProposition();
}
return;
}
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
*/
_onPrevious: function () {
this.trigger_up('change_offset', {'data': -1});
},
/**
* @private
*/
_onNext: function () {
this.trigger_up('change_offset', {'data': 1});
},
/**
* @private
* @param {MouseEvent} event
*/
_onSelectMoveLine: function (event) {
var $el = $(event.target)
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(_("Some fields are undefined"), invalid.join(', '));
return;
}
this.trigger_up('create_proposition');
},
/**
* @private
*/
_onValidate: function () {
this.trigger_up('validate');
},
/**
* @private
* @param {MouseEvent} event
*/
_onTogglePartialReconcile: function (e) {
e.stopPropagation();
var popover = $(e.target).data('bs.popover');
popover && popover.destroy();
this.trigger_up('toggle_partial_reconcile');
}
});
/**
* rendering of the manual reconciliation action contains progress bar, title
* and auto reconciliation button
*/
var ManualRenderer = StatementRenderer.extend({
template: "reconciliation.manual.statement",
/**
* avoid statement name edition
*
* @override
* @private
*/
_onClickStatementName: function () {}
});
/**
* 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 {Deferred}
*/
removeProposition: function (handle, id) {
if (!id) {
return $.when();
}
return this._super(handle, id);
},
/**
* move the partner field
*
* @override
*/
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
var defs = [];
var def;
if (self._initialState.partner_id) {
def = self._makePartnerRecord(self._initialState.partner_id, self._initialState.partner_name).then(function (recordID) {
self.fields.partner_id = new relational_fields.FieldMany2One(self,
'partner_id',
self.model.get(recordID),
{mode: 'readonly'}
);
});
defs.push(def);
} else {
def = 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'}
);
});
defs.push(def);
}
return $.when.apply($, defs).then(function () {
if (!self.fields.title_account_id) {
self.fields.partner_id.$el.prependTo(self.$('.accounting_view thead td:eq(1) span:first'));
} else {
self.fields.partner_id.destroy();
self.fields.title_account_id.appendTo(self.$('.accounting_view thead td:eq(1) 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) {
this._super(state);
this.$('.create .create_journal_id').show();
this.$('.create .create_journal_id .o_input').addClass('o_required_modifier');
},
});
return {
StatementRenderer: StatementRenderer,
ManualRenderer: ManualRenderer,
LineRenderer: LineRenderer,
ManualLineRenderer: ManualLineRenderer,
};
});