Merge branch 'master-patch-july-2018' into 'master-patch-july-2018'
Master patch july 2018 See merge request flectra-hq/flectra!111
This commit is contained in:
commit
f2e1110720
@ -250,6 +250,9 @@ class AccountMove(models.Model):
|
||||
date = date or fields.Date.today()
|
||||
reversed_moves = self.env['account.move']
|
||||
for ac_move in self:
|
||||
#unreconcile all lines reversed
|
||||
aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity')
|
||||
aml.remove_move_reconcile()
|
||||
reversed_move = ac_move._reverse_move(date=date,
|
||||
journal_id=journal_id)
|
||||
reversed_moves |= reversed_move
|
||||
@ -298,7 +301,7 @@ class AccountMoveLine(models.Model):
|
||||
for unreconciled lines, and something in-between for partially reconciled lines.
|
||||
"""
|
||||
for line in self:
|
||||
if not line.account_id.reconcile:
|
||||
if not line.account_id.reconcile and line.account_id.internal_type != 'liquidity':
|
||||
line.reconciled = False
|
||||
line.amount_residual = 0
|
||||
line.amount_residual_currency = 0
|
||||
@ -441,7 +444,7 @@ class AccountMoveLine(models.Model):
|
||||
help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line.")
|
||||
date = fields.Date(related='move_id.date', string='Date', index=True, store=True, copy=False) # related is required
|
||||
analytic_line_ids = fields.One2many('account.analytic.line', 'move_id', string='Analytic lines', oldname="analytic_lines")
|
||||
tax_ids = fields.Many2many('account.tax', string='Taxes')
|
||||
tax_ids = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
|
||||
tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict')
|
||||
analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account')
|
||||
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic tags')
|
||||
|
@ -527,14 +527,17 @@ var StatementModel = BasicModel.extend({
|
||||
|
||||
// Retrieve the toggle proposition
|
||||
var selected;
|
||||
_.each(line.reconciliation_proposition, function (prop) {
|
||||
var targetLineAmount = line.st_line.amount;
|
||||
line.reconciliation_proposition.every(function (prop) {
|
||||
if (!prop.invalid) {
|
||||
if (((line.balance.amount < 0 || !line.partial_reconcile) && prop.amount > 0 && line.st_line.amount > 0 && line.st_line.amount < prop.amount) ||
|
||||
((line.balance.amount > 0 || !line.partial_reconcile) && prop.amount < 0 && line.st_line.amount < 0 && line.st_line.amount > prop.amount)) {
|
||||
if (((line.balance.amount < 0 || !line.partial_reconcile) && prop.amount > 0 && targetLineAmount > 0 && targetLineAmount < prop.amount) ||
|
||||
((line.balance.amount > 0 || !line.partial_reconcile) && prop.amount < 0 && targetLineAmount < 0 && targetLineAmount > prop.amount)) {
|
||||
selected = prop;
|
||||
return false;
|
||||
}
|
||||
targetLineAmount -= prop.amount;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// If no toggled proposition found, reject it
|
||||
|
@ -359,18 +359,21 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
|
||||
// loop state propositions
|
||||
var props = [];
|
||||
var nb_debit_props = 0;
|
||||
var nb_credit_props = 0;
|
||||
var partialDebitProps = 0;
|
||||
var partialCreditProps = 0;
|
||||
_.each(state.reconciliation_proposition, function (prop) {
|
||||
if (prop.display) {
|
||||
props.push(prop);
|
||||
if (prop.amount < 0)
|
||||
nb_debit_props += 1;
|
||||
else if (prop.amount > 0)
|
||||
nb_credit_props += 1;
|
||||
if (prop.amount > 0 && prop.amount > state.st_line.amount) {
|
||||
partialDebitProps++;
|
||||
} else if (prop.amount < 0 && prop.amount < state.st_line.amount) {
|
||||
partialCreditProps++;
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
var targetLineAmount = state.st_line.amount;
|
||||
_.each(props, function (line) {
|
||||
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
|
||||
if (!isNaN(line.id)) {
|
||||
@ -379,10 +382,10 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line}));
|
||||
}
|
||||
if (line.already_paid === false &&
|
||||
((state.balance.amount_currency < 0 || line.partial_reconcile) && nb_credit_props == 1
|
||||
&& line.amount > 0 && state.st_line.amount > 0 && state.st_line.amount < line.amount) ||
|
||||
((state.balance.amount_currency > 0 || line.partial_reconcile) && nb_debit_props == 1
|
||||
&& line.amount < 0 && state.st_line.amount < 0 && state.st_line.amount > line.amount)) {
|
||||
((state.balance.amount_currency < 0 || line.partial_reconcile)
|
||||
&& line.amount > 0 && state.st_line.amount > 0 && targetLineAmount < line.amount && partialDebitProps <= 1) ||
|
||||
((state.balance.amount_currency > 0 || line.partial_reconcile)
|
||||
&& line.amount < 0 && state.st_line.amount < 0 && targetLineAmount > line.amount && partialCreditProps <= 1)) {
|
||||
var $cell = $line.find(line.amount > 0 ? '.cell_right' : '.cell_left');
|
||||
var text;
|
||||
if (line.partial_reconcile) {
|
||||
@ -396,6 +399,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
.prependTo($cell)
|
||||
.attr("data-content", text);
|
||||
}
|
||||
targetLineAmount -= line.amount;
|
||||
|
||||
$props.append($line);
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,14 @@
|
||||
flectra.define('account.reconciliation_tests.data', function () {
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
* Debug tip:
|
||||
* To be able to "see" the test in the browser:
|
||||
* var $body = $('body');
|
||||
* $body.addClass('debug');
|
||||
* clientAction.appendTo($body);
|
||||
*/
|
||||
|
||||
var Datas = {};
|
||||
|
||||
var db = {
|
||||
@ -1490,5 +1498,194 @@ QUnit.module('account', {
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Reconciliation: Payment < inv1 + inv2(partial)', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
/*
|
||||
* One payment: $1175
|
||||
* Two Invoices
|
||||
* The first invoice will be fully reconciled $650
|
||||
* The second invoice will be partially paid with the rest of the payment $999
|
||||
*/
|
||||
|
||||
// modify the second line that is already in db to put it at $999
|
||||
var indexModif = _.findIndex(this.params.mv_lines['[5,"",0,6]'], function (line) {return line.id === 112});
|
||||
this.params.mv_lines['[5,"",0,6]'][indexModif] =
|
||||
{account_type: "receivable", amount_currency_str: "", currency_id: false, date_maturity: "2017-02-07", date: "2017-01-08",
|
||||
total_amount_str: "$ 999.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: "$ 999.00", debit: 999.0, account_code: "101200", ref: "", already_paid: false};
|
||||
|
||||
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
|
||||
testUtils.addMockEnvironment(clientAction, {
|
||||
data: this.params.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'process_reconciliations') {
|
||||
assert.deepEqual(args.args,
|
||||
[
|
||||
[5], // Id of the bank statement line
|
||||
|
||||
[{counterpart_aml_dicts:
|
||||
[{name:"INV/2017/0002",
|
||||
debit: 0,
|
||||
credit: 650,
|
||||
counterpart_aml_id: 109},
|
||||
|
||||
{name: "INV/2017/0003",
|
||||
debit: 0,
|
||||
credit: 525,
|
||||
counterpart_aml_id: 112}],
|
||||
|
||||
payment_aml_ids: [],
|
||||
partner_id: 8,
|
||||
new_aml_dicts: []}]
|
||||
], "should call process_reconciliations with partial reconcile values");
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
session: {
|
||||
currencies: {
|
||||
3: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$"
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
clientAction.appendTo($('#qunit-fixture'));
|
||||
|
||||
// The first reconciliation "line" is where it happens
|
||||
var widget = clientAction.widgets[0];
|
||||
|
||||
// Add first invoice to reconcile fully
|
||||
widget.$('.match .cell_account_code:first').trigger('click');
|
||||
assert.notOk( widget.$('.cell_right .line_info_button').length,
|
||||
"should not display the partial reconciliation alert");
|
||||
|
||||
// Add second invoice to reconcile partially
|
||||
widget.$('.match .cell_account_code:first').trigger('click');
|
||||
var $reconciliationAlert = widget.$('.cell_right .line_info_button');
|
||||
|
||||
assert.ok($reconciliationAlert.length,
|
||||
"should display the partial reconciliation alert");
|
||||
|
||||
$reconciliationAlert.click();
|
||||
|
||||
var $buttonReconcile = widget.$('button.o_reconcile:not(hidden)');
|
||||
|
||||
assert.equal($buttonReconcile.length, 1,
|
||||
'The reconcile button must be visible');
|
||||
|
||||
$buttonReconcile.click();
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Reconciliation: payment and 2 partials', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
/*
|
||||
* One payment: $1175
|
||||
* Two Invoices as Inv1 = 1200; Inv2 = 1200:
|
||||
* Payment < Inv1 AND Payment < Inv2
|
||||
* No partial reconcile is possible, as a write-off of 1225 is necessary
|
||||
*/
|
||||
|
||||
// modify the invoice line to have their amount > payment
|
||||
var indexInv1 = _.findIndex(this.params.mv_lines['[5,"",0,6]'], function (line) {return line.id === 109});
|
||||
this.params.mv_lines['[5,"",0,6]'][indexInv1] =
|
||||
{account_type: "receivable", amount_currency_str: "", currency_id: false, date_maturity: "2017-02-07", date: "2017-01-08",
|
||||
total_amount_str: "$ 1200.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: "$ 1200.00", debit: 1200.0,
|
||||
account_code: "101200", ref: "", already_paid: false};
|
||||
|
||||
var indexInv2 = _.findIndex(this.params.mv_lines['[5,"",0,6]'], function (line) {return line.id === 112});
|
||||
this.params.mv_lines['[5,"",0,6]'][indexInv2] =
|
||||
{account_type: "receivable", amount_currency_str: "", currency_id: false, date_maturity: "2017-02-07", date: "2017-01-08",
|
||||
total_amount_str: "$ 1200.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: "$ 1200.00", debit: 1200.0, account_code: "101200", ref: "", already_paid: false};
|
||||
|
||||
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
|
||||
testUtils.addMockEnvironment(clientAction, {
|
||||
data: this.params.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'process_reconciliations') {
|
||||
assert.deepEqual(args.args,
|
||||
[
|
||||
[5], // Id of the bank statement line
|
||||
|
||||
[{counterpart_aml_dicts:
|
||||
[{name:"INV/2017/0002",
|
||||
debit: 0,
|
||||
credit: 1200,
|
||||
counterpart_aml_id: 109},
|
||||
|
||||
{name: "INV/2017/0003",
|
||||
debit: 0,
|
||||
credit: 1200,
|
||||
counterpart_aml_id: 112}],
|
||||
|
||||
payment_aml_ids: [],
|
||||
partner_id: 8,
|
||||
new_aml_dicts: [
|
||||
{account_id: 282,
|
||||
credit: 0,
|
||||
debit: 1225,
|
||||
name: 'SAJ/2014/002 and SAJ/2014/003',
|
||||
}
|
||||
]}]
|
||||
], "should call process_reconciliations with new aml dict reconcile values");
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
session: {
|
||||
currencies: {
|
||||
3: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$"
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
clientAction.appendTo($('#qunit-fixture'));
|
||||
|
||||
// The first reconciliation "line" is where it happens
|
||||
var widget = clientAction.widgets[0];
|
||||
|
||||
// Add first invoice
|
||||
// There should be the opportunity to reconcile partially
|
||||
widget.$('.match .cell_account_code:first').trigger('click');
|
||||
assert.ok(widget.$('.cell_right .line_info_button').length,
|
||||
"should display the partial reconciliation alert");
|
||||
|
||||
// Add second invoice
|
||||
widget.$('.match .cell_account_code:first').trigger('click');
|
||||
assert.notOk(widget.$('.cell_right .line_info_button').length,
|
||||
"should not display the partial reconciliation alert");
|
||||
|
||||
var writeOffCreate = widget.$('div.create');
|
||||
|
||||
assert.equal(writeOffCreate.length, 1,
|
||||
'A write-off creation should be present');
|
||||
|
||||
assert.equal(writeOffCreate.find('input[name=amount]').val(), -1225,
|
||||
'The right amount should be proposed for the write-off');
|
||||
|
||||
writeOffCreate.find('.create_account_id input.ui-autocomplete-input').click();
|
||||
$('ul.ui-autocomplete li a:first').click(); // select first account to do the write off in
|
||||
|
||||
var $buttonReconcile = widget.$('button.o_reconcile:not(hidden)');
|
||||
|
||||
assert.equal($buttonReconcile.length, 1,
|
||||
'The reconcile button must be visible');
|
||||
|
||||
$buttonReconcile.click();
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -789,11 +789,19 @@ class TestReconciliation(AccountingTestCase):
|
||||
|
||||
statement = self.make_payment(invoice, journal, 50)
|
||||
|
||||
# The report searches on the create_date to dispatch reconciled lines to report periods
|
||||
# Also, in this case, there can be only 1 partial_reconcile
|
||||
statement_partial_id = statement.move_line_ids.mapped(lambda l: l.matched_credit_ids + l.matched_debit_ids)
|
||||
self.env.cr.execute('UPDATE account_partial_reconcile SET create_date = %(date)s WHERE id = %(partial_id)s',
|
||||
{'date': report_date_to + ' 00:00:00',
|
||||
'partial_id': statement_partial_id.id})
|
||||
|
||||
# Case 1: The invoice and payment are reconciled: Nothing should appear
|
||||
report_lines, total, amls = AgedReport._get_partner_move_lines(account_type, report_date_to, 'posted', 30, self.env.user.company_id.branch_id)
|
||||
|
||||
partner_lines = [line for line in report_lines if line['partner_id'] == partner.id]
|
||||
self.assertEqual(partner_lines, [], 'The aged receivable shouldn\'t have lines at this point')
|
||||
self.assertFalse(partner.id in amls, 'The aged receivable should not have amls either')
|
||||
self.assertFalse(amls.get(partner.id, False), 'The aged receivable should not have amls either')
|
||||
|
||||
# Case 2: The invoice and payment are not reconciled: we should have one line on the report
|
||||
# and 2 amls
|
||||
@ -810,3 +818,71 @@ class TestReconciliation(AccountingTestCase):
|
||||
|
||||
self.assertEqual(positive_line[0]['amount'], 50.0, 'The amount of the amls should be 50')
|
||||
self.assertEqual(negative_line[0]['amount'], -50.0, 'The amount of the amls should be -50')
|
||||
|
||||
def test_revert_payment_and_reconcile_exchange(self):
|
||||
|
||||
# A reversal of a reconciled payment which created a currency exchange entry, should create reversal moves
|
||||
# which move lines should be reconciled two by two with the original move's lines
|
||||
|
||||
def _determine_debit_credit_line(move):
|
||||
line_ids_reconciliable = move.line_ids.filtered(lambda l: l.account_id.reconcile or l.account_id.internal_type == 'liquidity')
|
||||
return line_ids_reconciliable.filtered(lambda l: l.debit), line_ids_reconciliable.filtered(lambda l: l.credit)
|
||||
|
||||
def _move_revert_test_pair(move, revert):
|
||||
self.assertTrue(move.line_ids)
|
||||
self.assertTrue(revert.line_ids)
|
||||
|
||||
move_lines = _determine_debit_credit_line(move)
|
||||
revert_lines = _determine_debit_credit_line(revert)
|
||||
|
||||
# in the case of the exchange entry, only one pair of lines will be found
|
||||
if move_lines[0] and revert_lines[1]:
|
||||
self.assertTrue(move_lines[0].full_reconcile_id.exists())
|
||||
self.assertEqual(move_lines[0].full_reconcile_id.id, revert_lines[1].full_reconcile_id.id)
|
||||
|
||||
if move_lines[1] and revert_lines[0]:
|
||||
self.assertTrue(move_lines[1].full_reconcile_id.exists())
|
||||
self.assertEqual(move_lines[1].full_reconcile_id.id, revert_lines[0].full_reconcile_id.id)
|
||||
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': time.strftime('%Y') + '-07-01',
|
||||
'rate': 1.0,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'company_id': self.env.ref('base.main_company').id
|
||||
})
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': time.strftime('%Y') + '-08-01',
|
||||
'rate': 0.5,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'company_id': self.env.ref('base.main_company').id
|
||||
})
|
||||
inv = self.create_invoice(invoice_amount=111, currency_id=self.currency_usd_id)
|
||||
payment = self.env['account.payment'].create({
|
||||
'payment_type': 'inbound',
|
||||
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
|
||||
'partner_type': 'customer',
|
||||
'partner_id': self.partner_agrolait_id,
|
||||
'amount': 111,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'journal_id': self.bank_journal_usd.id,
|
||||
'payment_date': time.strftime('%Y') + '-08-01',
|
||||
})
|
||||
payment.post()
|
||||
|
||||
credit_aml = payment.move_line_ids.filtered('credit')
|
||||
inv.assign_outstanding_credit(credit_aml.id)
|
||||
self.assertTrue(inv.state == 'paid', 'The invoice should be paid')
|
||||
|
||||
exchange_reconcile = payment.move_line_ids.mapped('full_reconcile_id')
|
||||
exchange_move = exchange_reconcile.exchange_move_id
|
||||
payment_move = payment.move_line_ids[0].move_id
|
||||
|
||||
reverted_payment_move = self.env['account.move'].browse(payment_move.reverse_moves(time.strftime('%Y') + '-08-01'))
|
||||
|
||||
# After reversal of payment, the invoice should be open
|
||||
self.assertTrue(inv.state == 'open', 'The invoice should be open again')
|
||||
self.assertFalse(exchange_reconcile.exists())
|
||||
|
||||
reverted_exchange_move = self.env['account.move'].search([('journal_id', '=', exchange_move.journal_id.id), ('ref', 'ilike', exchange_move.name)], limit=1)
|
||||
_move_revert_test_pair(payment_move, reverted_payment_move)
|
||||
_move_revert_test_pair(exchange_move, reverted_exchange_move)
|
||||
|
@ -944,6 +944,14 @@ class Meeting(models.Model):
|
||||
self.start = self.start_datetime
|
||||
self.stop = fields.Datetime.to_string(start + timedelta(hours=self.duration))
|
||||
|
||||
@api.onchange('start_date')
|
||||
def _onchange_start_date(self):
|
||||
self.start = self.start_date
|
||||
|
||||
@api.onchange('stop_date')
|
||||
def _onchange_stop_date(self):
|
||||
self.stop = self.stop_date
|
||||
|
||||
####################################################
|
||||
# Calendar Business, Reccurency, ...
|
||||
####################################################
|
||||
|
@ -250,7 +250,8 @@ class ProductTemplate(models.Model):
|
||||
@api.one
|
||||
@api.depends('product_variant_ids.product_tmpl_id')
|
||||
def _compute_product_variant_count(self):
|
||||
self.product_variant_count = len(self.product_variant_ids)
|
||||
# do not pollute variants to be prefetched when counting variants
|
||||
self.product_variant_count = len(self.with_prefetch().product_variant_ids)
|
||||
|
||||
@api.depends('product_variant_ids', 'product_variant_ids.default_code')
|
||||
def _compute_default_code(self):
|
||||
|
@ -593,7 +593,9 @@ class ProductUoM(models.Model):
|
||||
if 'factor' in values or 'factor_inv' in values or 'category_id' in values:
|
||||
changed = self.filtered(
|
||||
lambda u: any(u[f] != values[f] if f in values else False
|
||||
for f in {'factor', 'factor_inv', 'category_id'}))
|
||||
for f in {'factor', 'factor_inv'})) + self.filtered(
|
||||
lambda u: any(u[f].id != int(values[f]) if f in values else False
|
||||
for f in {'category_id'}))
|
||||
if changed:
|
||||
stock_move_lines = self.env['stock.move.line'].search_count([
|
||||
('product_uom_id.category_id', 'in', changed.mapped('category_id.id')),
|
||||
|
@ -345,7 +345,8 @@ class ProductProduct(models.Model):
|
||||
qty_delivered += qty_to_consider
|
||||
# `move.price_unit` is negative if the move is out and positive if the move is
|
||||
# dropshipped. Use its absolute value to compute the average price unit.
|
||||
average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + abs(move.price_unit) * qty_to_consider) / qty_delivered
|
||||
if qty_delivered:
|
||||
average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + abs(move.price_unit) * qty_to_consider) / qty_delivered
|
||||
if qty_delivered == quantity:
|
||||
break
|
||||
return average_price_unit
|
||||
|
@ -151,7 +151,7 @@ class StockMove(models.Model):
|
||||
|
||||
def _get_price_unit(self):
|
||||
""" Returns the unit price to store on the quant """
|
||||
return self.price_unit or self.product_id.standard_price
|
||||
return not self.company_id.currency_id.is_zero(self.price_unit) and self.price_unit or self.product_id.standard_price
|
||||
|
||||
@api.model
|
||||
def _get_in_base_domain(self, company_id=False):
|
||||
|
Loading…
Reference in New Issue
Block a user