[FIX] account: reconciliation of payments and invoices, diff currencies and times
Use case: - 2 invoices in foreign currencies, made at different times (and different exchange rates) - 2 payments (same characteristics as invoices) - allocate the 1st payment to inv A - allocate the 2nd payment to inv A and then B so that A is fully paid, but B is not - try to make the final payment on B to fully reconcile everything Results (all in the payable account) - let CB be the credit line of B - let D2 be the debit line of 2nd payment - let D3 be the debit line of the 3rd and final payment Before this commit, the exchange difference computation dailed because: - There were 2 amls that had an amount_residual and needed to be reconciled (CB and D2) - Also, there was a structural flaw in the code that did not allow for multiple lines to have a residual After this commit, we still make 1 move to log the exchange difference, but we make exchange difference lines for each line that need to be reconciled: CB and D2 OPW 751710 Commit in master c36f70f6726e0c7ccca1002f78e4d402e27cae3d (2017-07-20) Closes #18347 [FIX] account: testing partial and full reconciliation
This commit is contained in:
@@ -1409,17 +1409,60 @@ class AccountPartialReconcile(models.Model):
|
||||
move.post()
|
||||
return line_to_reconcile, partial_rec
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
res = super(AccountPartialReconcile, self).create(vals)
|
||||
# Do not forwardport in master as of 2017-07-20
|
||||
def _fix_multiple_exchange_rates_diff(self, amls_to_fix, amount_diff, diff_in_currency, currency, move):
|
||||
self.ensure_one()
|
||||
move_lines = self.env['account.move.line'].with_context(check_move_validity=False)
|
||||
partial_reconciles = self.with_context(skip_full_reconcile_check=True)
|
||||
amount_diff = self.company_id.currency_id.round(amount_diff)
|
||||
diff_in_currency = currency.round(diff_in_currency)
|
||||
|
||||
for aml in amls_to_fix:
|
||||
account_payable_line = move_lines.create({
|
||||
'name': _('Currency exchange rate difference'),
|
||||
'debit': amount_diff < 0 and -aml.amount_residual or 0.0,
|
||||
'credit': amount_diff > 0 and aml.amount_residual or 0.0,
|
||||
'account_id': self.debit_move_id.account_id.id,
|
||||
'move_id': move.id,
|
||||
'currency_id': currency.id,
|
||||
'amount_currency': -aml.amount_residual_currency,
|
||||
'partner_id': self.debit_move_id.partner_id.id,
|
||||
})
|
||||
|
||||
move_lines.create({
|
||||
'name': _('Currency exchange rate difference'),
|
||||
'debit': amount_diff > 0 and aml.amount_residual or 0.0,
|
||||
'credit': amount_diff < 0 and -aml.amount_residual or 0.0,
|
||||
'account_id': amount_diff > 0 and self.company_id.currency_exchange_journal_id.default_debit_account_id.id or self.company_id.currency_exchange_journal_id.default_credit_account_id.id,
|
||||
'move_id': move.id,
|
||||
'currency_id': currency.id,
|
||||
'amount_currency': aml.amount_residual_currency,
|
||||
'partner_id': self.debit_move_id.partner_id.id})
|
||||
|
||||
partial_rec = super(AccountPartialReconcile, partial_reconciles).create({
|
||||
'debit_move_id': aml.credit and account_payable_line.id or aml.id,
|
||||
'credit_move_id': aml.debit and account_payable_line.id or aml.id,
|
||||
'amount': abs(aml.amount_residual),
|
||||
'amount_currency': abs(aml.amount_residual_currency),
|
||||
'currency_id': currency.id,
|
||||
})
|
||||
|
||||
move_lines |= account_payable_line
|
||||
partial_reconciles |= partial_rec
|
||||
|
||||
partial_reconciles._compute_partial_lines()
|
||||
return move_lines, partial_reconciles
|
||||
|
||||
# Do not forwardport in master as of 2017-07-20
|
||||
def _compute_partial_lines(self):
|
||||
if self._context.get('skip_full_reconcile_check'):
|
||||
#when running the manual reconciliation wizard, don't check the partials separately for full
|
||||
#reconciliation or exchange rate because it is handled manually after the whole processing
|
||||
return res
|
||||
return self
|
||||
#check if the reconcilation is full
|
||||
#first, gather all journal items involved in the reconciliation just created
|
||||
partial_rec_set = OrderedDict.fromkeys([x for x in res])
|
||||
aml_set = self.env['account.move.line']
|
||||
partial_rec_set = OrderedDict.fromkeys([x for x in self])
|
||||
aml_set = aml_to_balance = self.env['account.move.line']
|
||||
total_debit = 0
|
||||
total_credit = 0
|
||||
total_amount_currency = 0
|
||||
@@ -1427,7 +1470,7 @@ class AccountPartialReconcile(models.Model):
|
||||
#possible to compute the exchange difference entry and it has to be done manually.
|
||||
currency = list(partial_rec_set)[0].currency_id
|
||||
maxdate = None
|
||||
aml_to_balance = None
|
||||
|
||||
for partial_rec in partial_rec_set:
|
||||
if partial_rec.currency_id != currency:
|
||||
#no exchange rate entry will be created
|
||||
@@ -1435,7 +1478,7 @@ class AccountPartialReconcile(models.Model):
|
||||
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
|
||||
if aml not in aml_set:
|
||||
if aml.amount_residual or aml.amount_residual_currency:
|
||||
aml_to_balance = aml
|
||||
aml_to_balance |= aml
|
||||
maxdate = max(aml.date, maxdate)
|
||||
total_debit += aml.debit
|
||||
total_credit += aml.credit
|
||||
@@ -1453,16 +1496,21 @@ class AccountPartialReconcile(models.Model):
|
||||
aml_ids = aml_set.ids
|
||||
#then, if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full
|
||||
digits_rounding_precision = aml_set[0].company_id.currency_id.rounding
|
||||
|
||||
if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0:
|
||||
exchange_move_id = False
|
||||
exchange_partial_rec_id = False
|
||||
if currency and aml_to_balance:
|
||||
exchange_move = (self.env['account.move']
|
||||
.create(self.env['account.full.reconcile']
|
||||
._prepare_exchange_diff_move(move_date=maxdate, company=aml_to_balance[0].company_id)))
|
||||
#eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates
|
||||
rate_diff_aml, rate_diff_partial_rec = partial_rec.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, maxdate)
|
||||
aml_ids.append(rate_diff_aml.id)
|
||||
partial_rec_ids.append(rate_diff_partial_rec.id)
|
||||
exchange_move_id = rate_diff_aml.move_id.id
|
||||
exchange_partial_rec_id = rate_diff_partial_rec.id
|
||||
rate_diff_amls, rate_diff_partial_recs = partial_rec._fix_multiple_exchange_rates_diff(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, exchange_move)
|
||||
aml_ids += rate_diff_amls.ids
|
||||
partial_rec_ids += rate_diff_partial_recs.ids
|
||||
exchange_move.post()
|
||||
exchange_move_id = exchange_move.id
|
||||
exchange_partial_rec_id = rate_diff_partial_recs[-1:].id
|
||||
#mark the reference of the full reconciliation on the partial ones and on the entries
|
||||
self.env['account.full.reconcile'].with_context(check_move_validity=False).create({
|
||||
'partial_reconcile_ids': [(4, p_id) for p_id in partial_rec_ids],
|
||||
@@ -1470,6 +1518,12 @@ class AccountPartialReconcile(models.Model):
|
||||
'exchange_move_id': exchange_move_id,
|
||||
'exchange_partial_rec_id': exchange_partial_rec_id,
|
||||
})
|
||||
|
||||
# Do not forwardport in master as of 2017-07-20
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
res = super(AccountPartialReconcile, self).create(vals)
|
||||
res._compute_partial_lines()
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
@@ -1531,3 +1585,20 @@ class AccountFullReconcile(models.Model):
|
||||
#reconcile together the original exchange rate line and its reversal
|
||||
pair_to_rec.reconcile()
|
||||
return super(AccountFullReconcile, self).unlink()
|
||||
|
||||
# Do not forwardport in master as of 2017-07-20
|
||||
@api.model
|
||||
def _prepare_exchange_diff_move(self, move_date, company):
|
||||
if not company.currency_exchange_journal_id:
|
||||
raise UserError(_("You should configure the 'Exchange Rate Journal' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
|
||||
if not company.income_currency_exchange_account_id.id:
|
||||
raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
|
||||
if not company.expense_currency_exchange_account_id.id:
|
||||
raise UserError(_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
|
||||
res = {'journal_id': company.currency_exchange_journal_id.id}
|
||||
# The move date should be the maximum date between payment and invoice
|
||||
# (in case of payment in advance). However, we should make sure the
|
||||
# move date is not recorded after the end of year closing.
|
||||
if move_date > company.fiscalyear_lock_date:
|
||||
res['date'] = move_date
|
||||
return res
|
||||
|
||||
@@ -473,4 +473,148 @@ class TestReconciliation(AccountingTestCase):
|
||||
self.assertEquals(round(aml.debit, 2), line['debit'])
|
||||
self.assertEquals(round(aml.credit, 2), line['credit'])
|
||||
self.assertEquals(round(aml.amount_currency, 2), line['amount_currency'])
|
||||
self.assertEquals(aml.currency_id.id, line['currency_id'])
|
||||
self.assertEquals(aml.currency_id.id, line['currency_id'])
|
||||
|
||||
def test_partial_reconcile_currencies(self):
|
||||
# client Account (payable, rsa)
|
||||
# Debit Credit
|
||||
# --------------------------------------------------------
|
||||
# Pay a : 25/0.5 = 50 | Inv a : 50/0.5 = 100
|
||||
# Pay b: 50/0.75 = 66.66 | Inv b : 50/0.75 = 66.66
|
||||
# Pay c: 25/0.8 = 31.25 |
|
||||
#
|
||||
# Debit_currency = 100 | Credit currency = 100
|
||||
# Debit = 147.91 | Credit = 166.66
|
||||
# Balance Debit = 18.75
|
||||
# Counterpart Credit goes in Exchange diff
|
||||
|
||||
dest_journal_id = self.env['account.journal'].search([('type', '=', 'purchase'), ('company_id', '=', self.env.ref('base.main_company').id)], limit=1)
|
||||
account_expenses = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1)
|
||||
|
||||
self.bank_journal_euro.write({'default_debit_account_id': self.account_rsa.id,
|
||||
'default_credit_account_id': self.account_rsa.id})
|
||||
dest_journal_id.write({'default_debit_account_id': self.account_rsa.id,
|
||||
'default_credit_account_id': self.account_rsa.id})
|
||||
# Setting up rates for USD (main_company is in EUR)
|
||||
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '07' + '-01',
|
||||
'rate': 0.5,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'company_id': self.env.ref('base.main_company').id})
|
||||
|
||||
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '08' + '-01',
|
||||
'rate': 0.75,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'company_id': self.env.ref('base.main_company').id})
|
||||
|
||||
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '09' + '-01',
|
||||
'rate': 0.80,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'company_id': self.env.ref('base.main_company').id})
|
||||
|
||||
# Preparing Invoices (from vendor)
|
||||
invoice_a = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
|
||||
'reference_type': 'none',
|
||||
'currency_id': self.currency_usd_id,
|
||||
'name': 'invoice to vendor',
|
||||
'account_id': self.account_rsa.id,
|
||||
'type': 'in_invoice',
|
||||
'date_invoice': time.strftime('%Y') + '-' + '07' + '-01',
|
||||
})
|
||||
self.account_invoice_line_model.create({'product_id': self.product.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 50,
|
||||
'invoice_id': invoice_a.id,
|
||||
'name': 'product that cost ' + str(50),
|
||||
'account_id': account_expenses.id,
|
||||
})
|
||||
|
||||
invoice_b = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
|
||||
'reference_type': 'none',
|
||||
'currency_id': self.currency_usd_id,
|
||||
'name': 'invoice to vendor',
|
||||
'account_id': self.account_rsa.id,
|
||||
'type': 'in_invoice',
|
||||
'date_invoice': time.strftime('%Y') + '-' + '08' + '-01',
|
||||
})
|
||||
self.account_invoice_line_model.create({'product_id': self.product.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 50,
|
||||
'invoice_id': invoice_b.id,
|
||||
'name': 'product that cost ' + str(50),
|
||||
'account_id': account_expenses.id,
|
||||
})
|
||||
|
||||
invoice_a.action_invoice_open()
|
||||
invoice_b.action_invoice_open()
|
||||
|
||||
# Preparing Payments
|
||||
# One partial for invoice_a (fully assigned to it)
|
||||
payment_a = self.env['account.payment'].create({'payment_type': 'outbound',
|
||||
'amount': 25,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'company_id': self.env.ref('base.main_company').id,
|
||||
'payment_date': time.strftime('%Y') + '-' + '07' + '-01',
|
||||
'partner_id': self.partner_agrolait_id,
|
||||
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
|
||||
'destination_journal_id': dest_journal_id.id,
|
||||
'partner_type': 'supplier'})
|
||||
|
||||
# One that will complete the payment of a, the rest goes to b
|
||||
payment_b = self.env['account.payment'].create({'payment_type': 'outbound',
|
||||
'amount': 50,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'company_id': self.env.ref('base.main_company').id,
|
||||
'payment_date': time.strftime('%Y') + '-' + '08' + '-01',
|
||||
'partner_id': self.partner_agrolait_id,
|
||||
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
|
||||
'destination_journal_id': dest_journal_id.id,
|
||||
'partner_type': 'supplier'})
|
||||
|
||||
# The last one will complete the payment of b
|
||||
payment_c = self.env['account.payment'].create({'payment_type': 'outbound',
|
||||
'amount': 25,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'company_id': self.env.ref('base.main_company').id,
|
||||
'payment_date': time.strftime('%Y') + '-' + '09' + '-01',
|
||||
'partner_id': self.partner_agrolait_id,
|
||||
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
|
||||
'destination_journal_id': dest_journal_id.id,
|
||||
'partner_type': 'supplier'})
|
||||
|
||||
payment_a.post()
|
||||
payment_b.post()
|
||||
payment_c.post()
|
||||
|
||||
# Assigning payments to invoices
|
||||
debit_line_a = payment_a.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
|
||||
debit_line_b = payment_b.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
|
||||
debit_line_c = payment_c.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
|
||||
|
||||
invoice_a.assign_outstanding_credit(debit_line_a.id)
|
||||
invoice_a.assign_outstanding_credit(debit_line_b.id)
|
||||
invoice_b.assign_outstanding_credit(debit_line_b.id)
|
||||
invoice_b.assign_outstanding_credit(debit_line_c.id)
|
||||
|
||||
# Asserting correctness (only in the payable account)
|
||||
full_reconcile = False
|
||||
for inv in (invoice_a + invoice_b):
|
||||
self.assertTrue(inv.reconciled)
|
||||
for aml in (inv.payment_move_line_ids + inv.move_id.line_ids).filtered(lambda l: l.account_id == self.account_rsa):
|
||||
self.assertEqual(aml.amount_residual, 0.0)
|
||||
self.assertEqual(aml.amount_residual_currency, 0.0)
|
||||
self.assertTrue(aml.reconciled)
|
||||
if not full_reconcile:
|
||||
full_reconcile = aml.full_reconcile_id
|
||||
else:
|
||||
self.assertTrue(aml.full_reconcile_id == full_reconcile)
|
||||
|
||||
full_rec_move = full_reconcile.exchange_move_id
|
||||
# Globally check whether the amount is correct
|
||||
self.assertEqual(full_rec_move.amount, 18.75)
|
||||
|
||||
# Checking if the direction of the move is correct
|
||||
full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa)
|
||||
self.assertEqual(full_rec_payable.balance, 18.75)
|
||||
|
||||
Reference in New Issue
Block a user