[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:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user