[FIX] [tldr] account: multi-currencies fixes.

[FIX] account: fixed behavior in order to create the exchange rate difference entry at the full reconciliation only.

 This is needed in order to support the use case where an invoice in foreign currency is paid on a bank account in company currency,
 with the user encoding manually the difference due to the currency fluctuation (writeoff in the bank statement reconciliation interface): before
 this patch, Odoo was doing automatically the same, what results in a duplicated currency difference booking). ticket ID: 670876.
 A test has also been added in account/tests/test_reconcilication.py (test_statement_euro_invoice_usd_transaction_euro_full) to ensure that use case
 is now fixed.

[FIX] account_full_reconcile: a reconciliation is full as soon as either total_debit = total_credit OR if total_amount_currency is 0
[FIX] account: when registering a payment with a writeoff, always create the writeoff line exactly as entered by the user. Before, it was modified
 by the exchange rate entry, for some reason...
[FIX] account: on invoices, show the payment widget and outstanding payments in the invoice's currency always
[FIX] account: when assigning a payment in company currency on an invoice in foreign currency, save the amount_currency in order to create the correct
 exchange rate.
[FIX] account: show the currency difference entry on the invoice payments widget
[FIX] account: fixed the reconciliation methods in order to work with exchange rate entries that have debit = credit = 0
[FIX] account: fixed reconciled field computation. A line is reconciled only if its amount_residual AND amount_residual_currency are 0
[IMP] display the move name of payments on the invoice payments widget
This commit is contained in:
qdp-odoo
2016-03-15 11:54:47 +01:00
parent ce217acf18
commit 003f2ee9bc
5 changed files with 260 additions and 115 deletions
+22 -20
View File
@@ -115,18 +115,16 @@ class AccountInvoice(models.Model):
type_payment = _('Outstanding debits')
info = {'title': '', 'outstanding': True, 'content': [], 'invoice_id': self.id}
lines = self.env['account.move.line'].search(domain)
currency_id = self.currency_id
if len(lines) != 0:
for line in lines:
# get the outstanding residual value in invoice currency
# get the outstanding residual value in its currency. We don't want to show it
# in the invoice currency since the exchange rate between the invoice date and
# the payment date might have changed.
if line.currency_id:
currency_id = line.currency_id
if line.currency_id and line.currency_id == self.currency_id:
amount_to_show = abs(line.amount_residual_currency)
else:
currency_id = line.company_id.currency_id
amount_to_show = abs(line.amount_residual)
amount_to_show = line.company_id.currency_id.with_context(date=line.date).compute(abs(line.amount_residual), self.currency_id)
if float_is_zero(amount_to_show, precision_rounding=self.currency_id.rounding):
continue
info['content'].append({
'journal_name': line.ref or line.move_id.name,
'amount': amount_to_show,
@@ -145,25 +143,24 @@ class AccountInvoice(models.Model):
self.payments_widget = json.dumps(False)
if self.payment_move_line_ids:
info = {'title': _('Less Payment'), 'outstanding': False, 'content': []}
currency_id = self.currency_id
for payment in self.payment_move_line_ids:
#we don't take into account the movement created due to a change difference
if payment.currency_id and payment.move_id.rate_diff_partial_rec_id:
continue
if self.type in ('out_invoice', 'in_refund'):
amount = sum([p.amount for p in payment.matched_debit_ids if p.debit_move_id in self.move_id.line_ids])
amount_currency = sum([p.amount_currency for p in payment.matched_debit_ids if p.debit_move_id in self.move_id.line_ids])
elif self.type in ('in_invoice', 'out_refund'):
amount = sum([p.amount for p in payment.matched_credit_ids if p.credit_move_id in self.move_id.line_ids])
amount_currency = sum([p.amount_currency for p in payment.matched_credit_ids if p.credit_move_id in self.move_id.line_ids])
# Get the payment value in its currency. We don't want to show it in the invoice
# currency since the exchange rate between the invoice date and the payment date
# might have changed.
if payment.currency_id and amount_currency != 0:
currency_id = payment.currency_id
amount_to_show = -amount_currency
# get the payment value in invoice currency
if payment.currency_id and payment.currency_id == self.currency_id:
amount_to_show = amount_currency
else:
currency_id = payment.company_id.currency_id
amount_to_show = -amount
amount_to_show = payment.company_id.currency_id.with_context(date=payment.date).compute(amount, self.currency_id)
if float_is_zero(amount_to_show, precision_rounding=self.currency_id.rounding):
continue
payment_ref = payment.move_id.name
if payment.move_id.ref:
payment_ref += ' (' + payment.move_id.ref + ')'
info['content'].append({
'name': payment.name,
'journal_name': payment.journal_id.name,
@@ -174,7 +171,7 @@ class AccountInvoice(models.Model):
'date': payment.date,
'payment_id': payment.id,
'move_id': payment.move_id.id,
'ref': payment.move_id.ref,
'ref': payment_ref,
})
self.payments_widget = json.dumps(info)
@@ -543,9 +540,14 @@ class AccountInvoice(models.Model):
@api.v7
def assign_outstanding_credit(self, cr, uid, id, credit_aml_id, context=None):
credit_aml = self.pool.get('account.move.line').browse(cr, uid, credit_aml_id, context=context)
inv = self.browse(cr, uid, id, context=context)
if not credit_aml.currency_id and inv.currency_id != inv.company_id.currency_id:
credit_aml.with_context(allow_amount_currency=True).write({
'amount_currency': inv.company_id.currency_id.with_context(date=credit_aml.date).compute(credit_aml.balance, inv.currency_id),
'currency_id': inv.currency_id.id})
if credit_aml.payment_id:
credit_aml.payment_id.write({'invoice_ids': [(4, id, None)]})
return self.browse(cr, uid, id, context=context).register_payment(credit_aml)
return inv.register_payment(credit_aml)
@api.multi
def action_date_assign(self):
+139 -65
View File
@@ -5,9 +5,10 @@ from openerp import api, fields, models, _
from openerp.osv import expression
from openerp.exceptions import RedirectWarning, UserError
from openerp.tools.misc import formatLang
from openerp.tools import float_is_zero
from openerp.tools import float_compare, float_is_zero
from openerp.tools.safe_eval import safe_eval
from collections import OrderedDict
#----------------------------------------------------------
# Entries
@@ -239,6 +240,9 @@ class AccountMoveLine(models.Model):
amount = abs(line.debit - line.credit)
amount_residual_currency = abs(line.amount_currency) or 0.0
sign = 1 if (line.debit - line.credit) > 0 else -1
if not line.debit and not line.credit and line.amount_currency and line.currency_id:
#residual for exchange rate entries
sign = 1 if float_compare(line.amount_currency, 0, precision_rounding=line.currency_id.rounding) == 1 else -1
for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
# If line is a credit (sign = -1) we:
@@ -260,8 +264,12 @@ class AccountMoveLine(models.Model):
#we can only check the amount in company currency
reconciled = False
digits_rounding_precision = line.company_id.currency_id.rounding
if float_is_zero(amount, precision_rounding=digits_rounding_precision) and (line.debit or line.credit):
reconciled = True
if float_is_zero(amount, precision_rounding=digits_rounding_precision):
if line.currency_id and line.amount_currency:
if float_is_zero(amount_residual_currency, precision_rounding=line.currency_id.rounding):
reconciled = True
else:
reconciled = True
line.reconciled = reconciled
line.amount_residual = line.company_id.currency_id.round(amount * sign)
@@ -727,15 +735,20 @@ class AccountMoveLine(models.Model):
def _get_pair_to_reconcile(self):
#field is either 'amount_residual' or 'amount_residual_currency' (if the reconciled account has a secondary currency set)
field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
rounding = self[0].company_id.currency_id.rounding
if self[0].currency_id and all([x.amount_currency and x.currency_id == self[0].currency_id for x in self]):
#or if all lines share the same currency
field = 'amount_residual_currency'
rounding = self[0].currency_id.rounding
#target the pair of move in self that are the oldest
sorted_moves = sorted(self, key=lambda a: a.date)
debit = credit = False
for aml in sorted_moves:
if credit != False and debit != False:
if credit and debit:
break
if aml[field] > 0 and debit == False:
if float_compare(aml[field], 0, precision_rounding=rounding) == 1 and not debit:
debit = aml
elif aml[field] < 0 and credit == False:
elif float_compare(aml[field], 0, precision_rounding=rounding) == -1 and not credit:
credit = aml
return debit, credit
@@ -746,12 +759,18 @@ class AccountMoveLine(models.Model):
"""
if not self.ids:
return self
field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
sm_debit_move, sm_credit_move = self._get_pair_to_reconcile()
#there is no more pair to reconcile so return what move_line are left
if not sm_credit_move or not sm_debit_move:
return self
field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
if not sm_debit_move.debit and not sm_debit_move.credit:
#both debit and credit field are 0, consider the amount_residual_currency field because it's an exchange difference entry
field = 'amount_residual_currency'
if self[0].currency_id and all([x.currency_id == self[0].currency_id for x in self]):
#all the lines have the same currency, so we consider the amount_residual_currency field
field = 'amount_residual_currency'
#Reconcile the pair together
amount_reconcile = min(sm_debit_move[field], -sm_credit_move[field])
#Remove from recordset the one(s) that will be totally reconciled
@@ -1022,7 +1041,11 @@ class AccountMoveLine(models.Model):
raise UserError(_('You cannot change the tax, you should remove and recreate lines.'))
if ('account_id' in vals) and self.env['account.account'].browse(vals['account_id']).deprecated:
raise UserError(_('You cannot use deprecated account.'))
if any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit', 'amount_currency', 'currency_id')):
if any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit')):
self._update_check()
if not self._context.get('allow_amount_currency') and any(key in vals for key in ('amount_currency', 'currency_id')):
#hackish workaround to write the amount_currency when assigning a payment to an invoice through the 'add' button
#this is needed to compute the correct amount_residual_currency and potentially create an exchange difference entry
self._update_check()
#when we set the expected payment date, log a note on the invoice_id related (if any)
if vals.get('expected_pay_date') and self.invoice_id:
@@ -1074,15 +1097,20 @@ class AccountMoveLine(models.Model):
return result
@api.model
def compute_amount_fields(self, amount, src_currency, company_currency):
def compute_amount_fields(self, amount, src_currency, company_currency, invoice_currency=False):
""" Helper function to compute value for fields debit/credit/amount_currency based on an amount and the currencies given in parameter"""
amount_currency = False
currency_id = False
if src_currency and src_currency != company_currency:
amount_currency = amount
amount = src_currency.with_context(self._context).compute(amount, company_currency)
currency_id = src_currency.id
debit = amount > 0 and amount or 0.0
credit = amount < 0 and -amount or 0.0
return debit, credit, amount_currency
if invoice_currency and invoice_currency != company_currency and not amount_currency:
amount_currency = src_currency.with_context(self._context).compute(amount, invoice_currency)
currency_id = invoice_currency.id
return debit, credit, amount_currency, currency_id
@api.multi
def create_analytic_lines(self):
@@ -1184,69 +1212,88 @@ class AccountPartialReconcile(models.Model):
help='Utility field to express amount currency')
company_id = fields.Many2one('res.company', related='debit_move_id.company_id', store=True, string='Currency')
def create_exchange_rate_entry(self):
""" Automatically create a journal entry to book the exchange rate difference between the `debit_move_id`
and the `credit_move_id`, if both share the same currency, and at the prorata of the amount matched
between them.
def create_exchange_rate_entry(self, aml, amount_diff, diff_in_currency, currency, move_date):
""" Automatically create a journal entry to book the exchange rate difference.
That new journal entry is made in the company `currency_exchange_journal_id` and one of its journal
items is matched with the other lines to ensure having the residual amount in company currency that
reflects perfectly the residual amount in foreign currency.
items is matched with the other lines to balance the full reconciliation.
"""
for rec in self:
if rec.currency_id and rec.debit_move_id.amount_currency and rec.credit_move_id.amount_currency:
#create exchange rate difference journal entry
rate_diff = rec.debit_move_id.debit / rec.debit_move_id.amount_currency - rec.credit_move_id.credit / -rec.credit_move_id.amount_currency
if rec.amount_currency and rec.company_id.currency_id.round(rec.amount_currency * rate_diff):
if not rec.company_id.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 self.company_id.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 self.company_id.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."))
amount_diff = rec.company_id.currency_id.round(rec.amount_currency * rate_diff)
move_vals = {'journal_id': rec.company_id.currency_exchange_journal_id.id, 'rate_diff_partial_rec_id': rec.id}
if not rec.company_id.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 self.company_id.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 self.company_id.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."))
move_vals = {'journal_id': rec.company_id.currency_exchange_journal_id.id, 'rate_diff_partial_rec_id': rec.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.
move_date = max(rec.debit_move_id.date, rec.credit_move_id.date)
if move_date > rec.company_id.fiscalyear_lock_date:
move_vals['date'] = move_date
move = rec.env['account.move'].create(move_vals)
line_to_reconcile = rec.env['account.move.line'].with_context(check_move_validity=False).create({
'name': _('Currency exchange rate difference'),
'debit': amount_diff < 0 and -amount_diff or 0.0,
'credit': amount_diff > 0 and amount_diff or 0.0,
'account_id': rec.debit_move_id.account_id.id,
'move_id': move.id,
'currency_id': rec.currency_id.id,
'amount_currency': 0.0,
'partner_id': rec.debit_move_id.partner_id.id,
})
rec.env['account.move.line'].create({
'name': _('Currency exchange rate difference'),
'debit': amount_diff > 0 and amount_diff or 0.0,
'credit': amount_diff < 0 and -amount_diff or 0.0,
'account_id': amount_diff > 0 and rec.company_id.currency_exchange_journal_id.default_debit_account_id.id or rec.company_id.currency_exchange_journal_id.default_credit_account_id.id,
'move_id': move.id,
'currency_id': rec.currency_id.id,
'partner_id': rec.debit_move_id.partner_id.id,
})
rec.env['account.partial.reconcile'].create({
'debit_move_id': amount_diff < 0 and line_to_reconcile.id or rec.debit_move_id.id,
'credit_move_id': amount_diff > 0 and line_to_reconcile.id or rec.credit_move_id.id,
'amount': abs(amount_diff),
'amount_currency': 0.0,
'currency_id': rec.debit_move_id.currency_id.id,
})
move.post()
# 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 > rec.company_id.fiscalyear_lock_date:
move_vals['date'] = move_date
move = rec.env['account.move'].create(move_vals)
line_to_reconcile = rec.env['account.move.line'].with_context(check_move_validity=False).create({
'name': _('Currency exchange rate difference'),
'debit': amount_diff < 0 and -amount_diff or 0.0,
'credit': amount_diff > 0 and amount_diff or 0.0,
'account_id': rec.debit_move_id.account_id.id,
'move_id': move.id,
'currency_id': currency.id,
'amount_currency': -diff_in_currency,
'partner_id': rec.debit_move_id.partner_id.id,
})
rec.env['account.move.line'].create({
'name': _('Currency exchange rate difference'),
'debit': amount_diff > 0 and amount_diff or 0.0,
'credit': amount_diff < 0 and -amount_diff or 0.0,
'account_id': amount_diff > 0 and rec.company_id.currency_exchange_journal_id.default_debit_account_id.id or rec.company_id.currency_exchange_journal_id.default_credit_account_id.id,
'move_id': move.id,
'currency_id': currency.id,
'amount_currency': diff_in_currency,
'partner_id': rec.debit_move_id.partner_id.id,
})
partial_rec = rec.env['account.partial.reconcile'].create({
'debit_move_id': aml.credit and line_to_reconcile.id or aml.id,
'credit_move_id': aml.debit and line_to_reconcile.id or aml.id,
'amount': abs(amount_diff),
'amount_currency': abs(diff_in_currency),
'currency_id': currency.id,
})
move.post()
return line_to_reconcile.id, partial_rec.id
@api.model
def create(self, vals):
res = super(AccountPartialReconcile, self).create(vals)
#eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates
res.create_exchange_rate_entry()
#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']
total_debit = 0
total_credit = 0
total_amount_currency = 0
currency = None
maxdate = None
aml_to_balance = None
for partial_rec in partial_rec_set:
if currency is None:
currency = partial_rec.currency_id
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
maxdate = max(aml.date, maxdate)
total_debit += aml.debit
total_credit += aml.credit
aml_set |= aml
if aml.currency_id and aml.currency_id == currency:
total_amount_currency += aml.amount_currency
for x in aml.matched_debit_ids | aml.matched_credit_ids:
partial_rec_set[x] = None
#then, if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full
if currency and aml_to_balance and (float_is_zero(total_amount_currency, precision_rounding=currency.rounding) or float_compare(total_debit, total_credit, precision_rounding=currency.rounding) == 0):
#eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates
aml_id, partial_rec_id = partial_rec.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, maxdate)
return res
@api.multi
@@ -1260,6 +1307,31 @@ class AccountPartialReconcile(models.Model):
# we must unlink the reconciliation with the exchange rate difference entry as well, to be able to reconcile it with its reversal entry
to_unlink = self
exchange_rate_entries = self.env['account.move'].search([('rate_diff_partial_rec_id', 'in', self.ids)])
for rec in self:
partial_rec_set = OrderedDict.fromkeys([rec])
aml_set = self.env['account.move.line']
total_debit = 0
total_credit = 0
total_amount_currency = 0
currency = None
for partial_rec in partial_rec_set:
if currency is None:
currency = partial_rec.currency_id
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
if aml not in aml_set:
total_debit += aml.debit
total_credit += aml.credit
aml_set |= aml
if aml.currency_id and aml.currency_id == currency:
total_amount_currency += aml.amount_currency
for x in aml.matched_debit_ids | aml.matched_credit_ids:
partial_rec_set[x] = None
digits_rounding_precision = aml_set[0].company_id.currency_id.rounding
if float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0 \
or (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)):
#if the reconciliation is full, also unlink any currency rate diffence entry created
exchange_rate_entries = self.env['account.move'].search([('rate_diff_partial_rec_id', 'in', [x.id for x in partial_rec_set.keys()])])
# revert the currency difference entry
reversed_moves = exchange_rate_entries.reverse_moves()
# find the origin currency difference line on the partner account and its newly created reversal, and store them in a list
@@ -1275,6 +1347,8 @@ class AccountPartialReconcile(models.Model):
to_unlink |= origin_line.matched_debit_ids | origin_line.matched_credit_ids
to_rec = origin_line + acm_line
pairs_to_rec.append(to_rec)
#make sure that the exchange_rate_entries aren't linked to any partial reconciliation anymore
exchange_rate_entries.write({'rate_diff_partial_rec_id': False})
# the call to super() had to be delayed in order to mark the move lines to reconcile together (to use 'rate_diff_partial_rec_id')
res = super(AccountPartialReconcile, to_unlink).unlink()
# now that the origin currency difference line is not reconciled anymore, we can reconcile it with its reversal entry to cancel it completly
+26 -8
View File
@@ -353,23 +353,41 @@ class account_payment(models.Model):
Return the journal entry.
"""
aml_obj = self.env['account.move.line'].with_context(check_move_validity=False)
debit, credit, amount_currency = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id)
invoice_currency = False
if self.invoice_ids and all([x.currency_id == self.invoice_ids[0].currency_id for x in self.invoice_ids]):
#if all the invoices selected share the same currency, record the paiement in that currency too
invoice_currency = self.invoice_ids[0].currency_id
debit, credit, amount_currency, currency_id = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id, invoice_currency)
move = self.env['account.move'].create(self._get_move_vals())
#Write line corresponding to invoice payment
counterpart_aml_dict = self._get_shared_move_line_vals(debit, credit, amount_currency, move.id, False)
counterpart_aml_dict.update(self._get_counterpart_move_line_vals(self.invoice_ids))
counterpart_aml_dict.update({'currency_id': self.currency_id != self.company_id.currency_id and self.currency_id.id or False})
counterpart_aml_dict.update({'currency_id': currency_id})
counterpart_aml = aml_obj.create(counterpart_aml_dict)
#Reconcile with the invoices
if self.payment_difference_handling == 'reconcile':
self.invoice_ids.register_payment(counterpart_aml, self.writeoff_account_id, self.journal_id)
else:
self.invoice_ids.register_payment(counterpart_aml)
if self.payment_difference_handling == 'reconcile' and self.payment_difference:
writeoff_line = self._get_shared_move_line_vals(0, 0, 0, move.id, False)
debit_wo, credit_wo, amount_currency_wo, currency_id = aml_obj.with_context(date=self.payment_date).compute_amount_fields(self.payment_difference, self.currency_id, self.company_id.currency_id, invoice_currency)
writeoff_line['name'] = _('Write-Off')
writeoff_line['account_id'] = self.writeoff_account_id.id
writeoff_line['debit'] = debit_wo
writeoff_line['credit'] = credit_wo
writeoff_line['amount_currency'] = amount_currency_wo
writeoff_line['currency_id'] = currency_id
writeoff_line = aml_obj.create(writeoff_line)
if counterpart_aml['debit']:
counterpart_aml['debit'] += credit_wo - debit_wo
if counterpart_aml['credit']:
counterpart_aml['credit'] += debit_wo - credit_wo
counterpart_aml['amount_currency'] -= amount_currency_wo
self.invoice_ids.register_payment(counterpart_aml)
#Write counterpart lines
if not self.currency_id != self.company_id.currency_id:
amount_currency = 0
liquidity_aml_dict = self._get_shared_move_line_vals(credit, debit, -amount_currency, move.id, False)
liquidity_aml_dict.update(self._get_liquidity_move_line_vals(-amount))
aml_obj.create(liquidity_aml_dict)
@@ -381,7 +399,7 @@ class account_payment(models.Model):
""" Create the journal entry corresponding to the 'incoming money' part of an internal transfer, return the reconciliable move line
"""
aml_obj = self.env['account.move.line'].with_context(check_move_validity=False)
debit, credit, amount_currency = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id)
debit, credit, amount_currency, dummy = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id)
amount_currency = self.destination_journal_id.currency_id and self.currency_id.with_context(date=self.payment_date).compute(amount, self.destination_journal_id.currency_id) or 0
dst_move = self.env['account.move'].create(self._get_move_vals(self.destination_journal_id))
@@ -482,7 +500,7 @@ class account_payment(models.Model):
# If the journal has a currency specified, the journal item need to be expressed in this currency
if self.journal_id.currency_id and self.currency_id != self.journal_id.currency_id:
amount = self.currency_id.with_context(date=self.payment_date).compute(amount, self.journal_id.currency_id)
debit, credit, amount_currency = self.env['account.move.line'].with_context(date=self.payment_date).compute_amount_fields(amount, self.journal_id.currency_id, self.company_id.currency_id)
debit, credit, amount_currency, dummy = self.env['account.move.line'].with_context(date=self.payment_date).compute_amount_fields(amount, self.journal_id.currency_id, self.company_id.currency_id)
vals.update({
'amount_currency': amount_currency,
'currency_id': self.journal_id.currency_id.id,
+61 -16
View File
@@ -37,7 +37,7 @@ class TestReconciliation(AccountingTestCase):
self.diff_income_account = self.env['res.users'].browse(self.env.uid).company_id.income_currency_exchange_account_id
self.diff_expense_account = self.env['res.users'].browse(self.env.uid).company_id.expense_currency_exchange_account_id
def create_invoice(self, type='out_invoice', currency_id=None):
def create_invoice(self, type='out_invoice', invoice_amount=50, currency_id=None):
#we create an invoice in given currency
invoice = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
'reference_type': 'none',
@@ -49,9 +49,9 @@ class TestReconciliation(AccountingTestCase):
})
self.account_invoice_line_model.create({'product_id': self.product.id,
'quantity': 1,
'price_unit': 100,
'price_unit': invoice_amount,
'invoice_id': invoice.id,
'name': 'product that cost 100',
'name': 'product that cost ' + str(invoice_amount),
'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id,
})
@@ -95,7 +95,7 @@ class TestReconciliation(AccountingTestCase):
self.assertEquals(round(move_line.credit, 2), aml_dict[move_line.account_id.id]['credit'])
self.assertEquals(round(move_line.amount_currency, 2), aml_dict[move_line.account_id.id]['amount_currency'])
self.assertEquals(move_line.currency_id.id, aml_dict[move_line.account_id.id]['currency_id'])
if aml_dict[move_line.account_id.id].get('currency_diff'):
if 'currency_diff' in aml_dict[move_line.account_id.id]:
if move_line.credit:
rec_ids = [r.id for r in move_line.matched_debit_ids]
else:
@@ -103,37 +103,38 @@ class TestReconciliation(AccountingTestCase):
currency_diff_move = self.env['account.move'].search([('rate_diff_partial_rec_id', 'in', rec_ids)])
self.assertEqual(len(currency_diff_move), 1)
for currency_diff_line in currency_diff_move[0].line_ids:
if aml_dict[move_line.account_id.id].get('currency_diff') == 0:
if currency_diff_line.account_id.id == move_line.account_id.id:
self.assertAlmostEquals(currency_diff_line.amount_currency, aml_dict[move_line.account_id.id].get('amount_currency_diff'))
if aml_dict[move_line.account_id.id].get('currency_diff') > 0:
if currency_diff_line.account_id.id == move_line.account_id.id:
self.assertAlmostEquals(currency_diff_line.debit, aml_dict[move_line.account_id.id].get('currency_diff'))
else:
self.assertAlmostEquals(currency_diff_line.credit, aml_dict[move_line.account_id.id].get('currency_diff'))
self.assertIn(currency_diff_line.account_id.id, [self.diff_expense_account.id, self.diff_income_account.id], 'The difference accounts should be used correctly. ')
#self.assertEquals(currency_diff_line.account_id.id, self.account_rsa.id)
else:
if currency_diff_line.account_id.id == move_line.account_id.id:
self.assertAlmostEquals(currency_diff_line.credit, abs(aml_dict[move_line.account_id.id].get('currency_diff')))
else:
self.assertAlmostEquals(currency_diff_line.debit, abs(aml_dict[move_line.account_id.id].get('currency_diff')))
self.assertIn(currency_diff_line.account_id.id, [self.diff_expense_account.id, self.diff_income_account.id], 'The difference accounts should be used correctly. ')
#self.assertEquals(currency_diff_line.account_id.id, self.account_rsa.id)
def make_customer_and_supplier_flows(self, invoice_currency_id, bank_journal, amount, amount_currency, transaction_currency_id):
def make_customer_and_supplier_flows(self, invoice_currency_id, invoice_amount, bank_journal, amount, amount_currency, transaction_currency_id):
#we create an invoice in given invoice_currency
invoice_record = self.create_invoice(type='out_invoice', currency_id=invoice_currency_id)
invoice_record = self.create_invoice(type='out_invoice', invoice_amount=invoice_amount, currency_id=invoice_currency_id)
#we encode a payment on it, on the given bank_journal with amount, amount_currency and transaction_currency given
bank_stmt = self.make_payment(invoice_record, bank_journal, amount=amount, amount_currency=amount_currency, currency_id=transaction_currency_id)
customer_move_lines = bank_stmt.move_line_ids
#we create a supplier bill in given invoice_currency
invoice_record = self.create_invoice(type='in_invoice', currency_id=invoice_currency_id)
invoice_record = self.create_invoice(type='in_invoice', invoice_amount=invoice_amount, currency_id=invoice_currency_id)
#we encode a payment on it, on the given bank_journal with amount, amount_currency and transaction_currency given
bank_stmt = self.make_payment(invoice_record, bank_journal, amount=-amount, amount_currency=-amount_currency, currency_id=transaction_currency_id)
supplier_move_lines = bank_stmt.move_line_ids
return customer_move_lines, supplier_move_lines
def test_statement_usd_invoice_eur_transaction_eur(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_euro_id, self.bank_journal_usd, 42, 30, self.currency_euro_id)
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_euro_id, 30, self.bank_journal_usd, 42, 30, self.currency_euro_id)
self.check_results(customer_move_lines, {
self.account_usd.id: {'debit': 30.0, 'credit': 0.0, 'amount_currency': 42, 'currency_id': self.currency_usd_id},
self.account_rcv.id: {'debit': 0.0, 'credit': 30.0, 'amount_currency': -42, 'currency_id': self.currency_usd_id},
@@ -144,7 +145,7 @@ class TestReconciliation(AccountingTestCase):
})
def test_statement_usd_invoice_usd_transaction_usd(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, self.bank_journal_usd, 50, 0, False)
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_usd, 50, 0, False)
self.check_results(customer_move_lines, {
self.account_usd.id: {'debit': 32.70, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id},
self.account_rcv.id: {'debit': 0.0, 'credit': 32.70, 'amount_currency': -50, 'currency_id': self.currency_usd_id},
@@ -155,7 +156,7 @@ class TestReconciliation(AccountingTestCase):
})
def test_statement_usd_invoice_usd_transaction_eur(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, self.bank_journal_usd, 50, 40, self.currency_euro_id)
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_usd, 50, 40, self.currency_euro_id)
self.check_results(customer_move_lines, {
self.account_usd.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id},
self.account_rcv.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': -50, 'currency_id': self.currency_usd_id, 'currency_diff': 7.30},
@@ -166,7 +167,7 @@ class TestReconciliation(AccountingTestCase):
})
def test_statement_usd_invoice_chf_transaction_chf(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_swiss_id, self.bank_journal_usd, 42, 50, self.currency_swiss_id)
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_swiss_id, 50, self.bank_journal_usd, 42, 50, self.currency_swiss_id)
self.check_results(customer_move_lines, {
self.account_usd.id: {'debit': 27.47, 'credit': 0.0, 'amount_currency': 42, 'currency_id': self.currency_usd_id},
self.account_rcv.id: {'debit': 0.0, 'credit': 27.47, 'amount_currency': -50, 'currency_id': self.currency_swiss_id, 'currency_diff': -10.74},
@@ -177,7 +178,7 @@ class TestReconciliation(AccountingTestCase):
})
def test_statement_eur_invoice_usd_transaction_usd(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, self.bank_journal_euro, 40, 50, self.currency_usd_id)
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_euro, 40, 50, self.currency_usd_id)
self.check_results(customer_move_lines, {
self.account_euro.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id},
self.account_rcv.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': -50, 'currency_id': self.currency_usd_id, 'currency_diff': 7.30},
@@ -188,7 +189,7 @@ class TestReconciliation(AccountingTestCase):
})
def test_statement_eur_invoice_usd_transaction_eur(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, self.bank_journal_euro, 40, 0.0, False)
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_euro, 40, 0.0, False)
self.check_results(customer_move_lines, {
self.account_euro.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
self.account_rcv.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': -61.16, 'currency_id': self.currency_usd_id},
@@ -199,7 +200,7 @@ class TestReconciliation(AccountingTestCase):
})
def test_statement_euro_invoice_usd_transaction_chf(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, self.bank_journal_euro, 42, 50, self.currency_swiss_id)
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_euro, 42, 50, self.currency_swiss_id)
self.check_results(customer_move_lines, {
self.account_euro.id: {'debit': 42.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_swiss_id},
self.account_rcv.id: {'debit': 0.0, 'credit': 42.0, 'amount_currency': -50, 'currency_id': self.currency_swiss_id},
@@ -209,6 +210,50 @@ class TestReconciliation(AccountingTestCase):
self.account_rcv.id: {'debit': 42.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_swiss_id},
})
def test_statement_euro_invoice_usd_transaction_euro_full(self):
#we create an invoice in given invoice_currency
invoice_record = self.create_invoice(type='out_invoice', invoice_amount=50, currency_id=self.currency_usd_id)
#we encode a payment on it, on the given bank_journal with amount, amount_currency and transaction_currency given
bank_stmt = self.acc_bank_stmt_model.create({
'journal_id': self.bank_journal_euro.id,
'date': time.strftime('%Y') + '-01-01',
})
bank_stmt_line = self.acc_bank_stmt_line_model.create({'name': 'payment',
'statement_id': bank_stmt.id,
'partner_id': self.partner_agrolait_id,
'amount': 40,
'date': time.strftime('%Y') + '-01-01',})
#reconcile the payment with the invoice
for l in invoice_record.move_id.line_ids:
if l.account_id.id == self.account_rcv.id:
line_id = l
break
bank_stmt_line.process_reconciliation(counterpart_aml_dicts=[{
'move_line': line_id,
'debit': 0.0,
'credit': 32.7,
'name': line_id.name,
}], new_aml_dicts=[{
'debit': 0.0,
'credit': 7.3,
'name': 'exchange difference',
'account_id': self.diff_income_account.id
}])
self.check_results(bank_stmt.move_line_ids, {
self.account_euro.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 0, 'currency_id': False},
self.account_rcv.id: {'debit': 0.0, 'credit': 32.7, 'amount_currency': -41.97, 'currency_id': self.currency_usd_id, 'currency_diff': 0, 'amount_currency_diff': -8.03},
self.diff_income_account.id: {'debit': 0.0, 'credit': 7.3, 'amount_currency': 0, 'currency_id': False},
})
# The invoice should be paid, as the payments totally cover its total
self.assertEquals(invoice_record.state, 'paid', 'The invoice should be paid by now')
invoice_rec_line = invoice_record.move_id.line_ids.filtered(lambda x: x.account_id.reconcile)
self.assertTrue(invoice_rec_line.reconciled, 'The invoice should be totally reconciled')
self.assertEquals(invoice_rec_line.amount_residual, 0, 'The invoice should be totally reconciled')
self.assertEquals(invoice_rec_line.amount_residual_currency, 0, 'The invoice should be totally reconciled')
@unittest.skip('adapt to new accounting')
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.
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import OrderedDict
from openerp.tools import float_compare
from openerp.tools import float_compare, float_is_zero
from openerp import api, fields, models
@@ -25,19 +25,27 @@ class AccountPartialReconcile(models.Model):
aml_set = self.env['account.move.line']
total_debit = 0
total_credit = 0
total_amount_currency = 0
currency = None
for partial_rec in partial_rec_set:
if currency is None:
currency = partial_rec.currency_id
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
if aml not in aml_set:
total_debit += aml.debit
total_credit += aml.credit
aml_set |= aml
if aml.currency_id and aml.currency_id == currency:
total_amount_currency += aml.amount_currency
for x in aml.matched_debit_ids | aml.matched_credit_ids:
partial_rec_set[x] = None
partial_rec_ids = [x.id for x in partial_rec_set.keys()]
aml_ids = [x.id for x in aml_set]
#then, if the total debit and credit are equal, the reconciliation is full
#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 float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0:
if float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0 \
or (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)):
#in that case, mark the reference on the partial reconciliations and the entries
self.env['account.full.reconcile'].with_context(check_move_validity=False).create({
'partial_reconcile_ids': [(6, 0, partial_rec_ids)],
@@ -47,11 +55,9 @@ class AccountPartialReconcile(models.Model):
@api.multi
def unlink(self):
"""When removing a partial reconciliation, also unlink its full reconciliation if it exists"""
full_rec = self.env['account.full.reconcile']
for rec in self:
if rec.full_reconcile_id:
full_rec |= rec.full_reconcile_id
full_rec.unlink()
rec.full_reconcile_id.unlink()
return super(AccountPartialReconcile, self).unlink()