[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:
Laurent Smet
2022-04-06 16:45:41 +02:00
parent aa43799ed9
commit 5883f0a84c
12 changed files with 2931 additions and 2785 deletions
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,
-1
View File
@@ -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
+31 -35
View File
@@ -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):
"""
+4 -1
View File
@@ -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