summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLucas Perais (lpe) <lpe@odoo.com>2017-07-20 10:24:52 +0200
committerLucas Perais (lpe) <lpe@odoo.com>2017-08-04 09:45:31 +0200
commit2cd63aa2dc2ab0706922a660242b2132694e41a8 (patch)
tree6e0bb8355efb46cdfba9d7093923da5905886681
parent7cd2f63705f8c4ddfd05c70d79aa6261ae191d18 (diff)
[FIX] account: reconciliation of payments and invoices, diff currencies and times
Use case: - 2 invoices in foreign currencies, made at different times (and different exchange rates) - 2 payments (same characteristics as invoices) - allocate the 1st payment to inv A - allocate the 2nd payment to inv A and then B so that A is fully paid, but B is not - try to make the final payment on B to fully reconcile everything Results (all in the payable account) - let CB be the credit line of B - let D2 be the debit line of 2nd payment - let D3 be the debit line of the 3rd and final payment Before this commit, the exchange difference computation dailed because: - There were 2 amls that had an amount_residual and needed to be reconciled (CB and D2) - Also, there was a structural flaw in the code that did not allow for multiple lines to have a residual After this commit, we still make 1 move to log the exchange difference, but we make exchange difference lines for each line that need to be reconciled: CB and D2 OPW 751710 Commit in master c36f70f6726e0c7ccca1002f78e4d402e27cae3d (2017-07-20) Closes #18347 [FIX] account: testing partial and full reconciliation
-rw-r--r--addons/account/models/account_move.py97
-rw-r--r--addons/account/tests/test_reconciliation.py146
2 files changed, 229 insertions, 14 deletions
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 094c2a5a479..816eaafb382 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -1409,17 +1409,60 @@ class AccountPartialReconcile(models.Model):
move.post()
return line_to_reconcile, partial_rec
- @api.model
- def create(self, vals):
- res = super(AccountPartialReconcile, self).create(vals)
+ # Do not forwardport in master as of 2017-07-20
+ def _fix_multiple_exchange_rates_diff(self, amls_to_fix, amount_diff, diff_in_currency, currency, move):
+ self.ensure_one()
+ move_lines = self.env['account.move.line'].with_context(check_move_validity=False)
+ partial_reconciles = self.with_context(skip_full_reconcile_check=True)
+ amount_diff = self.company_id.currency_id.round(amount_diff)
+ diff_in_currency = currency.round(diff_in_currency)
+
+ for aml in amls_to_fix:
+ account_payable_line = move_lines.create({
+ 'name': _('Currency exchange rate difference'),
+ 'debit': amount_diff < 0 and -aml.amount_residual or 0.0,
+ 'credit': amount_diff > 0 and aml.amount_residual or 0.0,
+ 'account_id': self.debit_move_id.account_id.id,
+ 'move_id': move.id,
+ 'currency_id': currency.id,
+ 'amount_currency': -aml.amount_residual_currency,
+ 'partner_id': self.debit_move_id.partner_id.id,
+ })
+
+ move_lines.create({
+ 'name': _('Currency exchange rate difference'),
+ 'debit': amount_diff > 0 and aml.amount_residual or 0.0,
+ 'credit': amount_diff < 0 and -aml.amount_residual or 0.0,
+ 'account_id': amount_diff > 0 and self.company_id.currency_exchange_journal_id.default_debit_account_id.id or self.company_id.currency_exchange_journal_id.default_credit_account_id.id,
+ 'move_id': move.id,
+ 'currency_id': currency.id,
+ 'amount_currency': aml.amount_residual_currency,
+ 'partner_id': self.debit_move_id.partner_id.id})
+
+ partial_rec = super(AccountPartialReconcile, partial_reconciles).create({
+ 'debit_move_id': aml.credit and account_payable_line.id or aml.id,
+ 'credit_move_id': aml.debit and account_payable_line.id or aml.id,
+ 'amount': abs(aml.amount_residual),
+ 'amount_currency': abs(aml.amount_residual_currency),
+ 'currency_id': currency.id,
+ })
+
+ move_lines |= account_payable_line
+ partial_reconciles |= partial_rec
+
+ partial_reconciles._compute_partial_lines()
+ return move_lines, partial_reconciles
+
+ # Do not forwardport in master as of 2017-07-20
+ def _compute_partial_lines(self):
if self._context.get('skip_full_reconcile_check'):
#when running the manual reconciliation wizard, don't check the partials separately for full
#reconciliation or exchange rate because it is handled manually after the whole processing
- return res
+ return self
#check if the reconcilation is full
#first, gather all journal items involved in the reconciliation just created
- partial_rec_set = OrderedDict.fromkeys([x for x in res])
- aml_set = self.env['account.move.line']
+ partial_rec_set = OrderedDict.fromkeys([x for x in self])
+ aml_set = aml_to_balance = self.env['account.move.line']
total_debit = 0
total_credit = 0
total_amount_currency = 0
@@ -1427,7 +1470,7 @@ class AccountPartialReconcile(models.Model):
#possible to compute the exchange difference entry and it has to be done manually.
currency = list(partial_rec_set)[0].currency_id
maxdate = None
- aml_to_balance = None
+
for partial_rec in partial_rec_set:
if partial_rec.currency_id != currency:
#no exchange rate entry will be created
@@ -1435,7 +1478,7 @@ class AccountPartialReconcile(models.Model):
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
if aml not in aml_set:
if aml.amount_residual or aml.amount_residual_currency:
- aml_to_balance = aml
+ aml_to_balance |= aml
maxdate = max(aml.date, maxdate)
total_debit += aml.debit
total_credit += aml.credit
@@ -1453,16 +1496,21 @@ class AccountPartialReconcile(models.Model):
aml_ids = aml_set.ids
#then, if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full
digits_rounding_precision = aml_set[0].company_id.currency_id.rounding
+
if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0:
exchange_move_id = False
exchange_partial_rec_id = False
if currency and aml_to_balance:
+ exchange_move = (self.env['account.move']
+ .create(self.env['account.full.reconcile']
+ ._prepare_exchange_diff_move(move_date=maxdate, company=aml_to_balance[0].company_id)))
#eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates
- rate_diff_aml, rate_diff_partial_rec = partial_rec.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, maxdate)
- aml_ids.append(rate_diff_aml.id)
- partial_rec_ids.append(rate_diff_partial_rec.id)
- exchange_move_id = rate_diff_aml.move_id.id
- exchange_partial_rec_id = rate_diff_partial_rec.id
+ rate_diff_amls, rate_diff_partial_recs = partial_rec._fix_multiple_exchange_rates_diff(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, exchange_move)
+ aml_ids += rate_diff_amls.ids
+ partial_rec_ids += rate_diff_partial_recs.ids
+ exchange_move.post()
+ exchange_move_id = exchange_move.id
+ exchange_partial_rec_id = rate_diff_partial_recs[-1:].id
#mark the reference of the full reconciliation on the partial ones and on the entries
self.env['account.full.reconcile'].with_context(check_move_validity=False).create({
'partial_reconcile_ids': [(4, p_id) for p_id in partial_rec_ids],
@@ -1470,6 +1518,12 @@ class AccountPartialReconcile(models.Model):
'exchange_move_id': exchange_move_id,
'exchange_partial_rec_id': exchange_partial_rec_id,
})
+
+ # Do not forwardport in master as of 2017-07-20
+ @api.model
+ def create(self, vals):
+ res = super(AccountPartialReconcile, self).create(vals)
+ res._compute_partial_lines()
return res
@api.multi
@@ -1531,3 +1585,20 @@ class AccountFullReconcile(models.Model):
#reconcile together the original exchange rate line and its reversal
pair_to_rec.reconcile()
return super(AccountFullReconcile, self).unlink()
+
+ # Do not forwardport in master as of 2017-07-20
+ @api.model
+ def _prepare_exchange_diff_move(self, move_date, company):
+ if not company.currency_exchange_journal_id:
+ raise UserError(_("You should configure the 'Exchange Rate Journal' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
+ if not company.income_currency_exchange_account_id.id:
+ raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
+ if not company.expense_currency_exchange_account_id.id:
+ raise UserError(_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
+ res = {'journal_id': company.currency_exchange_journal_id.id}
+ # The move date should be the maximum date between payment and invoice
+ # (in case of payment in advance). However, we should make sure the
+ # move date is not recorded after the end of year closing.
+ if move_date > company.fiscalyear_lock_date:
+ res['date'] = move_date
+ return res
diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py
index ad316e2b0a2..c0430d720c9 100644
--- a/addons/account/tests/test_reconciliation.py
+++ b/addons/account/tests/test_reconciliation.py
@@ -473,4 +473,148 @@ class TestReconciliation(AccountingTestCase):
self.assertEquals(round(aml.debit, 2), line['debit'])
self.assertEquals(round(aml.credit, 2), line['credit'])
self.assertEquals(round(aml.amount_currency, 2), line['amount_currency'])
- self.assertEquals(aml.currency_id.id, line['currency_id']) \ No newline at end of file
+ self.assertEquals(aml.currency_id.id, line['currency_id'])
+
+ def test_partial_reconcile_currencies(self):
+ # client Account (payable, rsa)
+ # Debit Credit
+ # --------------------------------------------------------
+ # Pay a : 25/0.5 = 50 | Inv a : 50/0.5 = 100
+ # Pay b: 50/0.75 = 66.66 | Inv b : 50/0.75 = 66.66
+ # Pay c: 25/0.8 = 31.25 |
+ #
+ # Debit_currency = 100 | Credit currency = 100
+ # Debit = 147.91 | Credit = 166.66
+ # Balance Debit = 18.75
+ # Counterpart Credit goes in Exchange diff
+
+ dest_journal_id = self.env['account.journal'].search([('type', '=', 'purchase'), ('company_id', '=', self.env.ref('base.main_company').id)], limit=1)
+ account_expenses = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1)
+
+ self.bank_journal_euro.write({'default_debit_account_id': self.account_rsa.id,
+ 'default_credit_account_id': self.account_rsa.id})
+ dest_journal_id.write({'default_debit_account_id': self.account_rsa.id,
+ 'default_credit_account_id': self.account_rsa.id})
+ # Setting up rates for USD (main_company is in EUR)
+ self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '07' + '-01',
+ 'rate': 0.5,
+ '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.75,
+ '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') + '-' + '09' + '-01',
+ 'rate': 0.80,
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.env.ref('base.main_company').id})
+
+ # Preparing Invoices (from vendor)
+ invoice_a = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
+ 'reference_type': 'none',
+ 'currency_id': self.currency_usd_id,
+ 'name': 'invoice to vendor',
+ 'account_id': self.account_rsa.id,
+ 'type': 'in_invoice',
+ 'date_invoice': time.strftime('%Y') + '-' + '07' + '-01',
+ })
+ self.account_invoice_line_model.create({'product_id': self.product.id,
+ 'quantity': 1,
+ 'price_unit': 50,
+ 'invoice_id': invoice_a.id,
+ 'name': 'product that cost ' + str(50),
+ 'account_id': account_expenses.id,
+ })
+
+ invoice_b = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
+ 'reference_type': 'none',
+ 'currency_id': self.currency_usd_id,
+ 'name': 'invoice to vendor',
+ 'account_id': self.account_rsa.id,
+ 'type': 'in_invoice',
+ 'date_invoice': time.strftime('%Y') + '-' + '08' + '-01',
+ })
+ self.account_invoice_line_model.create({'product_id': self.product.id,
+ 'quantity': 1,
+ 'price_unit': 50,
+ 'invoice_id': invoice_b.id,
+ 'name': 'product that cost ' + str(50),
+ 'account_id': account_expenses.id,
+ })
+
+ invoice_a.action_invoice_open()
+ invoice_b.action_invoice_open()
+
+ # Preparing Payments
+ # One partial for invoice_a (fully assigned to it)
+ payment_a = self.env['account.payment'].create({'payment_type': 'outbound',
+ 'amount': 25,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': self.env.ref('base.main_company').id,
+ 'payment_date': time.strftime('%Y') + '-' + '07' + '-01',
+ 'partner_id': self.partner_agrolait_id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
+ 'destination_journal_id': dest_journal_id.id,
+ 'partner_type': 'supplier'})
+
+ # One that will complete the payment of a, the rest goes to b
+ payment_b = self.env['account.payment'].create({'payment_type': 'outbound',
+ 'amount': 50,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': self.env.ref('base.main_company').id,
+ 'payment_date': time.strftime('%Y') + '-' + '08' + '-01',
+ 'partner_id': self.partner_agrolait_id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
+ 'destination_journal_id': dest_journal_id.id,
+ 'partner_type': 'supplier'})
+
+ # The last one will complete the payment of b
+ payment_c = self.env['account.payment'].create({'payment_type': 'outbound',
+ 'amount': 25,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': self.env.ref('base.main_company').id,
+ 'payment_date': time.strftime('%Y') + '-' + '09' + '-01',
+ 'partner_id': self.partner_agrolait_id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
+ 'destination_journal_id': dest_journal_id.id,
+ 'partner_type': 'supplier'})
+
+ payment_a.post()
+ payment_b.post()
+ payment_c.post()
+
+ # Assigning payments to invoices
+ debit_line_a = payment_a.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
+ debit_line_b = payment_b.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
+ debit_line_c = payment_c.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
+
+ invoice_a.assign_outstanding_credit(debit_line_a.id)
+ invoice_a.assign_outstanding_credit(debit_line_b.id)
+ invoice_b.assign_outstanding_credit(debit_line_b.id)
+ invoice_b.assign_outstanding_credit(debit_line_c.id)
+
+ # Asserting correctness (only in the payable account)
+ full_reconcile = False
+ for inv in (invoice_a + invoice_b):
+ self.assertTrue(inv.reconciled)
+ for aml in (inv.payment_move_line_ids + inv.move_id.line_ids).filtered(lambda l: l.account_id == self.account_rsa):
+ self.assertEqual(aml.amount_residual, 0.0)
+ self.assertEqual(aml.amount_residual_currency, 0.0)
+ self.assertTrue(aml.reconciled)
+ if not full_reconcile:
+ full_reconcile = aml.full_reconcile_id
+ else:
+ self.assertTrue(aml.full_reconcile_id == full_reconcile)
+
+ full_rec_move = full_reconcile.exchange_move_id
+ # Globally check whether the amount is correct
+ self.assertEqual(full_rec_move.amount, 18.75)
+
+ # Checking if the direction of the move is correct
+ full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa)
+ self.assertEqual(full_rec_payable.balance, 18.75)