diff options
| author | Lucas Perais (lpe) <lpe@odoo.com> | 2017-07-20 10:24:52 +0200 |
|---|---|---|
| committer | Lucas Perais (lpe) <lpe@odoo.com> | 2017-08-04 09:45:31 +0200 |
| commit | 2cd63aa2dc2ab0706922a660242b2132694e41a8 (patch) | |
| tree | 6e0bb8355efb46cdfba9d7093923da5905886681 | |
| parent | 7cd2f63705f8c4ddfd05c70d79aa6261ae191d18 (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.py | 97 | ||||
| -rw-r--r-- | addons/account/tests/test_reconciliation.py | 146 |
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) |
