[IMP] account: foreign currency reconciliation (Exchange diff entries on partials)
* Previously, when reconciling journal items in multi-currencies, the exchange difference entry was created only on the full reconciliation. It will now be created directly at each partial to ensure the ratio between amount_residual_currency and amount_residual is always kept identical. * Also when reconciling two lines, one with a foreign currency and one expressed in company currency, the reconciliation is now made based on the foreign currency. ==== RATIONALE ==== This patch allows to fix the following use cases (among others) 1) When everything is expressed in foreign currency, the reconciliation is made in that currency: Suppose EUR is the foreign currency and USD is the company currency. Reconciling: L1: 120 EUR 60 USD (rate 2:1) L2: 240 EUR 80 USD (rate 3:1) ..leads to a partial of 120 EUR and min(80, 60) = 60 USD After the reconciliation, L1 is fully matched but L2 is still open with 120 EUR but only 20 USD. This is the first problem is the current reconciliation because L2 is supposed to have a rate 3:1 so the residual amount should be 120 / 3 = 40 USD. Since the rate is no longer consistent on L2, the user will probably close the reconciliation by using another line in EUR or will close manually the reconciliation with 20 USD but without any additional exchange difference journal items explaining where this unconsistency comes from. 2) When the current lines are mixing multiple currencies, the reconciliation is made using the company's currency. In some countries like Mexico, Ethiopia or Costa Rica, the customer is free to pay an invoice using the company's currency instead of the foreign one. So, suppose USD is the foreign currency and MXN is the company currency. The invoice is expressed by: L1: 120 USD 60 MXN (rate 2:1) If the customer is paying at a date in which the rate is 3:1, he is free to pay either L2: 120 USD 40 MXN (rate 3:1), either L2: 40 MXN. In the second case, he is expecting the invoice to be fully paid because its paiement is equivalent to 120 USD at the payment date. In Odoo, the second case led to an open balance of 20 MXN and the invoice was not completely paid. Even this situation could be easily fixed by a manual write-off, this makes the Mexican payment EDI very complicated to fullfil correctly because the government is expecting a complete matching between the invoice and the payment. When the customer is paying multiple invoices or the invoices are paid using multiple payments, the currently generated mexican EDI file in Odoo was wrong. ==== REFERENCES ==== Original idea suggested by hbto@vauxoo.com. Thanks for the contribution and patience of the many persons having, at some point, helped on that. github issue: https://github.com/odoo/odoo/issues/37469 closes odoo/odoo#84201 Task: 2669371 Related: odoo/enterprise#24268 Signed-off-by: Quentin De Paoli <qdp@odoo.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ class AccountPartialReconcile(models.Model):
|
||||
full_reconcile_id = fields.Many2one(
|
||||
comodel_name='account.full.reconcile',
|
||||
string="Full Reconcile", copy=False)
|
||||
exchange_move_id = fields.Many2one(comodel_name='account.move')
|
||||
|
||||
# ==== Currency fields ====
|
||||
company_currency_id = fields.Many2one(
|
||||
@@ -112,10 +113,15 @@ class AccountPartialReconcile(models.Model):
|
||||
|
||||
# Retrieve the CABA entries to reverse.
|
||||
moves_to_reverse = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', self.ids)])
|
||||
# Same for the exchange difference entries.
|
||||
moves_to_reverse += self.exchange_move_id
|
||||
|
||||
# Unlink partials before doing anything else to avoid 'Record has already been deleted' due to the recursion.
|
||||
res = super().unlink()
|
||||
|
||||
# Remove the matching numbers before reversing the moves to avoid trying to remove the full twice.
|
||||
full_to_unlink.unlink()
|
||||
|
||||
# Reverse CABA entries.
|
||||
today = fields.Date.context_today(self)
|
||||
default_values_list = [{
|
||||
@@ -124,9 +130,6 @@ class AccountPartialReconcile(models.Model):
|
||||
} for move in moves_to_reverse]
|
||||
moves_to_reverse._reverse_moves(default_values_list, cancel=True)
|
||||
|
||||
# Remove the matching numbers.
|
||||
full_to_unlink.unlink()
|
||||
|
||||
return res
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -150,13 +153,14 @@ class AccountPartialReconcile(models.Model):
|
||||
for move in {partial.debit_move_id.move_id, partial.credit_move_id.move_id}:
|
||||
|
||||
# Collect data about cash basis.
|
||||
if move.id not in tax_cash_basis_values_per_move:
|
||||
tax_cash_basis_values_per_move[move.id] = move._collect_tax_cash_basis_values()
|
||||
if move.id in tax_cash_basis_values_per_move:
|
||||
move_values = tax_cash_basis_values_per_move[move.id]
|
||||
else:
|
||||
move_values = move._collect_tax_cash_basis_values()
|
||||
|
||||
# Nothing to process on the move.
|
||||
if not tax_cash_basis_values_per_move.get(move.id):
|
||||
if not move_values:
|
||||
continue
|
||||
move_values = tax_cash_basis_values_per_move[move.id]
|
||||
|
||||
# Check the cash basis configuration only when at least one cash basis tax entry need to be created.
|
||||
journal = partial.company_id.tax_cash_basis_journal_id
|
||||
@@ -185,9 +189,17 @@ class AccountPartialReconcile(models.Model):
|
||||
counterpart_line = partial.debit_move_id
|
||||
|
||||
if move_values['currency'] == move.company_id.currency_id:
|
||||
# Ignore the exchange difference.
|
||||
if move.company_currency_id.is_zero(partial_amount):
|
||||
continue
|
||||
|
||||
# Percentage made on company's currency.
|
||||
percentage = partial_amount / move_values['total_balance']
|
||||
else:
|
||||
# Ignore the exchange difference.
|
||||
if move.currency_id.is_zero(partial_amount_currency):
|
||||
continue
|
||||
|
||||
# Percentage made on foreign currency.
|
||||
percentage = partial_amount_currency / move_values['total_amount_currency']
|
||||
|
||||
@@ -205,6 +217,8 @@ class AccountPartialReconcile(models.Model):
|
||||
else:
|
||||
payment_rate = 0.0
|
||||
|
||||
tax_cash_basis_values_per_move[move.id] = move_values
|
||||
|
||||
partial_vals = {
|
||||
'partial': partial,
|
||||
'percentage': percentage,
|
||||
|
||||
@@ -20,7 +20,6 @@ from . import test_account_invoice_report
|
||||
from . import test_account_move_line_tax_details
|
||||
from . import test_account_journal_dashboard
|
||||
from . import test_fiscal_position
|
||||
from . import test_reconciliation
|
||||
from . import test_sequence_mixin
|
||||
from . import test_settings
|
||||
from . import test_tax
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
from odoo import fields
|
||||
from odoo.tests.common import TransactionCase, HttpCase, tagged, Form
|
||||
|
||||
import json
|
||||
import time
|
||||
import base64
|
||||
from lxml import etree
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class AccountTestInvoicingCommon(TransactionCase):
|
||||
|
||||
@@ -412,6 +414,35 @@ class AccountTestInvoicingCommon(TransactionCase):
|
||||
self.assertRecordValues(sort_lines(move.invoice_line_ids.sorted()), expected_lines_values[:len(move.invoice_line_ids)])
|
||||
self.assertRecordValues(move, [expected_move_values])
|
||||
|
||||
def assert_invoice_outstanding_to_reconcile_widget(self, invoice, expected_amounts):
|
||||
""" Check the outstanding widget before the reconciliation.
|
||||
:param invoice: An invoice.
|
||||
:param expected_amounts: A map <move_id> -> <amount>
|
||||
"""
|
||||
invoice.invalidate_cache(['invoice_outstanding_credits_debits_widget'])
|
||||
widget_vals = json.loads(invoice.invoice_outstanding_credits_debits_widget)
|
||||
|
||||
if widget_vals:
|
||||
current_amounts = {vals['move_id']: vals['amount'] for vals in widget_vals['content']}
|
||||
else:
|
||||
current_amounts = {}
|
||||
self.assertDictEqual(current_amounts, expected_amounts)
|
||||
|
||||
def assert_invoice_outstanding_reconciled_widget(self, invoice, expected_amounts):
|
||||
""" Check the outstanding widget after the reconciliation.
|
||||
:param invoice: An invoice.
|
||||
:param expected_amounts: A map <move_id> -> <amount>
|
||||
"""
|
||||
invoice.invalidate_cache(['invoice_payments_widget'])
|
||||
widget_vals = json.loads(invoice.invoice_payments_widget)
|
||||
|
||||
if widget_vals:
|
||||
current_amounts = {vals['move_id']: vals['amount'] for vals in widget_vals['content']}
|
||||
else:
|
||||
current_amounts = {}
|
||||
formatted_expected_amounts = {k: invoice.currency_id.round(v) for k, v in expected_amounts.items()}
|
||||
self.assertDictEqual(current_amounts, formatted_expected_amounts)
|
||||
|
||||
####################################################
|
||||
# Xml Comparison
|
||||
####################################################
|
||||
@@ -676,38 +707,3 @@ class TestAccountReconciliationCommon(AccountTestInvoicingCommon):
|
||||
payment_term_id=payment_term_id,
|
||||
auto_validate=True
|
||||
)
|
||||
|
||||
def make_payment(self, invoice_record, bank_journal, amount=0.0, amount_currency=0.0, currency_id=None, reconcile_param=None):
|
||||
reconcile_param = reconcile_param or []
|
||||
bank_stmt = self.env['account.bank.statement'].create({
|
||||
'journal_id': bank_journal.id,
|
||||
'date': time.strftime('%Y') + '-07-15',
|
||||
'name': 'payment' + invoice_record.name,
|
||||
'line_ids': [(0, 0, {
|
||||
'payment_ref': 'payment',
|
||||
'partner_id': self.partner_agrolait_id,
|
||||
'amount': amount,
|
||||
'amount_currency': amount_currency,
|
||||
'foreign_currency_id': currency_id,
|
||||
})],
|
||||
})
|
||||
bank_stmt.button_post()
|
||||
|
||||
bank_stmt.line_ids[0].reconcile(reconcile_param)
|
||||
return bank_stmt
|
||||
|
||||
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(move_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
|
||||
line = invoice_record.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
|
||||
bank_stmt = self.make_payment(invoice_record, bank_journal, amount=amount, amount_currency=amount_currency, currency_id=transaction_currency_id, reconcile_param=[{'id': line.id}])
|
||||
customer_move_lines = bank_stmt.line_ids.line_ids
|
||||
|
||||
#we create a supplier bill in given invoice_currency
|
||||
invoice_record = self.create_invoice(move_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
|
||||
line = invoice_record.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
|
||||
bank_stmt = self.make_payment(invoice_record, bank_journal, amount=-amount, amount_currency=-amount_currency, currency_id=transaction_currency_id, reconcile_param=[{'id': line.id}])
|
||||
supplier_move_lines = bank_stmt.line_ids.line_ids
|
||||
return customer_move_lines, supplier_move_lines
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
import json
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon):
|
||||
@@ -71,40 +69,6 @@ class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon):
|
||||
})
|
||||
cls.payment_2017_curr_3.action_post()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# HELPERS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _test_all_outstanding_payments(self, invoice, expected_amounts):
|
||||
''' Check the outstanding payments widget before/after the reconciliation.
|
||||
:param invoice: An account.move record.
|
||||
:param expected_amounts: A map <move_id> -> <amount>
|
||||
'''
|
||||
|
||||
# Check suggested outstanding payments.
|
||||
to_reconcile_payments_widget_vals = json.loads(invoice.invoice_outstanding_credits_debits_widget)
|
||||
|
||||
self.assertTrue(to_reconcile_payments_widget_vals)
|
||||
|
||||
current_amounts = {vals['move_id']: vals['amount'] for vals in to_reconcile_payments_widget_vals['content']}
|
||||
self.assertDictEqual(current_amounts, expected_amounts)
|
||||
|
||||
# Reconcile
|
||||
pay_term_lines = invoice.line_ids\
|
||||
.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
|
||||
to_reconcile = self.env['account.move'].browse(list(current_amounts.keys()))\
|
||||
.line_ids\
|
||||
.filtered(lambda line: line.account_id == pay_term_lines.account_id)
|
||||
(pay_term_lines + to_reconcile).reconcile()
|
||||
|
||||
# Check payments after reconciliation.
|
||||
reconciled_payments_widget_vals = json.loads(invoice.invoice_payments_widget)
|
||||
|
||||
self.assertTrue(reconciled_payments_widget_vals)
|
||||
|
||||
current_amounts = {vals['move_id']: vals['amount'] for vals in reconciled_payments_widget_vals['content']}
|
||||
self.assertDictEqual(current_amounts, expected_amounts)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# TESTS
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -144,8 +108,8 @@ class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon):
|
||||
self.payment_2017_curr_3.id: 500.0,
|
||||
}
|
||||
|
||||
self._test_all_outstanding_payments(out_invoice, expected_amounts)
|
||||
self._test_all_outstanding_payments(in_invoice, expected_amounts)
|
||||
self.assert_invoice_outstanding_to_reconcile_widget(out_invoice, expected_amounts)
|
||||
self.assert_invoice_outstanding_to_reconcile_widget(in_invoice, expected_amounts)
|
||||
|
||||
def test_outstanding_payments_foreign_currency(self):
|
||||
''' Test the outstanding payments widget on invoices having a foreign currency. '''
|
||||
@@ -180,5 +144,5 @@ class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon):
|
||||
self.payment_2017_curr_3.id: 1000.0,
|
||||
}
|
||||
|
||||
self._test_all_outstanding_payments(out_invoice, expected_amounts)
|
||||
self._test_all_outstanding_payments(in_invoice, expected_amounts)
|
||||
self.assert_invoice_outstanding_to_reconcile_widget(out_invoice, expected_amounts)
|
||||
self.assert_invoice_outstanding_to_reconcile_widget(in_invoice, expected_amounts)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -67,14 +67,16 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 12.01})],
|
||||
'currency_id': cls.currency_data['currency'].id,
|
||||
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 24.02})],
|
||||
})
|
||||
cls.out_invoice_4 = cls.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 11.99})],
|
||||
'currency_id': cls.currency_data['currency'].id,
|
||||
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 23.98})],
|
||||
})
|
||||
(cls.out_invoice_1 + cls.out_invoice_2 + cls.out_invoice_3 + cls.out_invoice_4).action_post()
|
||||
|
||||
@@ -1006,3 +1008,53 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
'partner_bank_id': self.comp_bank_account2.id,
|
||||
},
|
||||
])
|
||||
|
||||
def test_register_payment_invoice_foreign_curr_payment_comp_curr(self):
|
||||
# Invoice 1200 Gol = 400 USD
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2016-01-01',
|
||||
'invoice_date': '2016-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 1200.0})],
|
||||
})
|
||||
invoice.action_post()
|
||||
|
||||
# Payment of 600 USD (equivalent to 1200 Gol in 2017).
|
||||
# 600.0 USD should be computed correctly to fully paid the invoices.
|
||||
payment = self.env['account.payment.register']\
|
||||
.with_context(active_model='account.move', active_ids=invoice.ids)\
|
||||
.create({'currency_id': self.company_data['currency'].id})\
|
||||
._create_payments()
|
||||
|
||||
lines = (invoice + payment.move_id).line_ids.filtered(lambda x: x.account_internal_type == 'receivable')
|
||||
self.assertRecordValues(lines, [
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'currency_id': self.currency_data['currency'].id, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'currency_id': self.company_data['currency'].id, 'reconciled': True},
|
||||
])
|
||||
|
||||
def test_register_payment_invoice_comp_curr_payment_foreign_curr(self):
|
||||
# Invoice of 400 USD (equivalent to 1200 Gol in 2016).
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2016-01-01',
|
||||
'invoice_date': '2016-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 400.0})],
|
||||
})
|
||||
invoice.action_post()
|
||||
|
||||
# Payment of 600 USD = 1200 Gol.
|
||||
# 1200.0 Gol should be computed correctly to fully paid the invoices.
|
||||
payment = self.env['account.payment.register']\
|
||||
.with_context(active_model='account.move', active_ids=invoice.ids)\
|
||||
.create({'currency_id': self.currency_data['currency'].id})\
|
||||
._create_payments()
|
||||
|
||||
lines = (invoice + payment.move_id).line_ids.filtered(lambda x: x.account_internal_type == 'receivable')
|
||||
self.assertRecordValues(lines, [
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'currency_id': self.company_data['currency'].id, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'currency_id': self.currency_data['currency'].id, 'reconciled': True},
|
||||
])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -414,29 +414,74 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
wizard.show_partner_bank_account = wizard.payment_method_line_id.code in self.env['account.payment']._get_method_codes_using_bank_account()
|
||||
wizard.require_partner_bank_account = wizard.payment_method_line_id.code in self.env['account.payment']._get_method_codes_needing_bank_account()
|
||||
|
||||
@api.depends('source_amount', 'source_amount_currency', 'source_currency_id', 'company_id', 'currency_id', 'payment_date')
|
||||
@api.depends('can_edit_wizard', 'source_amount', 'source_amount_currency', 'source_currency_id', 'company_id', 'currency_id', 'payment_date')
|
||||
def _compute_amount(self):
|
||||
for wizard in self:
|
||||
if wizard.source_currency_id == wizard.currency_id:
|
||||
# Same currency.
|
||||
wizard.amount = wizard.source_amount_currency
|
||||
elif wizard.currency_id == wizard.company_id.currency_id:
|
||||
# Payment expressed on the company's currency.
|
||||
wizard.amount = wizard.source_amount
|
||||
if wizard.source_currency_id and wizard.can_edit_wizard:
|
||||
comp_curr = wizard.company_id.currency_id
|
||||
if wizard.source_currency_id == wizard.currency_id:
|
||||
# Same currency.
|
||||
wizard.amount = wizard.source_amount_currency
|
||||
elif wizard.source_currency_id != comp_curr and wizard.currency_id == comp_curr:
|
||||
# Foreign currency on source line but the company currency one on the opposite line.
|
||||
wizard.amount = wizard.source_currency_id._convert(
|
||||
wizard.source_amount_currency,
|
||||
comp_curr,
|
||||
wizard.company_id,
|
||||
wizard.payment_date,
|
||||
)
|
||||
elif wizard.source_currency_id == comp_curr and wizard.currency_id != comp_curr:
|
||||
# Company currency on source line but a foreign currency one on the opposite line.
|
||||
batch = wizard._get_batches()[0]
|
||||
wizard.amount = abs(sum(
|
||||
comp_curr._convert(
|
||||
aml.amount_residual,
|
||||
wizard.currency_id,
|
||||
wizard.company_id,
|
||||
aml.date,
|
||||
)
|
||||
for aml in batch['lines']
|
||||
))
|
||||
else:
|
||||
# Foreign currency on payment different than the one set on the journal entries.
|
||||
wizard.amount = comp_curr._convert(
|
||||
wizard.source_amount,
|
||||
wizard.currency_id,
|
||||
wizard.company_id,
|
||||
wizard.payment_date,
|
||||
)
|
||||
else:
|
||||
# Foreign currency on payment different than the one set on the journal entries.
|
||||
amount_payment_currency = wizard.company_id.currency_id._convert(wizard.source_amount, wizard.currency_id, wizard.company_id, wizard.payment_date)
|
||||
wizard.amount = amount_payment_currency
|
||||
# The wizard is not editable so no partial payment allowed and then, 'amount' is not used.
|
||||
wizard.amount = None
|
||||
|
||||
@api.depends('amount')
|
||||
@api.depends('can_edit_wizard', 'amount')
|
||||
def _compute_payment_difference(self):
|
||||
for wizard in self:
|
||||
comp_curr = wizard.company_id.currency_id
|
||||
if wizard.source_currency_id == wizard.currency_id:
|
||||
# Same currency.
|
||||
wizard.payment_difference = wizard.source_amount_currency - wizard.amount
|
||||
elif wizard.currency_id == wizard.company_id.currency_id:
|
||||
# Payment expressed on the company's currency.
|
||||
wizard.payment_difference = wizard.source_amount - wizard.amount
|
||||
elif wizard.source_currency_id != comp_curr and wizard.currency_id == comp_curr:
|
||||
# Foreign currency on source line but the company currency one on the opposite line.
|
||||
rate = self.env['res.currency']._get_conversion_rate(
|
||||
wizard.company_id.currency_id,
|
||||
wizard.source_currency_id,
|
||||
wizard.company_id,
|
||||
wizard.payment_date,
|
||||
)
|
||||
amount_payment_foreign_curr = wizard.source_currency_id.round(wizard.amount * rate)
|
||||
residual_foreign_curr = wizard.source_amount_currency - amount_payment_foreign_curr
|
||||
wizard.payment_difference = wizard.company_id.currency_id.round(residual_foreign_curr / rate)
|
||||
elif wizard.source_currency_id == comp_curr and wizard.currency_id != comp_curr:
|
||||
# Company currency on source line but a foreign currency one on the opposite line.
|
||||
rate = self.env['res.currency']._get_conversion_rate(
|
||||
wizard.company_id.currency_id,
|
||||
wizard.source_currency_id,
|
||||
wizard.company_id,
|
||||
wizard.payment_date,
|
||||
)
|
||||
amount_to_pay_foreign_curr = wizard.currency_id.round(wizard.source_amount * rate)
|
||||
wizard.payment_difference = amount_to_pay_foreign_curr - wizard.amount
|
||||
else:
|
||||
# Foreign currency on payment different than the one set on the journal entries.
|
||||
amount_payment_currency = wizard.company_id.currency_id._convert(wizard.source_amount, wizard.currency_id, wizard.company_id, wizard.payment_date)
|
||||
|
||||
@@ -111,11 +111,11 @@ class TestPrintCheck(AccountTestInvoicingCommon):
|
||||
})
|
||||
invoice.action_post()
|
||||
|
||||
# Partial payment in foreign currency: 100Gol = 33.33$.
|
||||
# Partial payment in foreign currency.
|
||||
payment = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=invoice.ids).create({
|
||||
'payment_method_line_id': self.payment_method_line_check.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'amount': 100.0,
|
||||
'amount': 150.0,
|
||||
'payment_date': '2017-01-01',
|
||||
})._create_payments()
|
||||
|
||||
|
||||
@@ -910,41 +910,17 @@ class TestStockValuationWithCOA(AccountTestInvoicingCommon):
|
||||
for p in patchers:
|
||||
p.stop()
|
||||
|
||||
move_lines = inv.line_ids
|
||||
self.assertEqual(len(move_lines), 2)
|
||||
|
||||
# PAYABLE CHECK
|
||||
payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
|
||||
self.assertEqual(payable_line.amount_currency, -30.0)
|
||||
self.assertAlmostEqual(payable_line.balance, -15.00)
|
||||
|
||||
# PRODUCTS CHECKS
|
||||
|
||||
# DELIVERY DIFFERENCE (AVERAGE)
|
||||
# We ordered a product at 30 EUR valued at 20 USD
|
||||
# We received it when the exchange rate has appreciated
|
||||
# So, the actualized 20 USD are now 20*1.5/0.7 = 42.86 USD
|
||||
product_lines = move_lines.filtered(lambda l: l.product_id == product_avg)
|
||||
|
||||
# Although those 42.86 USD are just due to the exchange difference
|
||||
stock_line = product_lines.filtered(lambda l: l.account_id == self.stock_input_account)
|
||||
self.assertEqual(stock_line.journal_id, inv.journal_id)
|
||||
self.assertEqual(stock_line.amount_currency, 30.00)
|
||||
self.assertAlmostEqual(stock_line.balance, 15.00)
|
||||
full_reconcile = stock_line.full_reconcile_id
|
||||
self.assertTrue(full_reconcile.exists())
|
||||
|
||||
reconciled_lines = full_reconcile.reconciled_line_ids - stock_line
|
||||
self.assertEqual(len(reconciled_lines), 2)
|
||||
|
||||
stock_journal_line = reconciled_lines.filtered(lambda l: l.journal_id == self.stock_journal)
|
||||
self.assertEqual(stock_journal_line.amount_currency, -30.00)
|
||||
self.assertAlmostEqual(stock_journal_line.balance, -42.86)
|
||||
|
||||
exhange_diff_journal = company.currency_exchange_journal_id.exists()
|
||||
exchange_stock_line = reconciled_lines.filtered(lambda l: l.journal_id == exhange_diff_journal)
|
||||
self.assertEqual(exchange_stock_line.amount_currency, 0.00)
|
||||
self.assertAlmostEqual(exchange_stock_line.balance, 27.86)
|
||||
self.assertRecordValues(inv.line_ids, [
|
||||
# pylint: disable=C0326
|
||||
{'balance': 15.0, 'amount_currency': 30.0, 'account_id': self.stock_input_account.id},
|
||||
{'balance': -15.0, 'amount_currency': -30.0, 'account_id': self.company_data['default_account_payable'].id},
|
||||
])
|
||||
self.assertRecordValues(inv.line_ids.full_reconcile_id.reconciled_line_ids, [
|
||||
# pylint: disable=C0326
|
||||
{'balance': -42.86, 'amount_currency': -30.0, 'account_id': self.stock_input_account.id},
|
||||
{'balance': 15.0, 'amount_currency': 30.0, 'account_id': self.stock_input_account.id},
|
||||
{'balance': 27.86, 'amount_currency': 0.0, 'account_id': self.stock_input_account.id},
|
||||
])
|
||||
|
||||
def test_average_realtime_with_two_delivery_anglo_saxon_valuation_multicurrency_different_dates(self):
|
||||
"""
|
||||
@@ -1119,76 +1095,47 @@ class TestStockValuationWithCOA(AccountTestInvoicingCommon):
|
||||
##########################
|
||||
# Invoice 0 #
|
||||
##########################
|
||||
move_lines = inv.line_ids
|
||||
self.assertEqual(len(move_lines), 4)
|
||||
|
||||
# PAYABLE CHECK
|
||||
payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
|
||||
self.assertEqual(payable_line.amount_currency, -100.0)
|
||||
self.assertAlmostEqual(payable_line.balance, -50.00)
|
||||
self.assertRecordValues(inv.line_ids, [
|
||||
# pylint: disable=C0326
|
||||
{'balance': 50.0, 'amount_currency': 100.0, 'account_id': self.stock_input_account.id},
|
||||
{'balance': -50.0, 'amount_currency': -100.0, 'account_id': self.company_data['default_account_payable'].id},
|
||||
{'balance': -25.0, 'amount_currency': -50.0, 'account_id': self.price_diff_account.id},
|
||||
{'balance': 25.0, 'amount_currency': 50.0, 'account_id': self.stock_input_account.id},
|
||||
])
|
||||
|
||||
# # PRODUCTS CHECKS
|
||||
|
||||
# DELIVERY DIFFERENCE (AVERAGE)
|
||||
stock_lines = move_lines.filtered(lambda l: l.account_id == self.stock_input_account)
|
||||
self.assertEqual(len(stock_lines), 2)
|
||||
self.assertAlmostEqual(sum(stock_lines.mapped('amount_currency')), 150.00)
|
||||
self.assertAlmostEqual(sum(stock_lines.mapped('balance')), 75.00)
|
||||
|
||||
price_diff_line = move_lines.filtered(lambda l: l.account_id == self.price_diff_account)
|
||||
self.assertAlmostEqual(price_diff_line.amount_currency, -50.00)
|
||||
self.assertAlmostEqual(price_diff_line.balance, -25.00)
|
||||
|
||||
full_reconcile = stock_lines.mapped('full_reconcile_id')
|
||||
self.assertTrue(full_reconcile.exists())
|
||||
|
||||
reconciled_lines = full_reconcile.reconciled_line_ids - stock_lines
|
||||
self.assertEqual(len(reconciled_lines), 2)
|
||||
|
||||
stock_journal_line = reconciled_lines.filtered(lambda l: l.journal_id == self.stock_journal)
|
||||
self.assertEqual(stock_journal_line.amount_currency, -150)
|
||||
self.assertAlmostEqual(stock_journal_line.balance, -214.29)
|
||||
|
||||
exchange_stock_line = reconciled_lines.filtered(lambda l: l.journal_id == exchange_diff_journal)
|
||||
self.assertEqual(exchange_stock_line.amount_currency, 0.00)
|
||||
self.assertAlmostEqual(exchange_stock_line.balance, 139.29)
|
||||
self.assertRecordValues(inv.line_ids.full_reconcile_id.reconciled_line_ids, [
|
||||
# pylint: disable=C0326
|
||||
# Exchange difference lines:
|
||||
{'balance': 46.43, 'amount_currency': 0.0},
|
||||
{'balance': 92.86, 'amount_currency': 0.0},
|
||||
# Other lines:
|
||||
{'balance': 50.0, 'amount_currency': 100.0},
|
||||
{'balance': 25.0, 'amount_currency': 50.0},
|
||||
{'balance': -214.29, 'amount_currency': -150.0},
|
||||
])
|
||||
|
||||
##########################
|
||||
# Invoice 1 #
|
||||
##########################
|
||||
move_lines = inv1.line_ids
|
||||
self.assertEqual(len(move_lines), 4)
|
||||
|
||||
# PAYABLE CHECK
|
||||
payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
|
||||
self.assertEqual(payable_line.amount_currency, -200.0)
|
||||
self.assertAlmostEqual(payable_line.balance, -90.91)
|
||||
self.assertRecordValues(inv1.line_ids, [
|
||||
# pylint: disable=C0326
|
||||
{'balance': 90.91, 'amount_currency': 200.0, 'account_id': self.stock_input_account.id},
|
||||
{'balance': -90.91, 'amount_currency': -200.0, 'account_id': self.company_data['default_account_payable'].id},
|
||||
{'balance': 22.73, 'amount_currency': 50.0, 'account_id': self.price_diff_account.id},
|
||||
{'balance': -22.73, 'amount_currency': -50.0, 'account_id': self.stock_input_account.id},
|
||||
])
|
||||
|
||||
# # PRODUCTS CHECKS
|
||||
|
||||
# DELIVERY DIFFERENCE (AVERAGE)
|
||||
stock_lines = move_lines.filtered(lambda l: l.account_id == self.stock_input_account)
|
||||
self.assertEqual(stock_lines.mapped('journal_id'), inv.journal_id)
|
||||
self.assertAlmostEqual(sum(stock_lines.mapped('amount_currency')), 150.00)
|
||||
self.assertAlmostEqual(sum(stock_lines.mapped('balance')), 68.18)
|
||||
|
||||
price_diff_line = move_lines.filtered(lambda l: l.account_id == self.price_diff_account)
|
||||
self.assertEqual(price_diff_line.amount_currency, 50.00)
|
||||
self.assertAlmostEqual(price_diff_line.balance, 22.73)
|
||||
|
||||
full_reconcile = stock_lines.mapped('full_reconcile_id')
|
||||
self.assertTrue(full_reconcile.exists())
|
||||
|
||||
reconciled_lines = full_reconcile.reconciled_line_ids - stock_lines
|
||||
self.assertEqual(len(reconciled_lines), 3)
|
||||
|
||||
stock_journal_line = reconciled_lines.filtered(lambda l: l.journal_id == self.stock_journal)
|
||||
self.assertEqual(stock_journal_line.amount_currency, -150)
|
||||
self.assertAlmostEqual(stock_journal_line.balance, -187.5)
|
||||
|
||||
exchange_stock_lines = reconciled_lines.filtered(lambda l: l.journal_id == exchange_diff_journal)
|
||||
self.assertAlmostEqual(sum(exchange_stock_lines.mapped('amount_currency')), 0.00)
|
||||
self.assertAlmostEqual(sum(exchange_stock_lines.mapped('balance')), 119.32)
|
||||
self.assertRecordValues(inv1.line_ids.full_reconcile_id.reconciled_line_ids, [
|
||||
# pylint: disable=C0326
|
||||
# Other lines:
|
||||
{'balance': -187.5, 'amount_currency': -150.0},
|
||||
{'balance': 90.91, 'amount_currency': 200.0},
|
||||
{'balance': -22.73, 'amount_currency': -50.0},
|
||||
# Exchange difference lines:
|
||||
{'balance': 119.32, 'amount_currency': 0.0},
|
||||
])
|
||||
|
||||
def test_anglosaxon_valuation_price_total_diff_discount(self):
|
||||
"""
|
||||
|
||||
@@ -255,8 +255,11 @@ class Currency(models.Model):
|
||||
# apply conversion rate
|
||||
if self == to_currency:
|
||||
to_amount = from_amount
|
||||
else:
|
||||
elif from_amount:
|
||||
to_amount = from_amount * self._get_conversion_rate(self, to_currency, company, date)
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
# apply rounding
|
||||
return to_currency.round(to_amount) if round else to_amount
|
||||
|
||||
|
||||
Reference in New Issue
Block a user