[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-23 09:57:59 +01:00
parent 2f8c99d412
commit d8137d35b7
5 changed files with 270 additions and 163 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):
+156 -107
View File
@@ -9,7 +9,6 @@ from openerp.tools.misc import formatLang
from openerp.tools import float_is_zero, float_compare
from openerp.tools.safe_eval import safe_eval
#----------------------------------------------------------
# Entries
#----------------------------------------------------------
@@ -75,7 +74,6 @@ class AccountMove(models.Model):
date = fields.Date(required=True, states={'posted': [('readonly', True)]}, index=True, default=fields.Date.context_today)
journal_id = fields.Many2one('account.journal', string='Journal', required=True, states={'posted': [('readonly', True)]}, default=_get_default_journal)
currency_id = fields.Many2one('res.currency', compute='_compute_currency', store=True, string="Currency")
rate_diff_partial_rec_id = fields.Many2one('account.partial.reconcile', string='Exchange Rate Entry of', help="Technical field used to keep track of the origin of journal entries created in case of fluctuation of the currency exchange rate. This is needed when cancelling the source: it will post the inverse journal entry to cancel that part too.")
state = fields.Selection([('draft', 'Unposted'), ('posted', 'Posted')], string='Status',
required=True, readonly=True, copy=False, default='draft',
help='All manually created new journal entries are usually in the status \'Unposted\', '
@@ -240,6 +238,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:
@@ -261,8 +262,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)
@@ -729,15 +734,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
@@ -748,12 +758,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
@@ -1023,7 +1039,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:
@@ -1075,15 +1095,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):
@@ -1186,132 +1211,125 @@ class AccountPartialReconcile(models.Model):
company_id = fields.Many2one('res.company', related='debit_move_id.company_id', store=True, string='Currency')
full_reconcile_id = fields.Many2one('account.full.reconcile', string="Full Reconcile")
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}
# 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, partial_rec
@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
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
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 float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0:
#in that case, mark the reference on the partial reconciliations and the entries
if float_is_zero(total_amount_currency, precision_rounding=digits_rounding_precision) or float_compare(total_debit, total_credit, precision_rounding=currency.rounding) == 0:
exchange_move_id = False
exchange_partial_rec_id = False
if currency and aml_to_balance:
#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
#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': [(6, 0, partial_rec_ids)],
'reconciled_line_ids': [(6, 0, aml_ids)]})
'reconciled_line_ids': [(6, 0, aml_ids)],
'exchange_move_id': exchange_move_id,
'exchange_partial_rec_id': exchange_partial_rec_id,
})
return res
@api.multi
def unlink(self):
""" When removing a link between entries, we need to revert the eventual journal entries we created to book the
fluctuation of the foreign currency's exchange rate.
We need also to reconcile together the origin currency difference line and its reversal in order to completly
cancel the currency difference entry on the partner account (otherwise it will still appear on the aged balance
for example).
When removing a partial reconciliation, also unlink its full reconciliation if it exists
"""
#remove full reconciliation
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()
# revert the currency difference entry
exchange_rate_entries = self.env['account.move'].search([('rate_diff_partial_rec_id', 'in', self.ids)])
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
pairs_to_rec = []
for rev_move in self.env['account.move'].browse(reversed_moves):
if not rev_move.rate_diff_partial_rec_id:
continue
origin_move = exchange_rate_entries.filtered(lambda x: x.rate_diff_partial_rec_id == rev_move.rate_diff_partial_rec_id)
for acm_line in rev_move.line_ids:
if acm_line.account_id.reconcile:
for origin_line in origin_move.line_ids:
if origin_line.account_id == acm_line.account_id and origin_line.debit == acm_line.credit and origin_line.credit == acm_line.debit:
to_rec = origin_line + acm_line
pairs_to_rec.append(to_rec)
# 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, self).unlink()
# now that the origin currency difference line is not reconciled anymore, we can reconcile it with its reversal entry to cancel it completly
for to_rec in pairs_to_rec:
to_rec.reconcile()
return res
""" When removing a partial reconciliation, also unlink its full reconciliation if it exists """
to_unlink = self
if self._context.get('full_rec_lookup', True):
full_to_unlink = self.env['account.full.reconcile']
for rec in self:
#exclude partial reconciliations related to an exchange rate entry, because the unlink of the full reconciliation will already do it
if self.env['account.full.reconcile'].search([('exchange_partial_rec_id', '=', rec.id)]):
to_unlink = to_unlink - rec
#without the deleted partial reconciliations, the full reconciliation won't be full anymore
if rec.full_reconcile_id:
full_to_unlink |= rec.full_reconcile_id
if full_to_unlink:
full_to_unlink.unlink()
if to_unlink:
return super(AccountPartialReconcile, to_unlink).unlink()
return True
class AccountFullReconcile(models.Model):
_name = "account.full.reconcile"
@@ -1320,3 +1338,34 @@ class AccountFullReconcile(models.Model):
name = fields.Char(string='Number', required=True, copy=False, default=lambda self: self.env['ir.sequence'].next_by_code('account.reconcile'))
partial_reconcile_ids = fields.One2many('account.partial.reconcile', 'full_reconcile_id', string='Reconciliation Parts')
reconciled_line_ids = fields.One2many('account.move.line', 'full_reconcile_id', string='Matched Journal Items')
exchange_move_id = fields.Many2one('account.move')
exchange_partial_rec_id = fields.Many2one('account.partial.reconcile')
@api.multi
def unlink(self):
""" When removing a full reconciliation, we need to revert the eventual journal entries we created to book the
fluctuation of the foreign currency's exchange rate.
We need also to reconcile together the origin currency difference line and its reversal in order to completly
cancel the currency difference entry on the partner account (otherwise it will still appear on the aged balance
for example).
"""
for rec in self:
if not rec.exchange_move_id or not rec.exchange_partial_rec_id:
continue
#reverse the exchange rate entry
reversed_move_id = rec.exchange_move_id.reverse_moves()[0]
reversed_move = self.env['account.move'].browse(reversed_move_id)
#search the original line and its newly created reversal
for aml in reversed_move.line_ids:
if aml.account_id.reconcile:
break
if aml:
if aml.debit:
pair_to_rec = aml | rec.exchange_partial_rec_id.credit_move_id
else:
pair_to_rec = aml | rec.exchange_partial_rec_id.debit_move_id
#remove the partial reconciliation of the exchange rate entry as well
rec.exchange_partial_rec_id.with_context(full_rec_lookup=False).unlink()
#reconcile together the original exchange rate line and its reversal
pair_to_rec.reconcile()
return super(AccountFullReconcile, self).unlink()
+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,
+3 -5
View File
@@ -94,12 +94,10 @@ class TestPayment(AccountingTestCase):
self.assertEqual(len(aml_rec), 1, "Expected a move line with values : %s" % str(aml_dict))
if aml_dict.get('currency_diff'):
if aml_rec.credit:
rec_ids = [r.id for r in aml_rec.matched_debit_ids]
currency_diff_move = aml_rec.matched_debit_ids.full_reconcile_id.exchange_move_id
else:
rec_ids = [r.id for r in aml_rec.matched_credit_ids]
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:
currency_diff_move = aml_rec.matched_credit_ids.full_reconcile_id.exchange_move_id
for currency_diff_line in currency_diff_move.line_ids:
if aml_dict.get('currency_diff') > 0:
if currency_diff_line.account_id.id == aml_rec.account_id.id:
self.assertAlmostEquals(currency_diff_line.debit, aml_dict.get('currency_diff'))
+63 -23
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,45 +95,41 @@ 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 move_line.credit:
rec_ids = [r.id for r in move_line.matched_debit_ids]
else:
rec_ids = [r.id for r in move_line.matched_credit_ids]
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 'currency_diff' in aml_dict[move_line.account_id.id]:
currency_diff_move = move_line.full_reconcile_id.exchange_move_id
for currency_diff_line in currency_diff_move.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 +140,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 +151,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 +162,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 +173,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 +184,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 +195,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 +205,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.