[FIX] account: fix the broken reconciliation widget

Lot of features were not working and this commit fixes that
- Create entries upon reconciliation
- Add buttons at the end of the process to view/close bank statement
- Fix partial reconcile
- When changing partner, correctly fetch counterpart account and fix search domain
- Add ability to reconcile without choosing a line (will automatically create a line)
- some other small fixes
This commit is contained in:
Cedric Snauwaert
2017-07-13 14:17:58 +02:00
parent 0c496136a9
commit a562f5ced1
6 changed files with 222 additions and 42 deletions
@@ -335,6 +335,7 @@ class AccountBankStatement(models.Model):
'st_lines_ids': st_lines_left.ids,
'notifications': [],
'statement_name': len(statements) == 1 and statements[0].name or False,
'journal_id': statements and statements[0].journal_id.id or False,
'num_already_reconciled_lines': 0,
}
@@ -470,7 +471,7 @@ class AccountBankStatementLine(models.Model):
'details': {
'name': _("Automatically reconciled items"),
'model': 'account.move',
'ids': automatic_reconciliation_entries.mapped('journal_entry_ids').ids
'ids': automatic_reconciliation_entries.mapped('journal_entry_ids').mapped('move_id').ids
}
}]
return {
@@ -586,7 +587,7 @@ class AccountBankStatementLine(models.Model):
if str:
str_domain = self.env['account.move.line'].domain_move_lines_for_reconciliation(str=str)
if not partner_id:
str_domain = expression.OR([str_domain, ('partner_id.name', 'ilike', str)])
str_domain = expression.OR([str_domain, [('partner_id.name', 'ilike', str)]])
domain = expression.AND([domain, str_domain])
if excluded_ids:
domain = expression.AND([[('id', 'not in', excluded_ids)], domain])
@@ -29,6 +29,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
validate: '_onValidate',
validate_all_balanced: '_onValidate',
change_name: '_onChangeName',
close_statement: '_onCloseStatement',
},
config: {
// used to instanciate the model
@@ -202,7 +203,26 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
});
});
},
/**
* call 'closeStatement' model method
*
* @private
* @param {OdooEvent} event
*/
_onCloseStatement: function (event) {
var self = this;
return this.model.closeStatement().then(function (result) {
self.do_action({
name: 'Bank Statements',
res_model: 'account.bank.statement',
res_id: result,
views: [[false, 'form']],
type: 'ir.actions.act_window',
view_type: 'form',
view_mode: 'form',
});
});
},
/**
* call 'validate' or 'autoReconciliation' model method then destroy the
* validated lines and update the action renderer with the new status bar
@@ -222,6 +242,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
'title': self.title,
'time': Date.now()-self.time,
'notifications': result.notifications,
'context': self.model.getContext(),
});
_.each(result.handles, function (handle) {
self._getWidget(handle).destroy();
@@ -230,11 +230,31 @@ var StatementModel = BasicModel.extend({
var line = this.getLine(handle);
line.st_line.partner_id = partner && partner.id;
line.st_line.partner_name = partner && partner.display_name || '';
return this._performMoveLine(handle).then(function () {
if (line.mode === 'create') {
return self.createProposition(handle);
}
});
return this._changePartner(handle, partner.id)
.then(function() {
line.reconciliation_proposition = [];
return self._performMoveLine(handle)
})
.then(function () {
if (line.mode === 'create') {
return self.createProposition(handle);
}
});
},
/**
* close the statement
* @returns {Deferred<number>} resolves to the res_id of the closed statements
*/
closeStatement: function () {
var self = this;
return this._rpc({
model: 'account.bank.statement',
method: 'button_confirm_bank',
args: [self.bank_statement_id.id],
})
.then(function () {
return self.bank_statement_id.id
});
},
/**
*
@@ -256,6 +276,13 @@ var StatementModel = BasicModel.extend({
line.createForm = _.pick(prop, this.quickCreateFields);
return this._computeLine(line);
},
/**
* Return context information and journal_id
* @returns {Object} context
*/
getContext: function () {
return this.context;
},
/**
* get the line data for this handle
*
@@ -282,6 +309,8 @@ var StatementModel = BasicModel.extend({
if (!statement_ids) {
return $.when();
}
this.context = context;
var def_statement = this._rpc({
model: 'account.bank.statement',
method: 'reconciliation_widget_preprocess',
@@ -292,6 +321,7 @@ var StatementModel = BasicModel.extend({
self.bank_statement_id = statement_ids.length === 1 ? {id: statement_ids[0], display_name: statement.statement_name} : false;
self.valuenow = 0;
self.valuemax = statement.st_lines_ids.length;
self.context.journal_id = statement.journal_id;
_.each(statement.st_lines_ids, function (id) {
self.lines[_.uniqueId('rline')] = {
'id': id,
@@ -484,14 +514,19 @@ var StatementModel = BasicModel.extend({
_.each(handles, function (handle) {
var line = self.getLine(handle);
var props = _.filter(line.reconciliation_proposition, function (prop) {return !prop.is_tax && !prop.invalid;});
if (props.length === 0) {
props.push(self._formatQuickCreate(line, {
account_id: [line.st_line.open_balance_account_id, self.accounts[line.st_line.open_balance_account_id]],
}));
}
ids.push(line.id);
values.push({
"partner_id": line.st_line.partner_id,
"counterpart_aml_dicts": _.map(_.filter(props, function (prop) {
return !isNaN(prop.id) && prop.partial_reconcile;
return !isNaN(prop.id) && !prop.already_paid;
}), self._formatToProcessReconciliation.bind(self, line)),
"payment_aml_ids": _.pluck(_.filter(props, function (prop) {
return !isNaN(prop.id) && !prop.partial_reconcile;
return !isNaN(prop.id) && prop.already_paid;
}), 'id'),
"new_aml_dicts": _.map(_.filter(props, function (prop) {
return isNaN(prop.id);
@@ -557,6 +592,29 @@ var StatementModel = BasicModel.extend({
});
return this._computeLine(line);
},
/**
* When changing partner, read property_account_receivable and payable
* of that partner because the counterpart account might cahnge depending
* on the partner
*
* @private
* @param {string} handle
* @param {integer} partner_id
* @returns {Deferred}
*/
_changePartner: function (handle, partner_id) {
var self = this;
return this._rpc({
model: 'res.partner',
method: 'read',
args: [partner_id, ["property_account_receivable_id", "property_account_payable_id"]],
}).then(function (result) {
if (result.length > 0) {
var line = self.getLine(handle);
self.lines[handle].st_line.open_balance_account_id = line.amount < 0 ? result[0]['property_account_payable_id'][0] : result[0]['property_account_receivable_id'][0];
}
});
},
/**
* Calculates the balance; format each proposition amount_str and mark as
* invalid the line with empty account_id, amount or label
@@ -573,7 +631,7 @@ var StatementModel = BasicModel.extend({
// compute taxes
var tax_defs = [];
var reconciliation_proposition = [];
var format_options = {
var formatOptions = {
currency_id: line.st_line.currency_id,
};
_.each(line.reconciliation_proposition, function (prop) {
@@ -610,10 +668,10 @@ var StatementModel = BasicModel.extend({
});
prop.amount = tax.base;
prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, format_options);
prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);
prop.invalid = !self._isValid(prop);
tax_prop.amount_str = field_utils.format.monetary(Math.abs(tax_prop.amount), {}, format_options);
tax_prop.amount_str = field_utils.format.monetary(Math.abs(tax_prop.amount), {}, formatOptions);
tax_prop.invalid = prop.invalid;
reconciliation_proposition.push(tax_prop);
@@ -644,7 +702,7 @@ var StatementModel = BasicModel.extend({
total = Math.round(total*1000)/1000 || 0;
line.balance = {
'amount': total,
'amount_str': field_utils.format.monetary(Math.abs(total), {}, format_options),
'amount_str': field_utils.format.monetary(Math.abs(total), {}, formatOptions),
'account_code': self.accounts[line.st_line.open_balance_account_id],
};
line.balance.type = line.balance.amount ? (line.balance.amount > 0 && line.st_line.partner_id ? 0 : -1) : 1;
@@ -759,6 +817,9 @@ var StatementModel = BasicModel.extend({
_formatQuickCreate: function (line, values) {
values = values || {};
var account = this._formatNameGet(values.account_id);
var formatOptions = {
currency_id: line.st_line.currency_id,
};
var prop = {
'id': _.uniqueId('createLine'),
'label': values.label || line.st_line.name,
@@ -771,7 +832,7 @@ var StatementModel = BasicModel.extend({
'credit': 0,
'base_amount': values.amount_type !== "percentage" ?
(values.amount || line.balance.amount) :
line.st_line.amount * values.amount / 100,
line.balance.amount * values.amount / 100,
'percent': values.amount_type === "percentage" ? values.amount : null,
'link': values.link,
'display': true,
@@ -781,9 +842,10 @@ var StatementModel = BasicModel.extend({
'__focus': '__focus' in values ? values.__focus : true,
};
if (prop.base_amount) {
// Call to format and parse needed to round the value to the currency precision
var sign = prop.base_amount < 0 ? -1 : 1;
var amount = field_utils.format.monetary(Math.abs(prop.base_amount), {}, line);
prop.base_amount = sign * field_utils.parse.monetary(amount);
var amount = field_utils.format.monetary(Math.abs(prop.base_amount), {}, formatOptions);
prop.base_amount = sign * field_utils.parse.monetary(amount, {}, formatOptions);
}
prop.amount = prop.base_amount;
return prop;
@@ -843,7 +905,10 @@ var StatementModel = BasicModel.extend({
* @returns {object}
*/
_formatToProcessReconciliation: function (line, prop) {
var amount = prop.partial_reconcile ? -line.st_line.amount : prop.amount;
var amount = -prop.amount;
if (prop.partial_reconcile === true) {
amount = -line.st_line.amount;
}
var result = {
name : prop.label,
debit : amount > 0 ? amount : 0,
@@ -854,7 +919,7 @@ var StatementModel = BasicModel.extend({
} else {
result.account_id = prop.account_id.id;
if (prop.journal_id) {
result.journal_id = prop.account_id.id;
result.journal_id = prop.journal_id.id;
}
}
if (!isNaN(prop.id)) result.counterpart_aml_id = prop.id;
@@ -22,6 +22,8 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
'click div:first h1.statement_name': '_onClickStatementName',
'click div:first h1.statement_name_edition button': '_onValidateName',
"click *[rel='do_action']": "_onDoAction",
'click button.button_back_to_statement': '_onGoToBankStatement',
'click button.button_close_statement': '_onCloseBankStatement',
},
/**
* @override
@@ -107,7 +109,8 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
var $done = $(qweb.render("reconciliation.done", {
'duration': moment(dt).utc().format(time.strftime_to_moment_format(_t.database.parameters.time_format)),
'number': state.valuenow,
'timePerTransaction': Math.round(dt/1000/state.valuemax)
'timePerTransaction': Math.round(dt/1000/state.valuemax),
'context': state.context,
}));
$done.appendTo(this.$el.first());
this.$('.o_automatic_reconciliation').hide();
@@ -158,11 +161,12 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
},
/**
* @private
* Click on close bank statement button, this will
* close and then open form view of bank statement
* @param {MouseEvent} event
*/
_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();
_onCloseBankStatement: function (e) {
this.trigger_up('close_statement');
},
/**
* @private
@@ -183,6 +187,34 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
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',
});
},
/**
* @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();
},
});
@@ -305,6 +337,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
}
if ((state.balance.amount !== 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))
@@ -365,6 +398,16 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {jQueryElement} $el
*/
_destroyPopover: function ($el) {
var popover = $el.data('bs.popover');
if (popover) {
popover.destroy();
}
},
/**
* @private
* @param {integer} partnerID
@@ -550,16 +593,20 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
* @param {MouseEvent} event
*/
_onSelectMoveLine: function (event) {
var mv_line_id = $(event.target).closest('.mv_line').data('line-id');
this.trigger_up('add_proposition', {'data': mv_line_id});
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 mv_line_id = $(event.target).closest('.mv_line').data('line-id');
this.trigger_up('remove_proposition', {'data': mv_line_id});
var $el = $(event.target)
this._destroyPopover($el);
var moveLineId = $el.closest('.mv_line').data('line-id');
this.trigger_up('remove_proposition', {'data': moveLineId});
},
/**
* @private
@@ -49,6 +49,16 @@
<i class="fa fa-4x fa-thumbs-o-up congrats_icon pull-right"></i>
<h2>Congrats, you're all done !</h2>
<p>It took you <strong><t t-esc="duration"/></strong> to reconcile <strong><t t-esc="number"/> transactions.</strong><br/>That's on average <t t-esc="timePerTransaction"/> seconds per transaction.</p>
<t t-if="context &amp;&amp; context.active_model">
<p t-if="context['active_model'] === 'account.journal' || context['active_model'] === 'account.bank.statement'" class="actions_buttons">
<t t-if="context.journal_id">
<button class="button_back_to_statement btn btn-default btn-sm" t-att-data_journal_id='context.journal_id'>Go to bank statement(s)</button>
</t>
<t t-if="context['active_model'] === 'account.bank.statement'">
<button class="button_close_statement btn btn-primary btn-sm" style="display: inline-block;">Close statement</button>
</t>
</p>
</t>
</div>
<t t-name="reconciliation.line">
@@ -9,6 +9,9 @@ var data = {
image: {string: "image", type: 'integer'},
customer: {string: "customer", type: 'boolean'},
supplier: {string: "supplier", type: 'boolean'},
parent_id: {string: "Parent", type: 'boolean'},
property_account_receivable_id: {string: 'Account receivable', type: 'many2one', relation: 'account.account'},
property_account_payable_id: {string: 'Account payable', type: 'many2one', relation: 'account.account'},
},
records: [
{id: 1, display_name: "partner 1", image: 'AAA', customer: true},
@@ -16,7 +19,7 @@ var data = {
{id: 3, display_name: "partner 3", image: 'CCC', customer: true},
{id: 4, display_name: "partner 4", image: 'DDD', customer: true},
{id: 8, display_name: "Agrolait", image: 'EEE', customer: true},
{id: 12, display_name: "Camptocamp", image: 'FFF', supplier: true},
{id: 12, display_name: "Camptocamp", image: 'FFF', supplier: true, property_account_receivable_id: 287, property_account_payable_id: 287},
],
mark_as_reconciled: function () {
return $.when();
@@ -339,7 +342,10 @@ var mv_lines = {
'[]': [],
'[5,"",0,6]': [
{'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 650.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0002", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 109, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 650.00", 'debit': 650.0, 'account_code': "101200", 'ref': "", 'already_paid': false},
{'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 525.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0003", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 112, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 525.00", 'debit': 525.0, 'account_code': "101200", 'ref': "", 'already_paid': false}
{'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 525.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0003", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 112, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 525.00", 'debit': 525.0, 'account_code': "101200", 'ref': "", 'already_paid': false},
{'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 650.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0012", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 134, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 650.00", 'debit': 650.0, 'account_id': [287, "101200 Account Receivable"], 'account_code': "101200", 'ref': "", 'already_paid': false},
{'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-28", 'date': "2017-01-01", 'total_amount_str': "$ 4,610.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0001", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 106, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 4,610.00", 'debit': 4610.0, 'account_id': [287, "101200 Account Receivable"], 'account_code': "101200", 'ref': "", 'already_paid': false},
{'account_type': "payable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-28", 'date': "2017-01-01", 'total_amount_str': "$ 10,000.00", 'partner_id': 12, 'account_name': "Account Payable", 'name': "BILL/2017/0001", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 114, 'credit': 10000.0, 'journal_id': [2, "Vendor Bills"], 'amount_str': "$ 10,000.00", 'debit': 0.0, 'account_id': [284, "101110 Stock Valuation Account"], 'account_code': "111100", 'ref': "", 'already_paid': false}
],
'[5,"b",0,6]': [
{'account_type': "liquidity", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-01-23", 'date': "2017-01-23", 'total_amount_str': "$ 100.00", 'partner_id': 8, 'account_name': "Bank", 'name': "BNK1/2017/0003: CUST.IN/2017/0001", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 394, 'credit': 0.0, 'journal_id': "Bank", 'amount_str': "$ 100.00", 'debit': 100.0, 'account_code': "101401", 'ref': "", 'already_paid': true},
@@ -602,7 +608,19 @@ QUnit.module('account', {
testUtils.intercept(clientAction, 'call_service', function (event) {
assert.deepEqual(event.data.args[1].args,
[[5],[{partner_id: 8, counterpart_aml_dicts: [], payment_aml_ids: [109,112], new_aml_dicts: []}]],
[[5],[{partner_id: 8, counterpart_aml_dicts: [{
"counterpart_aml_id": 109,
"credit": 650,
"debit": 0,
"name": "INV/2017/0002"
},
{
"counterpart_aml_id": 112,
"credit": 525,
"debit": 0,
"name": "INV/2017/0003"
}],
payment_aml_ids: [], new_aml_dicts: []}]],
"Should call process_reconciliations with ids");
});
@@ -625,13 +643,8 @@ QUnit.module('account', {
[6],
[{
partner_id: false,
counterpart_aml_dicts:[{
name: "BNK1/2017/0002: SUPP.OUT/2017/0002",
debit: 32.58,
credit: 0,
counterpart_aml_id: 392
}],
payment_aml_ids: [],
counterpart_aml_dicts:[],
payment_aml_ids: [392],
new_aml_dicts: []
}]
], "should call process_reconciliations with partial reconcile values");
@@ -652,14 +665,14 @@ QUnit.module('account', {
assert.notOk( widget.$('.cell_left .line_info_button').length, "should not display the partial reconciliation alert");
widget.$('.accounting_view thead td:first').trigger('click');
widget.$('.match .cell_account_code:first').trigger('click');
assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 1, "should display the partial reconciliation alert");
assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 0, "should not display the partial reconciliation alert");
assert.ok( widget.$('button.btn-primary:not(hidden)').length, "should not display the reconcile button");
assert.ok( widget.$('.text-danger:not(hidden)').length, "should display counterpart alert");
widget.$('.accounting_view .cell_left .line_info_button').trigger('click');
assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 1, "should display a partial reconciliation alert");
assert.ok(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_false'), "should display the partial reconciliation information");
assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 0, "should not display a partial reconciliation alert");
assert.notOk(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_false'), "should not display the partial reconciliation information");
assert.ok( widget.$('button.btn-default:not(hidden)').length, "should display the validate button");
assert.strictEqual( widget.$el.data('mode'), "inactive", "should be inactive mode");
assert.strictEqual( widget.$el.data('mode'), "match", "should be inactive mode");
widget.$('button.btn-default:not(hidden)').trigger('click');
clientAction.destroy();
@@ -722,6 +735,29 @@ QUnit.module('account', {
clientAction.destroy();
});
QUnit.test('Reconciliation change partner', function (assert) {
assert.expect(4);
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
data: this.params.data,
});
clientAction.appendTo($('#qunit-fixture'));
var widget = clientAction.widgets[0];
assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Agrolait", "the partner many2one should display agrolait");
assert.strictEqual(widget.$('.match table tr').length, 2, "agrolait should have 2 propositions for reconciliation");
// Simulate changing partner
widget.$('.o_input_dropdown input').trigger('click');
$('.ui-autocomplete .ui-menu-item a:contains(Camptocamp)').trigger('mouseenter').trigger('click');
clientAction._onAction({target: widget, name: 'change_partner', data: {data: {display_name: 'Camptocamp', id: 12}}, stopped: false});
assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display Camptocamp");
assert.strictEqual(widget.$('.match table tr').length, 3, "camptocamp should have 3 propositions for reconciliation");
clientAction.destroy();
});
QUnit.test('Reconciliation create line', function (assert) {
assert.expect(23);