[FIX] purchase: anglosaxon stock valuation: use correct currency rates to determine whether or not to create a price difference line for invoices. (#24815)
Before that, the following scenario (tested in purchase/tests/test_anglosaxon_valuation_reconciliation.py) failed:
- create a PO
- change of currency rate
- create and validate vendor bill in foreign currency
- receive goods
- change of currency rate
- return the goods
- change of currency rate
- refund the bill
At the end of it, both invoices (bill and refund) were not in 'paid' state, but they should have been! The anglosaxon stock valuation code had some issues in the way it managed currency rates. The main one was that we converted the computed valuation_price_unit variable in company currency using the date of the invoice as a reference, which was wrong in case the invoice was a refund, as it should then use the date of the valuation for this. This problem caused the code to detect an exchange rate difference between refund and valuation as a price difference, hence generating a line in the price difference account, which caused the debtor account line of the associated account.move to be wrong, and hence impacted the residual field of the invoice ("amount due", in GUI). Finally, as this field contained an inconsistent value, the refund invoice was not marked as paid after reconciling it with the original one.
Solving this needed to refactor the code quite a bit, so that we don't convert everything in invoice currency in the end, but rather do it directly everytime we compute a part of valuation_price_unit. The test had also to be partly re-written as it was too naive in the way it used currency rates, we needed something closer to a real utilisation of the program.
This commit is contained in:
@@ -163,12 +163,14 @@ class AccountInvoice(models.Model):
|
||||
# calculate and write down the possible price difference between invoice price and product price
|
||||
for line in res:
|
||||
if line.get('invl_id', 0) == i_line.id and reference_account_id == line['account_id']:
|
||||
valuation_price_unit = i_line.product_id.uom_id._compute_price(i_line.product_id.standard_price, i_line.uom_id)
|
||||
# valuation_price unit is always expressed in invoice currency, so that it can always be computed with the good rate
|
||||
valuation_price_unit = company_currency.compute(i_line.product_id.uom_id._compute_price(i_line.product_id.standard_price, i_line.uom_id), inv.currency_id, round=False)
|
||||
line_quantity = line['quantity']
|
||||
|
||||
if i_line.product_id.cost_method != 'standard' and i_line.purchase_line_id:
|
||||
po_currency = i_line.purchase_id.currency_id
|
||||
#for average/fifo/lifo costing method, fetch real cost price from incomming moves
|
||||
valuation_price_unit = i_line.purchase_line_id.product_uom._compute_price(i_line.purchase_line_id.price_unit, i_line.uom_id)
|
||||
valuation_price_unit = po_currency.with_context(date=inv.date_invoice).compute(i_line.purchase_line_id.product_uom._compute_price(i_line.purchase_line_id.price_unit, i_line.uom_id), inv.currency_id, round=False)
|
||||
stock_move_obj = self.env['stock.move']
|
||||
valuation_stock_move = stock_move_obj.search([
|
||||
('purchase_line_id', '=', i_line.purchase_line_id.id),
|
||||
@@ -183,22 +185,20 @@ class AccountInvoice(models.Model):
|
||||
valuation_price_unit_total = 0
|
||||
valuation_total_qty = 0
|
||||
for val_stock_move in valuation_stock_move:
|
||||
valuation_price_unit_total += abs(val_stock_move.price_unit) * val_stock_move.product_qty
|
||||
# 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_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
|
||||
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
|
||||
|
||||
elif i_line.product_id.cost_method == 'real':
|
||||
elif i_line.product_id.cost_method == 'fifo':
|
||||
# In this condition, we have a real price-valuated product which has not yet been received
|
||||
valuation_price_unit = i_line.purchase_line_id.price_unit
|
||||
valuation_price_unit = po_currency.with_context(date=inv.date_invoice).compute(i_line.purchase_line_id.price_unit, inv.currency_id, round=False)
|
||||
|
||||
interim_account_price = valuation_price_unit * line_quantity
|
||||
if inv.currency_id.id != company_currency.id:
|
||||
# We express everyhting in the invoice currency
|
||||
valuation_price_unit = company_currency.with_context(date=inv.date_invoice).compute(valuation_price_unit, inv.currency_id, round=False)
|
||||
interim_account_price = company_currency.with_context(date=inv.date_invoice).compute(interim_account_price, inv.currency_id, round=False)
|
||||
|
||||
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:
|
||||
|
||||
@@ -36,8 +36,9 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
'product_qty': quantity,
|
||||
'product_uom': product.uom_po_id.id,
|
||||
'price_unit': self.product_price_unit,
|
||||
'date_planned': datetime.today(),
|
||||
'date_planned': '2017-10-07',
|
||||
})],
|
||||
'date_order': '2017-10-01',
|
||||
})
|
||||
rslt.button_confirm()
|
||||
return rslt
|
||||
@@ -51,7 +52,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
'currency_id': self.currency_two.id,
|
||||
'name': 'vendor bill',
|
||||
'type': 'in_invoice',
|
||||
'date_invoice': time.strftime('%Y') + '-12-22',
|
||||
'date_invoice': '2017-12-31',
|
||||
'account_id': account_receivable.id,
|
||||
})
|
||||
rslt.purchase_order_change()
|
||||
@@ -76,6 +77,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
def test_invoice_shipment(self):
|
||||
""" Tests the case into which we make the invoice first, and then receive the goods.
|
||||
"""
|
||||
# Create a PO and an invoice for it
|
||||
test_product = self.test_product_order
|
||||
purchase_order = self._create_purchase(test_product)
|
||||
|
||||
@@ -83,15 +85,29 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)])
|
||||
invoice_line.quantity = 1
|
||||
|
||||
self.currency_rate.rate = 13.834739702
|
||||
# The currency rate changes
|
||||
self.currency_rate = self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 13.834739702,
|
||||
'name': '2017-12-22',
|
||||
})
|
||||
|
||||
# Validate the invoice and refund the goods
|
||||
invoice.action_invoice_open()
|
||||
self._process_pickings(purchase_order.picking_ids)
|
||||
self._process_pickings(purchase_order.picking_ids, date='2017-12-24')
|
||||
picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)])
|
||||
self.check_reconciliation(invoice, picking)
|
||||
|
||||
#return the goods and refund the invoice
|
||||
self.currency_rate.rate = 10.54739702
|
||||
# The currency rate changes again
|
||||
self.currency_rate = self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 10.54739702,
|
||||
'name': '2018-01-01',
|
||||
})
|
||||
|
||||
# Return the goods and refund the invoice
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_ids=[picking.id], active_id=picking.id).create({})
|
||||
stock_return_picking.product_return_moves.quantity = 1.0
|
||||
@@ -100,12 +116,25 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
return_pick.action_assign()
|
||||
return_pick.move_lines.quantity_done = 1
|
||||
return_pick.action_done()
|
||||
self.currency_rate.rate = 9.56564564
|
||||
self._change_pickings_date(return_pick, '2018-01-13')
|
||||
|
||||
# The currency rate changes again
|
||||
self.currency_rate = self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 9.56564564,
|
||||
'name': '2018-03-12',
|
||||
})
|
||||
|
||||
# Refund the invoice
|
||||
refund_invoice_wiz = self.env['account.invoice.refund'].with_context(active_ids=[invoice.id]).create({
|
||||
'description': 'test_invoice_shipment_refund',
|
||||
'filter_refund': 'cancel',
|
||||
'date': '2018-03-15',
|
||||
})
|
||||
refund_invoice_wiz.invoice_refund()
|
||||
|
||||
# Check the result
|
||||
refund_invoice = self.env['account.invoice'].search([('name', '=', 'test_invoice_shipment_refund')])[0]
|
||||
self.assertTrue(invoice.state == refund_invoice.state == 'paid'), "Invoice and refund should both be in 'Paid' state"
|
||||
self.check_reconciliation(refund_invoice, return_pick)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
|
||||
from odoo import fields
|
||||
|
||||
class ValuationReconciliationTestCase(AccountingTestCase):
|
||||
""" Base class for tests checking interim accounts reconciliation works
|
||||
@@ -24,13 +25,20 @@ class ValuationReconciliationTestCase(AccountingTestCase):
|
||||
else:
|
||||
self.assertFalse(valuation_line.full_reconcile_id, "The reconciliation should not be total at that point.")
|
||||
|
||||
def _process_pickings(self, pickings, quantity=False):
|
||||
def _process_pickings(self, pickings, date=False, quantity=False):
|
||||
if not date:
|
||||
date = fields.Date.today()
|
||||
pickings.action_confirm()
|
||||
pickings.action_assign()
|
||||
for picking in pickings:
|
||||
for ml in picking.move_line_ids:
|
||||
ml.qty_done = quantity or ml.product_qty
|
||||
pickings.action_done()
|
||||
self._change_pickings_date(pickings, date)
|
||||
|
||||
def _change_pickings_date(self, pickings, date):
|
||||
pickings.mapped('move_lines').write({'date': date})
|
||||
pickings.mapped('move_lines.account_move_ids').write({'date': date})
|
||||
|
||||
def _create_product_category(self):
|
||||
return self.env['product.category'].create({
|
||||
@@ -54,6 +62,7 @@ class ValuationReconciliationTestCase(AccountingTestCase):
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 1.234343354, # Totally arbitratry value
|
||||
'name': '2017-01-01',
|
||||
})
|
||||
|
||||
self.input_account = self.env['account.account'].create({
|
||||
|
||||
Reference in New Issue
Block a user