[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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user