[FIX] purchase: fix anglosaxon valuation

Before that, when receiving, then invoicing with price diff, the value in interim account corresponded to the price on the invoice instead of the original valuation one.

Scenario to reproduce the issue:

* create PO with 1 product, FIFO valuation, price=10
* receive PO: the entry in stock interim account has debit=credit=10
* create invoice, with price=15 instead of 10
* the invoice entry should be: payable: C=15 ; price diff: D=5 ; interim account: D=10
  ==> Instead, we had payable: C=20 ; price diff: D=5 ; interim account: D=15 => this was wrong

Also added a test case for the same scenario, verifying the amounts written in the different accounts (and refactored a bit ValuationReconciliationTestCase for easiness)

Closes #27394
This commit is contained in:
Olivier Colson
2018-10-25 18:03:27 +02:00
committed by qdp-odoo
parent f9ee8cff14
commit e6228402e7
5 changed files with 161 additions and 58 deletions
+6 -2
View File
@@ -227,7 +227,7 @@ class AccountInvoice(models.Model):
# valuation difference, it means this difference is due to exchange rates,
# so we don't create anything, the exchange rate entries will
# be processed automatically by the rest of the code.
diff_res.append({
diff_line = {
'type': 'src',
'name': i_line.name[:64],
'price_unit': inv.currency_id.round(price_unit_val_dif),
@@ -238,7 +238,11 @@ class AccountInvoice(models.Model):
'uom_id': line['uom_id'],
'account_analytic_id': line['account_analytic_id'],
'tax_ids': tax_ids,
})
}
# We update the original line accordingly
line['price_unit'] = inv.currency_id.round(line['price_unit'] - diff_line['price_unit'])
line['price'] = inv.currency_id.round(line['quantity'] * line['price_unit'])
diff_res.append(diff_line)
return diff_res
return []
@@ -25,7 +25,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
})
self.test_product_category.property_account_creditor_price_difference_categ = self.price_dif_account.id
def _create_purchase(self, product, quantity=1.0):
def _create_purchase(self, product, date, quantity=1.0):
rslt = self.env['purchase.order'].create({
'partner_id': self.test_partner.id,
'currency_id': self.currency_two.id,
@@ -36,14 +36,14 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
'product_qty': quantity,
'product_uom': product.uom_po_id.id,
'price_unit': self.product_price_unit,
'date_planned': '2017-10-07',
'date_planned': date,
})],
'date_order': '2017-10-01',
'date_order': date,
})
rslt.button_confirm()
return rslt
def _create_invoice_for_po(self, purchase_order):
def _create_invoice_for_po(self, purchase_order, date):
account_receivable = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
rslt = self.env['account.invoice'].create({
'purchase_id': purchase_order.id,
@@ -52,8 +52,8 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
'currency_id': self.currency_two.id,
'name': 'vendor bill',
'type': 'in_invoice',
'date_invoice': '2017-12-31',
'date': '2017-12-31',
'date_invoice': date,
'date': date,
'account_id': account_receivable.id,
})
rslt.purchase_order_change()
@@ -63,11 +63,16 @@ 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)
purchase_order = self._create_purchase(test_product, '2018-01-01')
self._process_pickings(purchase_order.picking_ids)
invoice = self._create_invoice_for_po(purchase_order)
self.currency_rate.rate = 7.76435463
invoice = self._create_invoice_for_po(purchase_order, '2018-02-02')
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 7.76435463,
'name': '2018-02-01',
})
invoice.action_invoice_open()
picking = self.env['stock.picking'].search([('purchase_id','=',purchase_order.id)])
self.check_reconciliation(invoice, picking)
@@ -78,29 +83,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
def test_invoice_shipment(self):
""" Tests the case into which we make the invoice first, and then receive the goods.
"""
# First, we delete the currency rate defined in demo data for USD on June 6th.
# This is mandatory to ensure consistency of the data generated by the test,
# as stock pickings created from a PO are by design always created for
# the current date (there is no way forcing this), meaning that they
# always use today's exchange rate for their valuation.
# Despite the fact we rewrite the date of the valuation moves artificially,
# we cannot correct the debit and credit values, since the anglosaxon
# entries get automatically reconciled (and you cannot modify a reconciled entry).
# So, we have to make sure that "today"'s rate will always be the last rate we
# created in order to ensure complete control of the test.
self.env.ref('base.rateUSDbis').unlink()
# Create a PO and an invoice for it
test_product = self.test_product_order
purchase_order = self._create_purchase(test_product)
purchase_order = self._create_purchase(test_product, '2017-12-01')
invoice = self._create_invoice_for_po(purchase_order)
invoice = self._create_invoice_for_po(purchase_order, '2017-12-23')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)])
invoice_line.quantity = 1
# The currency rate changes
self.currency_rate = self.env['res.currency.rate'].create({
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 13.834739702,
@@ -114,7 +106,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
self.check_reconciliation(invoice, picking)
# The currency rate changes again
self.currency_rate = self.env['res.currency.rate'].create({
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 10.54739702,
@@ -133,7 +125,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
self._change_pickings_date(return_pick, '2018-01-13')
# The currency rate changes again
self.currency_rate = self.env['res.currency.rate'].create({
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 9.56564564,
@@ -158,25 +150,40 @@ 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, quantity=5.0)
purchase_order = self._create_purchase(test_product, '2017-01-01', quantity=5.0)
self._process_pickings(purchase_order.picking_ids, quantity=2.0)
picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order="id asc", limit=1)
invoice = self._create_invoice_for_po(purchase_order)
invoice = self._create_invoice_for_po(purchase_order, '2017-01-15')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)])
invoice_line.quantity = 3
self.currency_rate.rate = 7.76435463
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 7.76435463,
'name': '2017-02-01',
})
invoice.action_invoice_open()
self.check_reconciliation(invoice, picking, full_reconcile=False)
invoice2 = self._create_invoice_for_po(purchase_order)
invoice2 = self._create_invoice_for_po(purchase_order, '2017-02-15')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice2.id)])
invoice_line.quantity = 2
self.currency_rate.rate = 13.834739702
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 13.834739702,
'name': '2017-03-01',
})
invoice2.action_invoice_open()
self.check_reconciliation(invoice2, picking, full_reconcile=False)
self.currency_rate.rate = 12.195747002
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 12.195747002,
'name': '2017-04-01',
})
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)
@@ -4,6 +4,7 @@
import time
from datetime import datetime
from odoo.tests import Form
from odoo.tests.common import TransactionCase, tagged
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
@@ -412,3 +413,52 @@ class TestStockValuationWithCOA(AccountingTestCase):
# check the anglo saxon entries
price_diff_entry = self.env['account.move.line'].search([('account_id', '=', price_diff_account.id)])
self.assertEqual(price_diff_entry.credit, 100)
def test_anglosaxon_valuation(self):
self.env.user.company_id.anglo_saxon_accounting = True
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
# Create PO
po_form = Form(self.env['purchase.order'])
po_form.partner_id = self.partner_id
with po_form.order_line.new() as po_line:
po_line.product_id = self.product1
po_line.product_qty = 1
po_line.price_unit = 10.0
order = po_form.save()
order.button_confirm()
# Receive the goods
receipt = order.picking_ids[0]
receipt.move_lines.quantity_done = 1
receipt.button_validate()
# Create an invoice with a different price
invoice = self.env['account.invoice'].create({
'partner_id': order.partner_id.id,
'purchase_id': order.id,
'account_id': order.partner_id.property_account_payable_id.id,
'type': 'in_invoice',
})
invoice.purchase_order_change()
invoice.invoice_line_ids[0].price_unit = 15.0
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)])
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)])
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.")
@@ -30,7 +30,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
#set the invoice_policy to delivery to have an accurate COGS entry
self.test_product_delivery.invoice_policy = "delivery"
def _create_sale(self, product, quantity=1.0):
def _create_sale(self, product, date, quantity=1.0):
rslt = self.env['sale.order'].create({
'partner_id': self.test_partner.id,
'currency_id': self.currency_two.id,
@@ -42,18 +42,19 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
'product_uom': product.uom_po_id.id,
'price_unit': self.product_price_unit,
})],
'date_order': date,
})
rslt.action_confirm()
return rslt
def _create_invoice_for_so(self, sale_order, product):
def _create_invoice_for_so(self, sale_order, product, date):
rslt = self.env['account.invoice'].create({
'partner_id': self.test_partner.id,
'reference_type': 'none',
'currency_id': self.currency_two.id,
'name': 'customer invoice',
'type': 'out_invoice',
'date_invoice': time.strftime('%Y') + '-12-22',
'date_invoice': date,
'account_id': self.account_receivable.id,
'invoice_line_ids': [(0, 0, {
'name': 'test line',
@@ -93,11 +94,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
test_product = self.test_product_delivery
self._set_initial_stock_for_product(test_product)
sale_order = self._create_sale(test_product)
sale_order = self._create_sale(test_product, '2108-01-01')
self._process_pickings(sale_order.picking_ids)
invoice = self._create_invoice_for_so(sale_order, test_product)
self.currency_rate.rate = 9.87366352
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-12')
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 9.87366352,
'name': '2018-02-01',
})
invoice.action_invoice_open()
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)])
self.check_reconciliation(invoice, picking, operation='sale')
@@ -111,10 +117,15 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
self.test_product_delivery.standard_price = 13
self._set_initial_stock_for_product(test_product)
sale_order = self._create_sale(test_product)
sale_order = self._create_sale(test_product, '2018-01-01')
invoice = self._create_invoice_for_so(sale_order, test_product)
self.currency_rate.rate = 0.974784
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03')
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 0.974784,
'name': '2018-02-01',
})
invoice.action_invoice_open()
self._process_pickings(sale_order.picking_ids)
@@ -123,7 +134,12 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
self.check_reconciliation(invoice, picking, operation='sale')
#return the goods and refund the invoice
self.currency_rate.rate = 10.54739702
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 10.54739702,
'name': '2018-03-01',
})
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
@@ -132,7 +148,12 @@ 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.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 9.56564564,
'name': '2018-04-01',
})
refund_invoice_wiz = self.env['account.invoice.refund'].with_context(active_ids=[invoice.id]).create({
'description': 'test_invoice_shipment_refund',
'filter_refund': 'cancel',
@@ -148,26 +169,41 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
test_product = self.test_product_delivery
self._set_initial_stock_for_product(test_product)
sale_order = self._create_sale(test_product, quantity=5)
sale_order = self._create_sale(test_product, '2018-01-01', quantity=5)
self._process_pickings(sale_order.picking_ids, quantity=2.0)
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order="id asc", limit=1)
invoice = self._create_invoice_for_so(sale_order, test_product)
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)])
invoice_line.quantity = 3
self.currency_rate.rate = 7.76435463
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 7.76435463,
'name': '2018-02-01',
})
invoice.action_invoice_open()
self.check_reconciliation(invoice, picking, full_reconcile=False, operation='sale')
invoice2 = self._create_invoice_for_so(sale_order, test_product)
invoice2 = self._create_invoice_for_so(sale_order, test_product, '2018-03-12')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice2.id)])
invoice_line.quantity = 2
self.currency_rate.rate = 13.834739702
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 13.834739702,
'name': '2018-03-01',
})
invoice2.action_invoice_open()
self.check_reconciliation(invoice2, picking, full_reconcile=False, operation='sale')
self.currency_rate.rate = 12.195747002
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 12.195747002,
'name': '2018-04-01',
})
self._process_pickings(sale_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0)
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order='id desc', limit=1)
self.check_reconciliation(invoice2, picking, operation='sale')
@@ -58,12 +58,6 @@ class ValuationReconciliationTestCase(AccountingTestCase):
self.currency_one = self.company.currency_id
currency_two_name = 'USD' if self.currency_one.name != 'USD' else 'EUR'
self.currency_two = self.env['res.currency'].search([('name', '=', currency_two_name)])
self.currency_rate = self.env['res.currency.rate'].create({
'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({
'name': 'Test stock in',
@@ -127,3 +121,15 @@ class ValuationReconciliationTestCase(AccountingTestCase):
})
self.product_price_unit = 66.0
# We delete the currency rate defined in demo data for USD on June 6th.
# This is mandatory to ensure consistency of the data generated by the test,
# as stock pickings created from a PO are by design always created for
# the current date (there is no way forcing this), meaning that they
# always use today's exchange rate for their valuation.
# Despite the fact we rewrite the date of the valuation moves artificially,
# we cannot correct the debit and credit values, since the anglosaxon
# entries get automatically reconciled (and you cannot modify a reconciled entry).
# So, we have to make sure that "today"'s rate will always be the last rate we
# created in order to ensure complete control of the test.
self.env.ref('base.rateUSDbis').unlink()