[ADD] account: reconciliation widget, add load more button in widget to load more statements

This commit is contained in:
Christophe Matthieu
2017-08-23 14:46:07 +02:00
6 changed files with 407 additions and 45 deletions
@@ -499,7 +499,6 @@ class AccountBankStatementLine(models.Model):
'st_line': st_line.get_statement_line_for_reconciliation_widget(),
'reconciliation_proposition': rp
})
return ret
def get_statement_line_for_reconciliation_widget(self):
@@ -30,6 +30,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
validate_all_balanced: '_onValidate',
change_name: '_onChangeName',
close_statement: '_onCloseStatement',
load_more: '_onLoadMore',
},
config: {
// used to instanciate the model
@@ -71,6 +72,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
'bank_statement_id': self.model.bank_statement_id,
'valuenow': self.model.valuenow,
'valuemax': self.model.valuemax,
'defaultDisplayQty': self.model.defaultDisplayQty,
'title': self.title,
});
});
@@ -89,12 +91,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
this.update_control_panel({breadcrumbs: breadcrumbs, search_view_hidden: true}, {clear: true});
this.renderer.prependTo(self.$('.o_form_sheet'));
_.each(this.model.lines, function (line, handle) {
var widget = new self.config.LineRenderer(self, self.model, line);
widget.handle = handle;
self.widgets.push(widget);
widget.appendTo(self.$('.o_reconciliation_lines'));
});
this._renderLines();
this._openFirstLine();
},
@@ -134,6 +131,15 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
return _.find(this.widgets, function (widget) {return widget.handle===handle;});
},
/**
*
*/
_loadMore: function(qty) {
var self = this;
return this.model.loadMore(qty).then(function () {
self._renderLines();
});
},
/**
* sitch to 'match' the first available line
*
@@ -153,6 +159,24 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
}
return handle;
},
/**
* render line widget and append to view
*
* @private
*/
_renderLines: function () {
var self = this;
var linesToDisplay = this.model.getStatementLines();
_.each(linesToDisplay, function (line, handle) {
var widget = new self.config.LineRenderer(self, self.model, line);
widget.handle = handle;
self.widgets.push(widget);
widget.appendTo(self.$('.o_reconciliation_lines'));
});
if (this.model.hasMoreLines() === false) {
this.renderer.hideLoadMoreButton();
}
},
//--------------------------------------------------------------------------
// Handlers
@@ -223,6 +247,14 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
});
});
},
/**
* Load more statement and render them
*
* @param {OdooEvent} event
*/
_onLoadMore: function (event) {
return this._loadMore(this.model.defaultDisplayQty);
},
/**
* call 'validate' or 'autoReconciliation' model method then destroy the
* validated lines and update the action renderer with the new status bar
@@ -246,7 +278,15 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
});
_.each(result.handles, function (handle) {
self._getWidget(handle).destroy();
var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;});
self.widgets.splice(index, 1);
});
// Get number of widget and if less than constant and if there are more to laod, load until constant
if (self.widgets.length < self.model.defaultDisplayQty
&& self.model.valuemax - self.model.valuenow >= self.model.defaultDisplayQty) {
var toLoad = self.model.defaultDisplayQty - self.widgets.length;
self._loadMore(toLoad);
}
self._openFirstLine();
});
},
@@ -100,6 +100,8 @@ var StatementModel = BasicModel.extend({
this.lines = {};
this.valuenow = 0;
this.valuemax = 0;
this.alreadyDisplayed = [];
this.defaultDisplayQty = 10;
},
//--------------------------------------------------------------------------
@@ -283,6 +285,34 @@ var StatementModel = BasicModel.extend({
getContext: function () {
return this.context;
},
/**
* Return the lines that needs to be displayed by the widget
*
* @returns {Object} lines that are loaded and not yet displayed
*/
getStatementLines: function () {
var self = this;
var linesToDisplay = _.pick(this.lines, function(value, key, object) {
if (value.visible === true && self.alreadyDisplayed.indexOf(key) === -1) {
self.alreadyDisplayed.push(key);
return object;
}
});
return linesToDisplay;
},
/**
* Return a boolean telling if load button needs to be displayed or not
*
* @returns {boolean} true if load more button needs to be displayed
*/
hasMoreLines: function () {
var self = this;
var notDisplayed = _.filter(this.lines, function(line) { return !line.visible; });
if (notDisplayed.length > 0) {
return true;
}
return false;
},
/**
* get the line data for this handle
*
@@ -355,14 +385,42 @@ var StatementModel = BasicModel.extend({
line.reconcileModels = self.reconcileModels;
});
var ids = _.pluck(self.lines, 'id');
return self._rpc({
model: 'account.bank.statement.line',
method: 'get_data_for_reconciliation_widget',
args: [ids],
})
.then(self._formatLine.bind(self));
ids = ids.splice(0, self.defaultDisplayQty);
self.pagerIndex = ids.length;
return self.loadData(ids, []);
});
},
/**
* Load more bank statement line
*
* @param {integer} quantity to load
* @returns {Deferred}
*/
loadMore: function(qty) {
if (qty === undefined) {
qty = this.defaultDisplayQty;
}
var ids = _.pluck(this.lines, 'id');
ids = ids.splice(this.pagerIndex, qty);
this.pagerIndex += qty;
return this.loadData(ids, this._getExcludedIds());
},
/**
* RPC method to load informations on lines
*
* @param {Array} ids of bank statement line passed to rpc call
* @param {Array} list of move_line ids that needs to be excluded from search
* @returns {Deferred}
*/
loadData: function(ids, excluded_ids) {
var self = this;
return self._rpc({
model: 'account.bank.statement.line',
method: 'get_data_for_reconciliation_widget',
args: [ids, excluded_ids],
})
.then(self._formatLine.bind(self));
},
/**
* Add lines into the propositions from the reconcile model
* Can add 2 lines, and each with its taxes. The second line become editable
@@ -770,6 +828,7 @@ var StatementModel = BasicModel.extend({
var line = _.find(self.lines, function (l) {
return l.id === data.st_line.id;
});
line.visible = true;
_.extend(line, data);
self._formatLineProposition(line, line.reconciliation_proposition);
if (!line.reconciliation_proposition.length) {
@@ -850,6 +909,26 @@ var StatementModel = BasicModel.extend({
prop.amount = prop.base_amount;
return prop;
},
/**
* Return list of account_move_line that has been selected and needs to be removed
* from other calls.
*
* @private
* @returns {Array} list of excluded ids
*/
_getExcludedIds: function () {
var excludedIds = [];
_.each(this.lines, function(line) {
if (line.reconciliation_proposition) {
_.each(line.reconciliation_proposition, function(prop) {
if (parseInt(prop['id'])) {
excludedIds.push(prop['id']);
}
})
}
});
return excludedIds;
},
/**
* Defined whether the line is to be displayed or not. Here, we only display
* the line if it comes from the server or if an account is defined when it
@@ -1168,7 +1247,8 @@ var ManualModel = StatementModel.extend({
'filter': "",
'reconcileModels': [],
'account_id': this._formatNameGet([data.account_id, data.account_name]),
'st_line': data
'st_line': data,
'visible': true
});
this._formatLineProposition(line, line.reconciliation_proposition);
if (!line.reconciliation_proposition.length) {
@@ -24,6 +24,7 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
"click *[rel='do_action']": "_onDoAction",
'click button.button_back_to_statement': '_onGoToBankStatement',
'click button.button_close_statement': '_onCloseBankStatement',
'click button.js_load_more': '_onLoadMore',
},
/**
* @override
@@ -87,6 +88,12 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/*
* hide the button to load more statement line
*/
hideLoadMoreButton: function () {
this.$('.js_load_more').hide();
},
/**
* update the statement rendering
*
@@ -207,6 +214,14 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
view_mode: 'form',
});
},
/**
* Load more statement lines for reconciliation
* @private
* @param {MouseEvent} event
*/
_onLoadMore: function (e) {
this.trigger_up('load_more');
},
/**
* @private
*/
@@ -25,6 +25,9 @@
<div class="notification_area"/>
</div>
<div class="o_reconciliation_lines"/>
<div t-if="widget._initialState.valuemax > widget._initialState.defaultDisplayQty">
<button class="btn btn-default js_load_more">Load more</button>
</div>
<div class="pull-right text-muted">Tip: Hit CTRL-Enter to reconcile all the balanced items in the sheet.</div>
</div>
<div t-else="" class="o_view_nocontent">
@@ -1,7 +1,9 @@
odoo.define('account.reconciliation_tests.data', function () {
"use strict";
var data = {
var Datas = {};
var db = {
'res.partner': {
fields: {
id: {string: "ID", type: 'integer'},
@@ -58,7 +60,7 @@ var data = {
{id: 7, display_name: "Tax 10.00% include", amount: 10, price_include: true, account_id: 288},
],
json_friendly_compute_all: function (args) {
var tax = _.find(data['account.tax'].records, {'id': args[0][0]});
var tax = _.find(db['account.tax'].records, {'id': args[0][0]});
var amount = args[1];
var tax_base = tax.price_include ? amount*100/(100+tax.amount) : amount;
return $.when({
@@ -103,7 +105,7 @@ var data = {
'account.bank.statement': {
fields: {},
reconciliation_widget_preprocess: function () {
return $.when(data_preprocess);
return $.when(Datas.used.data_preprocess);
},
},
'account.bank.statement.line': {
@@ -122,19 +124,19 @@ var data = {
var partner_id = args.splice(1, 1)[0];
var excluded_ids = args.splice(1, 1)[0];
var key = JSON.stringify(args);
if (!mv_lines[key]) {
if (!Datas.used.mv_lines[key]) {
throw new Error("Unknown parameters for get_move_lines_for_reconciliation_widget: '"+ key + "'");
}
return $.when(_.filter(mv_lines[key], function (line) {
return $.when(_.filter(Datas.used.mv_lines[key], function (line) {
return excluded_ids.indexOf(line.id) === -1 && (!partner_id || partner_id === line.partner_id);
}));
},
get_data_for_reconciliation_widget: function (args) {
var ids = args[0];
return $.when(_.filter(data_widget, function (w) {return _.contains(ids, w.st_line.id);}));
return $.when(_.filter(Datas.used.data_widget, function (w) {return _.contains(ids, w.st_line.id);}));
},
reconciliation_widget_auto_reconcile: function () {
return $.when(auto_reconciliation);
return $.when(Datas.used.auto_reconciliation);
},
process_reconciliations: function (args) {
var datas = args[1];
@@ -142,8 +144,8 @@ var data = {
ids = ids.concat(_.flatten(_.pluck(datas, 'payment_aml_ids')));
ids = _.compact(ids);
for (var key in move_lines_for_manual_reconciliation) {
move_lines_for_manual_reconciliation[key] = _.filter(move_lines_for_manual_reconciliation[key], function (mv_line) {
for (var key in Datas.used.move_lines_for_manual_reconciliation) {
Datas.used.move_lines_for_manual_reconciliation[key] = _.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (mv_line) {
return ids.indexOf(mv_line.id) === -1;
});
}
@@ -154,18 +156,18 @@ var data = {
fields: {},
get_data_for_manual_reconciliation_widget: function (args) {
var key = JSON.stringify(args);
if (!data_for_manual_reconciliation_widget[key]) {
if (!Datas.used.data_for_manual_reconciliation_widget[key]) {
throw new Error("Unknown parameters for get_data_for_manual_reconciliation_widget: '"+ key + "'");
}
return $.when(data_for_manual_reconciliation_widget[key]);
return $.when(Datas.used.data_for_manual_reconciliation_widget[key]);
},
get_move_lines_for_manual_reconciliation: function (args) {
var excluded_ids = args.splice(2, 1)[0];
var key = JSON.stringify(args);
if (!move_lines_for_manual_reconciliation[key]) {
if (!Datas.used.move_lines_for_manual_reconciliation[key]) {
throw new Error("Unknown parameters for get_move_lines_for_manual_reconciliation: '"+ key + "'");
}
return $.when(_.filter(move_lines_for_manual_reconciliation[key], function (line) {
return $.when(_.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (line) {
return excluded_ids.indexOf(line.id) === -1;
}));
},
@@ -174,8 +176,8 @@ var data = {
var datas = args[0];
for (var i in datas) {
var data = datas[i];
for (var key in move_lines_for_manual_reconciliation) {
move_lines_for_manual_reconciliation[key] = _.filter(move_lines_for_manual_reconciliation[key], function (mv_line) {
for (var key in Datas.used.move_lines_for_manual_reconciliation) {
Datas.used.move_lines_for_manual_reconciliation[key] = _.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (mv_line) {
return data.mv_line_ids.indexOf(mv_line.id) === -1;
});
}
@@ -468,23 +470,22 @@ var options = {
}
};
return {
params: {
data: data,
data_preprocess: data_preprocess,
data_widget: data_widget,
mv_lines: mv_lines,
auto_reconciliation: auto_reconciliation,
data_for_manual_reconciliation_widget: data_for_manual_reconciliation_widget,
move_lines_for_manual_reconciliation: move_lines_for_manual_reconciliation,
session: session,
options: options,
},
// this is the main function for this module. Its job is to export (and clone) all data for a test.
getParams: function () {
return $.extend(true, {}, this.params);
}
Datas.params = {
data: db,
data_preprocess: data_preprocess,
data_widget: data_widget,
mv_lines: mv_lines,
auto_reconciliation: auto_reconciliation,
data_for_manual_reconciliation_widget: data_for_manual_reconciliation_widget,
move_lines_for_manual_reconciliation: move_lines_for_manual_reconciliation,
session: session,
options: options,
};
// this is the main function for this module. Its job is to export (and clone) all data for a test.
Datas.getParams = function () {
return (this.used = $.extend(true, {}, this.params));
};
return Datas;
});
odoo.define('account.reconciliation_tests', function (require) {
@@ -992,5 +993,229 @@ QUnit.module('account', {
clientAction.destroy();
});
QUnit.test('Reconciliation pager', function (assert) {
assert.expect(6);
this.params.data['account.bank.statement.line'].records.push(
{id: 9, display_name: "add 9"},
{id: 10, display_name: "add 10"},
{id: 11, display_name: "add 11"},
{id: 12, display_name: "add 12"},
{id: 13, display_name: "add 13"},
{id: 14, display_name: "add 14"},
{id: 15, display_name: "add 15"},
{id: 16, display_name: "add 16"}
);
var data_preprocess = this.params.data_preprocess;
data_preprocess.st_lines_ids.push(9, 10, 11, 12, 13, 14, 15, 16);
this.params.data_widget.push(
{
'st_line': {
'currency_id': 3,
'communication_partner_name': false,
'open_balance_account_id': 287,
'name': "add 9",
'partner_name': "Camptocamp",
'partner_id': 12,
'has_no_partner': false,
'journal_id': 84,
'account_name': "Bank",
'note': "",
'amount': 650.0,
'amount_str': "$ 650.00",
'amount_currency_str': "",
'date': "2017-01-01",
'account_code': "101401",
'ref': "",
'id': 9,
'statement_id': 2
},
'reconciliation_proposition': []
},
{
'st_line': {
'currency_id': 3,
'communication_partner_name': false,
'open_balance_account_id': 287,
'name': "add 10",
'partner_name': "Camptocamp",
'partner_id': 12,
'has_no_partner': false,
'journal_id': 84,
'account_name': "Bank",
'note': "",
'amount': 650.0,
'amount_str': "$ 650.00",
'amount_currency_str': "",
'date': "2017-01-01",
'account_code': "101401",
'ref': "",
'id': 10,
'statement_id': 2
},
'reconciliation_proposition': []
},
{
'st_line': {
'currency_id': 3,
'communication_partner_name': false,
'open_balance_account_id': 287,
'name': "add 11",
'partner_name': "Camptocamp",
'partner_id': 12,
'has_no_partner': false,
'journal_id': 84,
'account_name': "Bank",
'note': "",
'amount': 650.0,
'amount_str': "$ 650.00",
'amount_currency_str': "",
'date': "2017-01-01",
'account_code': "101401",
'ref': "",
'id': 11,
'statement_id': 2
},
'reconciliation_proposition': []
},
{
'st_line': {
'currency_id': 3,
'communication_partner_name': false,
'open_balance_account_id': 287,
'name': "add 12",
'partner_name': "Camptocamp",
'partner_id': 12,
'has_no_partner': false,
'journal_id': 84,
'account_name': "Bank",
'note': "",
'amount': 650.0,
'amount_str': "$ 650.00",
'amount_currency_str': "",
'date': "2017-01-01",
'account_code': "101401",
'ref': "",
'id': 12,
'statement_id': 2
},
'reconciliation_proposition': []
},
{
'st_line': {
'currency_id': 3,
'communication_partner_name': false,
'open_balance_account_id': 287,
'name': "add 13",
'partner_name': "Camptocamp",
'partner_id': 12,
'has_no_partner': false,
'journal_id': 84,
'account_name': "Bank",
'note': "",
'amount': 650.0,
'amount_str': "$ 650.00",
'amount_currency_str': "",
'date': "2017-01-01",
'account_code': "101401",
'ref': "",
'id': 13,
'statement_id': 2
},
'reconciliation_proposition': []
},
{
'st_line': {
'currency_id': 3,
'communication_partner_name': false,
'open_balance_account_id': 287,
'name': "add 14",
'partner_name': "Camptocamp",
'partner_id': 12,
'has_no_partner': false,
'journal_id': 84,
'account_name': "Bank",
'note': "",
'amount': 650.0,
'amount_str': "$ 650.00",
'amount_currency_str': "",
'date': "2017-01-01",
'account_code': "101401",
'ref': "",
'id': 14,
'statement_id': 2
},
'reconciliation_proposition': []
},
{
'st_line': {
'currency_id': 3,
'communication_partner_name': false,
'open_balance_account_id': 287,
'name': "add 15",
'partner_name': "Camptocamp",
'partner_id': 12,
'has_no_partner': false,
'journal_id': 84,
'account_name': "Bank",
'note': "",
'amount': 650.0,
'amount_str': "$ 650.00",
'amount_currency_str': "",
'date': "2017-01-01",
'account_code': "101401",
'ref': "",
'id': 15,
'statement_id': 2
},
'reconciliation_proposition': []
},
{
'st_line': {
'currency_id': 3,
'communication_partner_name': false,
'open_balance_account_id': 287,
'name': "add 16",
'partner_name': "Camptocamp",
'partner_id': 12,
'has_no_partner': false,
'journal_id': 84,
'account_name': "Bank",
'note': "",
'amount': 650.0,
'amount_str': "$ 650.00",
'amount_currency_str': "",
'date': "2017-01-01",
'account_code': "101401",
'ref': "",
'id': 16,
'statement_id': 2
},
'reconciliation_proposition': []
}
);
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
'data': this.params.data,
});
clientAction.appendTo($('#qunit-fixture'));
assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 10, "should display 10 lines");
assert.strictEqual(clientAction.$('.js_load_more:visible').length, 1, "should display the load more button");
clientAction.$('.o_reconcile:visible').click();
assert.strictEqual(clientAction.$('.o_reconcile:visible').length, 0, "should remove the reconciled line");
assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 10, "should load one line to complete the 10");
clientAction.$('.js_load_more').click();
assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 11, "should load the last record");
assert.strictEqual(clientAction.$('.js_load_more:visible').length, 0, "should hide the load more button");
clientAction.destroy();
});
});
});