[FIX] account: foreign exchanges gain/loss in reconciliation

When processing the reconciliation of invoices with bank statements
in foreign currencies, this is possible that there is a cent of difference,
due to the fact
the sum of amount exchanged could not be equal to the exchanged
sum of amount received.

For instance,
with a company in EUR as currency,
with a rate of 0.033 for USD,
with an invoice of 2.00 USD
(60.606060... rounded to 60.61 EUR)
and a bank statement of two lines of 1.00 USD
(30.30303030... rounded to 30.30 EUR)
The exchanged invoice amount, 60.61 EUR, is not equal to the sum of
statement lines exchanged amount (30.30 + 30.30 = 60.60 EUR).

In such a case, two journal items should be created in addition:
 - 0.01 in the debtors account
 - 0.01 in the foreign exchange loss account

opw-640078
This commit is contained in:
Denis Ledoux 2015-05-26 18:18:11 +02:00
parent 4fb497b653
commit a475a2721a
2 changed files with 172 additions and 5 deletions

View File

@ -711,6 +711,60 @@ class account_bank_statement_line(osv.osv):
'account_id': account_id
}
def _get_exchange_lines(self, cr, uid, st_line, mv_line, currency_diff, currency_id, move_id, context=None):
'''
Prepare the two lines in company currency due to currency rate difference.
:param line: browse record of the voucher.line for which we want to create currency rate difference accounting
entries
:param move_id: Account move wher the move lines will be.
:param currency_diff: Amount to be posted.
:param company_currency: id of currency of the company to which the voucher belong
:param current_currency: id of currency of the voucher
:return: the account move line and its counterpart to create, depicted as mapping between fieldname and value
:rtype: tuple of dict
'''
if currency_diff > 0:
exchange_account_id = st_line.company_id.expense_currency_exchange_account_id.id
else:
exchange_account_id = st_line.company_id.income_currency_exchange_account_id.id
# Even if the amount_currency is never filled, we need to pass the foreign currency because otherwise
# the receivable/payable account may have a secondary currency, which render this field mandatory
if mv_line.account_id.currency_id:
account_currency_id = mv_line.account_id.currency_id.id
else:
account_currency_id = st_line.company_id.currency_id.id != currency_id and currency_id or False
move_line = {
'journal_id': st_line.journal_id.id,
'period_id': st_line.statement_id.period_id.id,
'name': _('change') + ': ' + (st_line.name or '/'),
'account_id': mv_line.account_id.id,
'move_id': move_id,
'partner_id': st_line.partner_id.id,
'currency_id': account_currency_id,
'amount_currency': 0.0,
'quantity': 1,
'credit': currency_diff > 0 and currency_diff or 0.0,
'debit': currency_diff < 0 and -currency_diff or 0.0,
'date': st_line.date,
'counterpart_move_line_id': mv_line.id,
}
move_line_counterpart = {
'journal_id': st_line.journal_id.id,
'period_id': st_line.statement_id.period_id.id,
'name': _('change') + ': ' + (st_line.name or '/'),
'account_id': exchange_account_id,
'move_id': move_id,
'amount_currency': 0.0,
'partner_id': st_line.partner_id.id,
'currency_id': account_currency_id,
'quantity': 1,
'debit': currency_diff > 0 and currency_diff or 0.0,
'credit': currency_diff < 0 and -currency_diff or 0.0,
'date': st_line.date,
}
return (move_line, move_line_counterpart)
def process_reconciliations(self, cr, uid, data, context=None):
for datum in data:
self.process_reconciliation(cr, uid, datum[0], datum[1], context=context)
@ -809,6 +863,21 @@ class account_bank_statement_line(osv.osv):
if credit_at_old_rate - credit_at_current_rate:
currency_diff = credit_at_current_rate - credit_at_old_rate
to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context))
if mv_line.currency_id and mv_line_dict['currency_id'] == mv_line.currency_id.id:
amount_unreconciled = mv_line.amount_residual_currency
else:
amount_unreconciled = currency_obj.compute(cr, uid, company_currency.id, mv_line_dict['currency_id'] , mv_line.amount_residual, context=ctx)
if float_is_zero(mv_line_dict['amount_currency'] + amount_unreconciled, precision_rounding=mv_line.currency_id.rounding):
import pudb
pu.db
amount = mv_line_dict['debit'] or mv_line_dict['credit']
sign = -1 if mv_line_dict['debit'] else 1
currency_rate_difference = sign * (mv_line.amount_residual - amount)
if not company_currency.is_zero(currency_rate_difference):
exchange_lines = self._get_exchange_lines(cr, uid, st_line, mv_line, currency_rate_difference, mv_line_dict['currency_id'], move_id, context=context)
for exchange_line in exchange_lines:
to_create.append(exchange_line)
else:
mv_line_dict['debit'] = debit_at_current_rate
mv_line_dict['credit'] = credit_at_current_rate

View File

@ -14,20 +14,25 @@ class TestReconciliation(TransactionCase):
self.account_invoice_line_model = self.registry('account.invoice.line')
self.acc_bank_stmt_model = self.registry('account.bank.statement')
self.acc_bank_stmt_line_model = self.registry('account.bank.statement.line')
self.res_currency_model = self.registry('res.currency')
self.res_currency_rate_model = self.registry('res.currency.rate')
self.partner_agrolait_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "base", "res_partner_2")[1]
self.currency_swiss_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "base", "CHF")[1]
self.currency_usd_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "base", "USD")[1]
self.account_rcv_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "a_recv")[1]
self.account_rsa_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "rsa")[1]
self.account_fx_income_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "income_fx_income")[1]
self.account_fx_expense_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "income_fx_expense")[1]
self.product_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "product", "product_product_4")[1]
self.bank_journal_usd_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "bank_journal_usd")[1]
self.account_usd_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "usd_bnk")[1]
self.company_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "base", "main_company")[1]
#set expense_currency_exchange_account_id and income_currency_exchange_account_id to a random account
self.registry("res.company").write(self.cr, self.uid, [self.company_id], {'expense_currency_exchange_account_id': self.account_rsa_id, 'income_currency_exchange_account_id':self.account_rsa_id})
#set expense_currency_exchange_account_id and income_currency_exchange_account_id to the according accounts
self.registry("res.company").write(self.cr, self.uid, [self.company_id], {'expense_currency_exchange_account_id': self.account_fx_expense_id, 'income_currency_exchange_account_id':self.account_fx_income_id})
def test_balanced_customer_invoice(self):
cr, uid = self.cr, self.uid
@ -92,7 +97,7 @@ class TestReconciliation(TransactionCase):
self.assertEquals(move_line.currency_id.id, self.currency_swiss_id)
checked_line += 1
continue
if move_line.account_id.id == self.account_rsa_id:
if move_line.account_id.id == self.account_fx_expense_id:
self.assertEquals(move_line.debit, 10.74)
self.assertEquals(move_line.credit, 0.0)
checked_line += 1
@ -164,10 +169,103 @@ class TestReconciliation(TransactionCase):
self.assertEquals(move_line.currency_id.id, self.currency_swiss_id)
checked_line += 1
continue
if move_line.account_id.id == self.account_rsa_id:
if move_line.account_id.id == self.account_fx_income_id:
self.assertEquals(move_line.debit, 0.0)
self.assertEquals(move_line.credit, 10.74)
checked_line += 1
continue
self.assertEquals(checked_line, 3)
def test_balanced_exchanges_gain_loss(self):
# The point of this test is to show that we handle correctly the gain/loss exchanges during reconciliations in foreign currencies.
# For instance, with a company set in EUR, and a USD rate set to 0.033,
# the reconciliation of an invoice of 2.00 USD (60.61 EUR) and a bank statement of two lines of 1.00 USD (30.30 EUR)
# will lead to an exchange loss, that should be handled correctly within the journal items.
cr, uid = self.cr, self.uid
# We update the currency rate of the currency USD in order to force the gain/loss exchanges in next steps
self.res_currency_rate_model.create(cr, uid, {
'name': time.strftime('%Y-%m-%d') + ' 00:00:00',
'currency_id': self.currency_usd_id,
'rate': 0.033,
})
# We create a customer invoice of 2.00 USD
invoice_id = self.account_invoice_model.create(cr, uid, {
'partner_id': self.partner_agrolait_id,
'currency_id': self.currency_usd_id,
'name': 'Foreign invoice with exchange gain',
'account_id': self.account_rcv_id,
'type': 'out_invoice',
'date_invoice': time.strftime('%Y-%m-%d'),
'journal_id': self.bank_journal_usd_id,
'invoice_line': [
(0, 0, {
'name': 'line that will lead to an exchange gain',
'quantity': 1,
'price_unit': 2,
})
]
})
self.registry('account.invoice').signal_workflow(cr, uid, [invoice_id], 'invoice_open')
invoice = self.account_invoice_model.browse(cr, uid, invoice_id)
# We create a bank statement with two lines of 1.00 USD each.
bank_stmt_id = self.acc_bank_stmt_model.create(cr, uid, {
'journal_id': self.bank_journal_usd_id,
'date': time.strftime('%Y-%m-%d'),
'line_ids': [
(0, 0, {
'name': 'half payment',
'partner_id': self.partner_agrolait_id,
'amount': 1.0,
'date': time.strftime('%Y-%m-%d')
}),
(0, 0, {
'name': 'second half payment',
'partner_id': self.partner_agrolait_id,
'amount': 1.0,
'date': time.strftime('%Y-%m-%d')
})
]
})
statement = self.acc_bank_stmt_model.browse(cr, uid, bank_stmt_id)
# We process the reconciliation of the invoice line with the two bank statement lines
line_id = None
for l in invoice.move_id.line_id:
if l.account_id.id == self.account_rcv_id:
line_id = l
break
for statement_line in statement.line_ids:
self.acc_bank_stmt_line_model.process_reconciliation(cr, uid, statement_line.id, [
{'counterpart_move_line_id': line_id.id, 'credit': 1.0, 'debit': 0.0, 'name': line_id.name}
])
# The invoice should be paid, as the payments totally cover its total
self.assertEquals(invoice.state, 'paid', 'The invoice should be paid by now')
reconcile = None
for payment in invoice.payment_ids:
reconcile = payment.reconcile_id
break
# The invoice should be reconciled (entirely, not a partial reconciliation)
self.assertTrue(reconcile, 'The invoice should be totally reconciled')
result = {}
exchange_loss_line = None
for line in reconcile.line_id:
res_account = result.setdefault(line.account_id, {'debit': 0.0, 'credit': 0.0, 'count': 0})
res_account['debit'] = res_account['debit'] + line.debit
res_account['credit'] = res_account['credit'] + line.credit
res_account['count'] += 1
if line.credit == 0.01:
exchange_loss_line = line
# We should be able to find a move line of 0.01 EUR on the Debtors account, being the cent we lost during the currency exchange
self.assertTrue(exchange_loss_line, 'There should be one move line of 0.01 EUR in credit')
# The journal items of the reconciliation should have their debit and credit total equal
# Besides, the total debit and total credit should be 60.61 EUR (2.00 USD)
self.assertEquals(sum([res['debit'] for res in result.values()]), 60.61)
self.assertEquals(sum([res['credit'] for res in result.values()]), 60.61)
counterpart_exchange_loss_line = None
for line in exchange_loss_line.move_id.line_id:
if line.account_id.id == self.account_fx_expense_id:
counterpart_exchange_loss_line = line
# We should be able to find a move line of 0.01 EUR on the Foreign Exchange Loss account
self.assertTrue(counterpart_exchange_loss_line, 'There should be one move line of 0.01 EUR on account "Foreign Exchange Loss"')