[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
This commit is contained in:
Lucas Perais (lpe)
2017-08-04 09:45:31 +02:00
parent 7cd2f63705
commit 2cd63aa2dc
2 changed files with 229 additions and 14 deletions
+84 -13
View File
@@ -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
+145 -1
View File
@@ -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'])
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)