[FIX] purchase: real time average multicurrency valuation

Do a PO in a foreign currency
Invoice it in the same foreign currency
Validate the invoice

Before this commit, The account move of the invoice
contained a price difference line
which is wrong, since the price has not changed in any ways

After this commit, the move has only two lines
one in the stock input, one in the payable

Also, the case where the price does change
due to a change in currency rate is handled

OPW 1955315

closes odoo/odoo#32616

Signed-off-by: Lucas Perais (lpe) <lpe@odoo.com>
This commit is contained in:
Lucas Perais (lpe)
2019-04-11 09:07:11 +00:00
parent 44aedc9b13
commit 56fb25a695
5 changed files with 440 additions and 24 deletions
+3 -2
View File
@@ -194,9 +194,11 @@ class AccountInvoice(models.Model):
for val_stock_move in valuation_stock_move:
# In case val_stock_move is a return move, its valuation entries have been made with the
# currency rate corresponding to the original stock move
valuation_date = val_stock_move.origin_returned_move_id.date or val_stock_move.date_expected
valuation_date = val_stock_move.origin_returned_move_id.date or val_stock_move.date
valuation_price_unit_total += company_currency.with_context(date=valuation_date).compute(abs(val_stock_move.price_unit) * val_stock_move.product_qty, inv.currency_id, round=False)
valuation_total_qty += val_stock_move.product_qty
# in Stock Move, price unit is in company_currency
valuation_price_unit = valuation_price_unit_total / valuation_total_qty
valuation_price_unit = i_line.product_id.uom_id._compute_price(valuation_price_unit, i_line.uom_id)
line_quantity = valuation_total_qty
@@ -209,7 +211,6 @@ class AccountInvoice(models.Model):
invoice_cur_prec = inv.currency_id.decimal_places
if float_compare(valuation_price_unit, i_line.price_unit, precision_digits=invoice_cur_prec) != 0 and float_compare(line['price_unit'], i_line.price_unit, precision_digits=invoice_cur_prec) == 0:
# price with discount and without tax included
price_unit = i_line.price_unit * (1 - (i_line.discount or 0.0) / 100.0)
tax_ids = []
+2 -2
View File
@@ -749,7 +749,7 @@ class PurchaseOrderLine(models.Model):
if line.product_uom.id != line.product_id.uom_id.id:
price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
if order.currency_id != order.company_id.currency_id:
price_unit = order.currency_id.compute(price_unit, order.company_id.currency_id, round=False)
price_unit = order.currency_id.with_context(date=order.date_approve).compute(price_unit, order.company_id.currency_id, round=False)
return price_unit
@api.multi
@@ -769,7 +769,7 @@ class PurchaseOrderLine(models.Model):
'name': self.name or '',
'product_id': self.product_id.id,
'product_uom': self.product_uom.id,
'date': self.order_id.date_order,
'date': self.order_id.date_approve,
'date_expected': self.date_planned,
'location_id': self.order_id.partner_id.property_stock_supplier.id,
'location_dest_id': self.order_id._get_destination_location(),
+1 -1
View File
@@ -46,7 +46,7 @@ class StockMove(models.Model):
if line.product_uom.id != line.product_id.uom_id.id:
price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
if order.currency_id != order.company_id.currency_id:
price_unit = order.currency_id.compute(price_unit, order.company_id.currency_id, round=False)
price_unit = order.currency_id.with_context(date=self.date or order.date_approve).compute(price_unit, order.company_id.currency_id, round=False)
return price_unit
return super(StockMove, self)._get_price_unit()
@@ -63,8 +63,10 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
""" Tests the case into which we receive the goods first, and then make the invoice.
"""
test_product = self.test_product_delivery
purchase_order = self._create_purchase(test_product, '2018-01-01')
self._process_pickings(purchase_order.picking_ids)
date_po_and_delivery = '2018-01-01'
purchase_order = self._create_purchase(test_product, date_po_and_delivery)
self._process_pickings(purchase_order.picking_ids, date=date_po_and_delivery)
invoice = self._create_invoice_for_po(purchase_order, '2018-02-02')
self.env['res.currency.rate'].create({
@@ -150,8 +152,9 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
""" Tests the case into which we receive part of the goods first, then 2 invoices at different rates, and finally the remaining quantities
"""
test_product = self.test_product_delivery
purchase_order = self._create_purchase(test_product, '2017-01-01', quantity=5.0)
self._process_pickings(purchase_order.picking_ids, quantity=2.0)
date_po_and_delivery0 = '2017-01-01'
purchase_order = self._create_purchase(test_product, date_po_and_delivery0, quantity=5.0)
self._process_pickings(purchase_order.picking_ids, quantity=2.0, date=date_po_and_delivery0)
picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order="id asc", limit=1)
invoice = self._create_invoice_for_po(purchase_order, '2017-01-15')
@@ -184,6 +187,8 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
'rate': 12.195747002,
'name': '2017-04-01',
})
# We don't need to make the date of processing explicit since the very last rate
# will be taken
self._process_pickings(purchase_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0)
picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order='id desc', limit=1)
self.check_reconciliation(invoice2, picking)
+425 -15
View File
@@ -3,6 +3,7 @@
import time
from datetime import datetime
from unittest.mock import patch
from odoo.tests import Form
from odoo.tests.common import TransactionCase, tagged
@@ -143,6 +144,7 @@ class TestStockValuation(TransactionCase):
the currency rate, validate the receipt and then check that the value of the received goods
is set according to the last currency rate.
"""
self.env['res.currency.rate'].search([]).unlink()
usd_currency = self.env.ref('base.USD')
self.env.user.company_id.currency_id = usd_currency.id
@@ -285,6 +287,9 @@ class TestStockValuationWithCOA(AccountingTestCase):
self.partner_id = self.env.ref('base.res_partner_1')
self.product1 = self.env.ref('product.product_product_8')
Account = self.env['account.account']
self.usd_currency = self.env.ref('base.USD')
self.eur_currency = self.env.ref('base.EUR')
self.stock_input_account = Account.create({
'name': 'Stock Input',
'code': 'StockIn',
@@ -302,6 +307,11 @@ class TestStockValuationWithCOA(AccountingTestCase):
'code': 'Stock Valuation',
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
})
self.price_diff_account = Account.create({
'name': 'price diff account',
'code': 'price diff account',
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
})
self.stock_journal = self.env['account.journal'].create({
'name': 'Stock Journal',
'code': 'STJTEST',
@@ -319,12 +329,7 @@ class TestStockValuationWithCOA(AccountingTestCase):
self.product1.product_tmpl_id.cost_method = 'fifo'
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'delivery'
price_diff_account = self.env['account.account'].create({
'name': 'price diff account',
'code': 'price diff account',
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
})
self.product1.property_account_creditor_price_difference = price_diff_account
self.product1.property_account_creditor_price_difference = self.price_diff_account
# Receive 10@10 ; create the vendor bill
po1 = self.env['purchase.order'].create({
@@ -411,7 +416,7 @@ class TestStockValuationWithCOA(AccountingTestCase):
creditnote_po2.action_invoice_open()
# check the anglo saxon entries
price_diff_entry = self.env['account.move.line'].search([('account_id', '=', price_diff_account.id)])
price_diff_entry = self.env['account.move.line'].search([('account_id', '=', self.price_diff_account.id)])
self.assertEqual(price_diff_entry.credit, 100)
def test_anglosaxon_valuation(self):
@@ -419,12 +424,7 @@ class TestStockValuationWithCOA(AccountingTestCase):
self.product1.product_tmpl_id.cost_method = 'fifo'
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'delivery'
price_diff_account = self.env['account.account'].create({
'name': 'price diff account',
'code': 'price diff account',
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
})
self.product1.property_account_creditor_price_difference = price_diff_account
self.product1.property_account_creditor_price_difference = self.price_diff_account
# Create PO
po_form = Form(self.env['purchase.order'])
@@ -453,12 +453,422 @@ class TestStockValuationWithCOA(AccountingTestCase):
invoice.action_invoice_open()
# Check what was posted in the price difference account
price_diff_aml = self.env['account.move.line'].search([('account_id','=',price_diff_account.id)])
price_diff_aml = self.env['account.move.line'].search([('account_id','=', self.price_diff_account.id)])
self.assertEquals(len(price_diff_aml), 1, "Only one line should have been generated in the price difference account.")
self.assertAlmostEquals(price_diff_aml.debit, 5, "Price difference should be equal to 5 (15-10)")
# Check what was posted in stock input account
input_aml = self.env['account.move.line'].search([('account_id','=',self.stock_input_account.id)])
input_aml = self.env['account.move.line'].search([('account_id','=', self.stock_input_account.id)])
self.assertEquals(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
self.assertAlmostEquals(sum(input_aml.mapped('debit')), 10, "Total debit value on stock input account should be equal to the original PO price of the product.")
self.assertAlmostEquals(sum(input_aml.mapped('credit')), 10, "Total credit value on stock input account should be equal to the original PO price of the product.")
def test_average_realtime_anglo_saxon_valuation_multicurrency_same_date(self):
"""
The PO and invoice are in the same foreign currency.
The PO is invoiced on the same date as its creation.
This shouldn't create a price difference entry.
"""
company = self.env.user.company_id
company.anglo_saxon_accounting = True
date_po = '2019-01-01'
# SetUp product
self.product1.product_tmpl_id.cost_method = 'average'
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'order'
self.product1.product_tmpl_id.purchase_method = 'purchase'
self.product1.property_account_creditor_price_difference = self.price_diff_account
# SetUp currency and rates
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
self.env['res.currency.rate'].search([]).unlink()
self.env['res.currency.rate'].create({
'name': date_po,
'rate': 1.0,
'currency_id': self.usd_currency.id,
'company_id': company.id,
})
self.env['res.currency.rate'].create({
'name': date_po,
'rate': 1.5,
'currency_id': self.eur_currency.id,
'company_id': company.id,
})
# Proceed
po = self.env['purchase.order'].create({
'currency_id': self.eur_currency.id,
'partner_id': self.partner_id.id,
'order_line': [
(0, 0, {
'name': self.product1.name,
'product_id': self.product1.id,
'product_qty': 1.0,
'product_uom': self.product1.uom_po_id.id,
'price_unit': 100.0,
'date_planned': date_po,
}),
],
})
po.button_confirm()
inv = self.env['account.invoice'].create({
'type': 'in_invoice',
'date_invoice': date_po,
'currency_id': self.eur_currency.id,
'purchase_id': po.id,
'partner_id': self.partner_id.id,
'invoice_line_ids': [(0, 0, {
'name': 'Test',
'price_subtotal': 100.0,
'price_unit': 100.0,
'product_id': self.product1.id,
'purchase_id': po.id,
'purchase_line_id': po.order_line.id,
'quantity': 1.0,
'account_id': self.stock_input_account.id,
})]
})
inv.action_invoice_open()
move_lines = inv.move_id.line_ids
self.assertEqual(len(move_lines), 2)
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, -66.67)
stock_line = move_lines.filtered(lambda l: l.account_id == self.stock_input_account)
self.assertEqual(stock_line.amount_currency, 100.0)
self.assertAlmostEqual(stock_line.balance, 66.67)
def test_realtime_anglo_saxon_valuation_multicurrency_different_dates(self):
"""
The PO and invoice are in the same foreign currency.
The PO is invoiced at a later date than its creation.
This should create a price difference entry for standard cost method
Not for average cost method though, since the PO and invoice have the same currency
"""
company = self.env.user.company_id
company.anglo_saxon_accounting = True
date_po = '2019-01-01'
date_invoice = '2019-01-16'
# SetUp product Average
self.product1.product_tmpl_id.write({
'cost_method': 'average',
'valuation': 'real_time',
'purchase_method': 'purchase',
'property_account_creditor_price_difference': self.price_diff_account.id,
})
self.product1.invoice_policy = 'order'
# SetUp product Standard
# should have bought at 60 USD
# actually invoiced at 70 EUR > 35 USD
product_standard = self.product1.product_tmpl_id.copy({
'cost_method': 'standard',
'name': 'Standard Val',
'standard_price': 60,
'property_account_creditor_price_difference': self.price_diff_account.id
}).product_variant_id
# SetUp currency and rates
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
self.env['res.currency.rate'].search([]).unlink()
self.env['res.currency.rate'].create({
'name': date_po,
'rate': 1.0,
'currency_id': self.usd_currency.id,
'company_id': company.id,
})
self.env['res.currency.rate'].create({
'name': date_po,
'rate': 1.5,
'currency_id': self.eur_currency.id,
'company_id': company.id,
})
self.env['res.currency.rate'].create({
'name': date_invoice,
'rate': 2,
'currency_id': self.eur_currency.id,
'company_id': company.id,
})
# To allow testing validation of PO
def _today(*args, **kwargs):
return date_po
patchers = [
patch('odoo.fields.Date.context_today', _today),
]
for p in patchers:
p.start()
# Proceed
po = self.env['purchase.order'].create({
'currency_id': self.eur_currency.id,
'partner_id': self.partner_id.id,
'order_line': [
(0, 0, {
'name': self.product1.name,
'product_id': self.product1.id,
'product_qty': 1.0,
'product_uom': self.product1.uom_po_id.id,
'price_unit': 100.0,
'date_planned': date_po,
}),
(0, 0, {
'name': product_standard.name,
'product_id': product_standard.id,
'product_qty': 1.0,
'product_uom': product_standard.uom_po_id.id,
'price_unit': 40.0,
'date_planned': date_po,
}),
],
})
po.button_confirm()
line_product_average = po.order_line.filtered(lambda l: l.product_id == self.product1)
line_product_standard = po.order_line.filtered(lambda l: l.product_id == product_standard)
inv = self.env['account.invoice'].create({
'type': 'in_invoice',
'date_invoice': date_invoice,
'currency_id': self.eur_currency.id,
'purchase_id': po.id,
'partner_id': self.partner_id.id,
'invoice_line_ids': [
(0, 0, {
'name': self.product1.name,
'price_subtotal': 100.0,
'price_unit': 100.0,
'product_id': self.product1.id,
'purchase_id': po.id,
'purchase_line_id': line_product_average.id,
'quantity': 1.0,
'account_id': self.stock_input_account.id,
}),
(0, 0, {
'name': product_standard.name,
'price_subtotal': 70.0,
'price_unit': 70.0,
'product_id': product_standard.id,
'purchase_id': po.id,
'purchase_line_id': line_product_standard.id,
'quantity': 1.0,
'account_id': self.stock_input_account.id,
})
]
})
inv.action_invoice_open()
for p in patchers:
p.stop()
move_lines = inv.move_id.line_ids
self.assertEqual(len(move_lines), 4)
# Ensure no exchange difference move has been created
self.assertTrue(all([not l.reconciled for l in move_lines]))
# PAYABLE CHECK
payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
self.assertEqual(payable_line.amount_currency, -170.0)
self.assertAlmostEqual(payable_line.balance, -85.00)
# PRODUCTS CHECKS
# NO EXCHANGE DIFFERENCE (average)
# We ordered for a value of 100 EUR
# But by the time we are invoiced for it
# the foreign currency appreciated from 1.5 to 2.0
# We still have to pay 100 EUR, which now values at 50 USD
product_lines = move_lines.filtered(lambda l: l.product_id == self.product1)
# Stock-wise, we have been invoiced 100 EUR, and we ordered 100 EUR
# there is no price difference
# However, 100 EUR should be converted at the time of the invoice
stock_line = product_lines.filtered(lambda l: l.account_id == self.stock_input_account)
self.assertEqual(stock_line.amount_currency, 100.00)
self.assertAlmostEqual(stock_line.balance, 50.00)
# PRICE DIFFERENCE (STANDARD)
# We ordered a product that should have cost 60 USD (120 EUR)
# However, we effectively got invoiced 70 EUR (35 USD)
product_lines = move_lines.filtered(lambda l: l.product_id == product_standard)
stock_line = product_lines.filtered(lambda l: l.account_id == self.stock_input_account)
self.assertEqual(stock_line.amount_currency, 120.00)
self.assertAlmostEqual(stock_line.balance, 60.00)
price_diff_line = product_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, -25.00)
def test_average_realtime_with_delivery_anglo_saxon_valuation_multicurrency_different_dates(self):
"""
The PO and invoice are in the same foreign currency.
The delivery occurs in between PO validation and invoicing
The invoice is created at an even different date
This should create a price difference entry.
"""
company = self.env.user.company_id
company.anglo_saxon_accounting = True
date_po = '2019-01-01'
date_delivery = '2019-01-08'
date_invoice = '2019-01-16'
product_avg = self.product1.product_tmpl_id.copy({
'valuation': 'real_time',
'purchase_method': 'purchase',
'cost_method': 'average',
'name': 'AVG',
'standard_price': 60,
'property_account_creditor_price_difference': self.price_diff_account.id
}).product_variant_id
product_avg.invoice_policy = 'order'
# SetUp currency and rates
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
self.env['res.currency.rate'].search([]).unlink()
self.env['res.currency.rate'].create({
'name': date_po,
'rate': 1.0,
'currency_id': self.usd_currency.id,
'company_id': company.id,
})
self.env['res.currency.rate'].create({
'name': date_po,
'rate': 1.5,
'currency_id': self.eur_currency.id,
'company_id': company.id,
})
self.env['res.currency.rate'].create({
'name': date_delivery,
'rate': 0.7,
'currency_id': self.eur_currency.id,
'company_id': company.id,
})
self.env['res.currency.rate'].create({
'name': date_invoice,
'rate': 2,
'currency_id': self.eur_currency.id,
'company_id': company.id,
})
# To allow testing validation of PO
def _today(*args, **kwargs):
return date_po
# To allow testing validation of Delivery
def _now(*args, **kwargs):
return date_delivery + ' 01:00:00'
patchers = [
patch('odoo.fields.Date.context_today', _today),
patch('odoo.fields.Datetime.now', _now),
]
for p in patchers:
p.start()
# Proceed
po = self.env['purchase.order'].create({
'currency_id': self.eur_currency.id,
'partner_id': self.partner_id.id,
'order_line': [
(0, 0, {
'name': product_avg.name,
'product_id': product_avg.id,
'product_qty': 1.0,
'product_uom': product_avg.uom_po_id.id,
'price_unit': 30.0,
'date_planned': date_po,
})
],
})
po.button_confirm()
line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
picking = po.picking_ids
(picking.move_lines
.filtered(lambda l: l.purchase_line_id == line_product_avg)
.write({'quantity_done': 1.0}))
picking.button_validate()
inv = self.env['account.invoice'].create({
'type': 'in_invoice',
'date_invoice': date_invoice,
'currency_id': self.eur_currency.id,
'purchase_id': po.id,
'partner_id': self.partner_id.id,
'invoice_line_ids': [
(0, 0, {
'name': product_avg.name,
'price_subtotal': 30.0,
'price_unit': 30.0,
'product_id': product_avg.id,
'purchase_id': po.id,
'purchase_line_id': line_product_avg.id,
'quantity': 1.0,
'account_id': self.stock_input_account.id,
})
]
})
inv.action_invoice_open()
for p in patchers:
p.stop()
move_lines = inv.move_id.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)