Currently the standard price for products with FIFO as cost_method, show the price of the next product to go out. It could be confusing on the report page because the standard price * quantity is different than the stock value. Another issue, is the margin, they are only computed on the last piece even if we sell multiple ones. For example: MOVE IN 5 units @ 10$ MOVE IN 5 units @ 20$ If we do an order for 10 units at 30$ each we will have a margin of 200$ (30$ - 10$)*10. And the report page will be 10*10$ --> 150$. The purpose of this task is to have always a report correct. For the margin it will not be always true because it's depending the number of pieces that we sell. In the above example: - if we sell 9 units, the margin will be better with this patch 135$/130$ with this commit and 90$/130$ without - but if we sell 6 units it will be closer to reality with the old patch. 90$/70$ and without 60$/70$ It has other implications: the inventory adjustement and edition of moves will now takes the AVCO values and not the next to go out. With the upper example if we add 10 units following an inventory adjustement (10 * `standard_price`): - With this commit: 150$ - Current behavior: 100$ Globally it will be closer to reality for products with a price having a lot of variance and it won't change for the other. Task-2918885 closes odoo/odoo#108468 Signed-off-by: William Henrotin (whe) <whe@odoo.com>
1146 lines
52 KiB
Python
1146 lines
52 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
""" Implementation of "INVENTORY VALUATION TESTS (With valuation layers)" spreadsheet. """
|
|
|
|
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
|
from odoo.addons.stock_account.tests.test_stockvaluation import _create_accounting_data
|
|
from odoo.tests import Form, tagged
|
|
from odoo.tests.common import TransactionCase
|
|
|
|
|
|
class TestStockValuationCommon(TransactionCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestStockValuationCommon, cls).setUpClass()
|
|
cls.stock_location = cls.env.ref('stock.stock_location_stock')
|
|
cls.customer_location = cls.env.ref('stock.stock_location_customers')
|
|
cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
|
|
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
|
|
cls.product1 = cls.env['product.product'].create({
|
|
'name': 'product1',
|
|
'type': 'product',
|
|
'categ_id': cls.env.ref('product.product_category_all').id,
|
|
})
|
|
cls.picking_type_in = cls.env.ref('stock.picking_type_in')
|
|
cls.picking_type_out = cls.env.ref('stock.picking_type_out')
|
|
cls.env.ref('base.EUR').active = True
|
|
|
|
def setUp(self):
|
|
super(TestStockValuationCommon, self).setUp()
|
|
# Counter automatically incremented by `_make_in_move` and `_make_out_move`.
|
|
self.days = 0
|
|
|
|
def _make_in_move(self, product, quantity, unit_cost=None, create_picking=False, loc_dest=None, pick_type=None):
|
|
""" Helper to create and validate a receipt move.
|
|
"""
|
|
unit_cost = unit_cost or product.standard_price
|
|
loc_dest = loc_dest or self.stock_location
|
|
pick_type = pick_type or self.picking_type_in
|
|
in_move = self.env['stock.move'].create({
|
|
'name': 'in %s units @ %s per unit' % (str(quantity), str(unit_cost)),
|
|
'product_id': product.id,
|
|
'location_id': self.supplier_location.id,
|
|
'location_dest_id': loc_dest.id,
|
|
'product_uom': self.uom_unit.id,
|
|
'product_uom_qty': quantity,
|
|
'price_unit': unit_cost,
|
|
'picking_type_id': pick_type.id,
|
|
})
|
|
|
|
if create_picking:
|
|
picking = self.env['stock.picking'].create({
|
|
'picking_type_id': in_move.picking_type_id.id,
|
|
'location_id': in_move.location_id.id,
|
|
'location_dest_id': in_move.location_dest_id.id,
|
|
})
|
|
in_move.write({'picking_id': picking.id})
|
|
|
|
in_move._action_confirm()
|
|
in_move._action_assign()
|
|
in_move.move_line_ids.qty_done = quantity
|
|
in_move._action_done()
|
|
|
|
self.days += 1
|
|
return in_move.with_context(svl=True)
|
|
|
|
def _make_out_move(self, product, quantity, force_assign=None, create_picking=False, loc_src=None, pick_type=None):
|
|
""" Helper to create and validate a delivery move.
|
|
"""
|
|
loc_src = loc_src or self.stock_location
|
|
pick_type = pick_type or self.picking_type_out
|
|
out_move = self.env['stock.move'].create({
|
|
'name': 'out %s units' % str(quantity),
|
|
'product_id': product.id,
|
|
'location_id': loc_src.id,
|
|
'location_dest_id': self.customer_location.id,
|
|
'product_uom': self.uom_unit.id,
|
|
'product_uom_qty': quantity,
|
|
'picking_type_id': pick_type.id,
|
|
})
|
|
|
|
if create_picking:
|
|
picking = self.env['stock.picking'].create({
|
|
'picking_type_id': out_move.picking_type_id.id,
|
|
'location_id': out_move.location_id.id,
|
|
'location_dest_id': out_move.location_dest_id.id,
|
|
})
|
|
out_move.write({'picking_id': picking.id})
|
|
|
|
out_move._action_confirm()
|
|
out_move._action_assign()
|
|
if force_assign:
|
|
self.env['stock.move.line'].create({
|
|
'move_id': out_move.id,
|
|
'product_id': out_move.product_id.id,
|
|
'product_uom_id': out_move.product_uom.id,
|
|
'location_id': out_move.location_id.id,
|
|
'location_dest_id': out_move.location_dest_id.id,
|
|
})
|
|
out_move.move_line_ids.qty_done = quantity
|
|
out_move._action_done()
|
|
|
|
self.days += 1
|
|
return out_move.with_context(svl=True)
|
|
|
|
def _make_dropship_move(self, product, quantity, unit_cost=None):
|
|
dropshipped = self.env['stock.move'].create({
|
|
'name': 'dropship %s units' % str(quantity),
|
|
'product_id': product.id,
|
|
'location_id': self.supplier_location.id,
|
|
'location_dest_id': self.customer_location.id,
|
|
'product_uom': self.uom_unit.id,
|
|
'product_uom_qty': quantity,
|
|
'picking_type_id': self.picking_type_out.id,
|
|
})
|
|
if unit_cost:
|
|
dropshipped.price_unit = unit_cost
|
|
dropshipped._action_confirm()
|
|
dropshipped._action_assign()
|
|
dropshipped.move_line_ids.qty_done = quantity
|
|
dropshipped._action_done()
|
|
return dropshipped
|
|
|
|
def _make_return(self, move, quantity_to_return):
|
|
stock_return_picking = Form(self.env['stock.return.picking']\
|
|
.with_context(active_ids=[move.picking_id.id], active_id=move.picking_id.id, active_model='stock.picking'))
|
|
stock_return_picking = stock_return_picking.save()
|
|
stock_return_picking.product_return_moves.quantity = quantity_to_return
|
|
stock_return_picking_action = stock_return_picking.create_returns()
|
|
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
|
return_pick.move_ids[0].move_line_ids[0].qty_done = quantity_to_return
|
|
return_pick._action_done()
|
|
return return_pick.move_ids
|
|
|
|
|
|
class TestStockValuationStandard(TestStockValuationCommon):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
|
cls.product1.product_tmpl_id.standard_price = 10
|
|
|
|
def test_normal_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
move2 = self._make_in_move(self.product1, 10)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
|
|
self.assertEqual(self.product1.value_svl, 50)
|
|
self.assertEqual(self.product1.quantity_svl, 5)
|
|
|
|
def test_change_in_past_increase_in_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
move2 = self._make_in_move(self.product1, 10)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
move1.move_line_ids.qty_done = 15
|
|
|
|
self.assertEqual(self.product1.value_svl, 100)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
|
|
def test_change_in_past_decrease_in_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
move2 = self._make_in_move(self.product1, 10)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
move1.move_line_ids.qty_done = 5
|
|
|
|
self.assertEqual(self.product1.value_svl, 0)
|
|
self.assertEqual(self.product1.quantity_svl, 0)
|
|
|
|
def test_change_in_past_add_ml_in_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
move2 = self._make_in_move(self.product1, 10)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
self.env['stock.move.line'].create({
|
|
'move_id': move1.id,
|
|
'product_id': move1.product_id.id,
|
|
'qty_done': 5,
|
|
'product_uom_id': move1.product_uom.id,
|
|
'location_id': move1.location_id.id,
|
|
'location_dest_id': move1.location_dest_id.id,
|
|
})
|
|
|
|
self.assertEqual(self.product1.value_svl, 100)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
|
|
def test_change_in_past_increase_out_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
move2 = self._make_out_move(self.product1, 1)
|
|
move2.move_line_ids.qty_done = 5
|
|
|
|
self.assertEqual(self.product1.value_svl, 50)
|
|
self.assertEqual(self.product1.quantity_svl, 5)
|
|
|
|
def test_change_in_past_decrease_out_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
move2 = self._make_out_move(self.product1, 5)
|
|
move2.move_line_ids.qty_done = 1
|
|
|
|
self.assertEqual(self.product1.value_svl, 90)
|
|
self.assertEqual(self.product1.quantity_svl, 9)
|
|
|
|
def test_change_standard_price_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
move2 = self._make_in_move(self.product1, 10)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
|
|
# change cost from 10 to 15
|
|
self.product1.standard_price = 15.0
|
|
|
|
self.assertEqual(self.product1.value_svl, 75)
|
|
self.assertEqual(self.product1.quantity_svl, 5)
|
|
self.assertEqual(self.product1.stock_valuation_layer_ids.sorted()[-1].description, 'Product value manually modified (from 10.0 to 15.0)')
|
|
|
|
def test_negative_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
move2 = self._make_out_move(self.product1, 15)
|
|
self.env['stock.move.line'].create({
|
|
'move_id': move1.id,
|
|
'product_id': move1.product_id.id,
|
|
'qty_done': 10,
|
|
'product_uom_id': move1.product_uom.id,
|
|
'location_id': move1.location_id.id,
|
|
'location_dest_id': move1.location_dest_id.id,
|
|
})
|
|
|
|
self.assertEqual(self.product1.value_svl, 50)
|
|
self.assertEqual(self.product1.quantity_svl, 5)
|
|
|
|
def test_dropship_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_dropship_move(self.product1, 10)
|
|
|
|
valuation_layers = self.product1.stock_valuation_layer_ids
|
|
self.assertEqual(len(valuation_layers), 2)
|
|
self.assertEqual(valuation_layers[0].value, 100)
|
|
self.assertEqual(valuation_layers[1].value, -100)
|
|
self.assertEqual(self.product1.value_svl, 0)
|
|
self.assertEqual(self.product1.quantity_svl, 0)
|
|
|
|
def test_change_in_past_increase_dropship_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_dropship_move(self.product1, 10)
|
|
move1.move_line_ids.qty_done = 15
|
|
|
|
valuation_layers = self.product1.stock_valuation_layer_ids
|
|
self.assertEqual(len(valuation_layers), 4)
|
|
self.assertEqual(valuation_layers[0].value, 100)
|
|
self.assertEqual(valuation_layers[1].value, -100)
|
|
self.assertEqual(valuation_layers[2].value, 50)
|
|
self.assertEqual(valuation_layers[3].value, -50)
|
|
self.assertEqual(self.product1.value_svl, 0)
|
|
self.assertEqual(self.product1.quantity_svl, 0)
|
|
|
|
def test_empty_stock_move_valorisation(self):
|
|
product1 = self.env['product.product'].create({
|
|
'name': 'p1',
|
|
'type': 'product',
|
|
})
|
|
product2 = self.env['product.product'].create({
|
|
'name': 'p2',
|
|
'type': 'product',
|
|
})
|
|
picking = self.env['stock.picking'].create({
|
|
'picking_type_id': self.picking_type_in.id,
|
|
'location_id': self.supplier_location.id,
|
|
'location_dest_id': self.stock_location.id,
|
|
})
|
|
for product in (product1, product2):
|
|
product.standard_price = 10
|
|
in_move = self.env['stock.move'].create({
|
|
'name': 'in %s units @ %s per unit' % (2, str(10)),
|
|
'product_id': product.id,
|
|
'location_id': self.supplier_location.id,
|
|
'location_dest_id': self.stock_location.id,
|
|
'product_uom': self.uom_unit.id,
|
|
'product_uom_qty': 2,
|
|
'price_unit': 10,
|
|
'picking_type_id': self.picking_type_in.id,
|
|
'picking_id': picking.id
|
|
})
|
|
|
|
picking.action_confirm()
|
|
# set quantity done only on one move
|
|
in_move.move_line_ids.qty_done = 2
|
|
res_dict = picking.button_validate()
|
|
wizard = self.env[(res_dict.get('res_model'))].with_context(res_dict.get('context')).browse(res_dict.get('res_id'))
|
|
res_dict_for_back_order = wizard.process()
|
|
|
|
self.assertTrue(product2.stock_valuation_layer_ids)
|
|
self.assertFalse(product1.stock_valuation_layer_ids)
|
|
|
|
def test_currency_precision_and_standard_svl_value(self):
|
|
currency = self.env['res.currency'].create({
|
|
'name': 'Odoo',
|
|
'symbol': 'O',
|
|
'rounding': 1,
|
|
})
|
|
new_company = self.env['res.company'].create({
|
|
'name': 'Super Company',
|
|
'currency_id': currency.id,
|
|
})
|
|
|
|
old_company = self.env.user.company_id
|
|
try:
|
|
self.env.user.company_id = new_company
|
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', new_company.id)])
|
|
product = self.product1.with_company(new_company)
|
|
product.standard_price = 3
|
|
|
|
self._make_in_move(product, 0.5, loc_dest=warehouse.lot_stock_id, pick_type=warehouse.in_type_id)
|
|
self._make_out_move(product, 0.5, loc_src=warehouse.lot_stock_id, pick_type=warehouse.out_type_id)
|
|
|
|
self.assertEqual(product.value_svl, 0.0)
|
|
finally:
|
|
self.env.user.company_id = old_company
|
|
|
|
class TestStockValuationAVCO(TestStockValuationCommon):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
|
|
|
|
def test_normal_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
self.assertEqual(self.product1.standard_price, 10)
|
|
self.assertEqual(move1.stock_valuation_layer_ids.value, 100)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
self.assertEqual(self.product1.standard_price, 15)
|
|
self.assertEqual(move2.stock_valuation_layer_ids.value, 200)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
self.assertEqual(self.product1.standard_price, 15)
|
|
self.assertEqual(move3.stock_valuation_layer_ids.value, -225)
|
|
|
|
self.assertEqual(self.product1.value_svl, 75)
|
|
self.assertEqual(self.product1.quantity_svl, 5)
|
|
|
|
def test_change_in_past_increase_in_1(self):
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
move1.move_line_ids.qty_done = 15
|
|
|
|
self.assertEqual(self.product1.value_svl, 125)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
|
|
def test_change_in_past_decrease_in_1(self):
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
move1.move_line_ids.qty_done = 5
|
|
|
|
self.assertEqual(self.product1.value_svl, 0)
|
|
self.assertEqual(self.product1.quantity_svl, 0)
|
|
|
|
def test_change_in_past_add_ml_in_1(self):
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
self.env['stock.move.line'].create({
|
|
'move_id': move1.id,
|
|
'product_id': move1.product_id.id,
|
|
'qty_done': 5,
|
|
'product_uom_id': move1.product_uom.id,
|
|
'location_id': move1.location_id.id,
|
|
'location_dest_id': move1.location_dest_id.id,
|
|
})
|
|
|
|
self.assertEqual(self.product1.value_svl, 125)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
self.assertEqual(self.product1.standard_price, 12.5)
|
|
|
|
def test_change_in_past_add_move_in_1(self):
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10, create_picking=True)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
self.env['stock.move.line'].create({
|
|
'product_id': move1.product_id.id,
|
|
'qty_done': 5,
|
|
'product_uom_id': move1.product_uom.id,
|
|
'location_id': move1.location_id.id,
|
|
'location_dest_id': move1.location_dest_id.id,
|
|
'state': 'done',
|
|
'picking_id': move1.picking_id.id,
|
|
})
|
|
|
|
self.assertEqual(self.product1.value_svl, 150)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
self.assertEqual(self.product1.standard_price, 15)
|
|
|
|
def test_change_in_past_increase_out_1(self):
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
move3.move_line_ids.qty_done = 20
|
|
|
|
self.assertEqual(self.product1.value_svl, 0)
|
|
self.assertEqual(self.product1.quantity_svl, 0)
|
|
self.assertEqual(self.product1.standard_price, 15)
|
|
|
|
def test_change_in_past_decrease_out_1(self):
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
move3.move_line_ids.qty_done = 10
|
|
|
|
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 10)
|
|
self.assertEqual(self.product1.value_svl, 150)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
self.assertEqual(self.product1.standard_price, 15)
|
|
|
|
def test_negative_1(self):
|
|
""" Ensures that, in AVCO, the `remaining_qty` field is computed and the vacuum is ran
|
|
when necessary.
|
|
"""
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 30)
|
|
self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, -10)
|
|
move4 = self._make_in_move(self.product1, 10, unit_cost=30)
|
|
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 0)
|
|
move5 = self._make_in_move(self.product1, 10, unit_cost=40)
|
|
|
|
self.assertEqual(self.product1.value_svl, 400)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
|
|
def test_negative_2(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
self.product1.standard_price = 10
|
|
move1 = self._make_out_move(self.product1, 1, force_assign=True)
|
|
move2 = self._make_in_move(self.product1, 1, unit_cost=15)
|
|
|
|
self.assertEqual(self.product1.value_svl, 0)
|
|
self.assertEqual(self.product1.quantity_svl, 0)
|
|
|
|
def test_negative_3(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_out_move(self.product1, 2, force_assign=True)
|
|
self.assertEqual(move1.stock_valuation_layer_ids.value, 0)
|
|
move2 = self._make_in_move(self.product1, 20, unit_cost=3.33)
|
|
self.assertEqual(move1.stock_valuation_layer_ids[1].value, -6.66)
|
|
|
|
self.assertEqual(self.product1.standard_price, 3.33)
|
|
self.assertEqual(self.product1.value_svl, 59.94)
|
|
self.assertEqual(self.product1.quantity_svl, 18)
|
|
|
|
def test_return_receipt_1(self):
|
|
move1 = self._make_in_move(self.product1, 1, unit_cost=10, create_picking=True)
|
|
move2 = self._make_in_move(self.product1, 1, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 1)
|
|
move4 = self._make_return(move1, 1)
|
|
|
|
self.assertEqual(self.product1.value_svl, 0)
|
|
self.assertEqual(self.product1.quantity_svl, 0)
|
|
self.assertEqual(self.product1.standard_price, 15)
|
|
|
|
def test_return_delivery_1(self):
|
|
move1 = self._make_in_move(self.product1, 1, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 1, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 1, create_picking=True)
|
|
move4 = self._make_return(move3, 1)
|
|
|
|
self.assertEqual(self.product1.value_svl, 30)
|
|
self.assertEqual(self.product1.quantity_svl, 2)
|
|
self.assertEqual(self.product1.standard_price, 15)
|
|
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 2)
|
|
|
|
def test_rereturn_receipt_1(self):
|
|
move1 = self._make_in_move(self.product1, 1, unit_cost=10, create_picking=True)
|
|
move2 = self._make_in_move(self.product1, 1, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 1)
|
|
move4 = self._make_return(move1, 1) # -15, current avco
|
|
move5 = self._make_return(move4, 1) # +10, original move's price unit
|
|
|
|
self.assertEqual(self.product1.value_svl, 15)
|
|
self.assertEqual(self.product1.quantity_svl, 1)
|
|
self.assertEqual(self.product1.standard_price, 15)
|
|
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 1)
|
|
|
|
def test_rereturn_delivery_1(self):
|
|
move1 = self._make_in_move(self.product1, 1, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 1, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 1, create_picking=True)
|
|
move4 = self._make_return(move3, 1)
|
|
move5 = self._make_return(move4, 1)
|
|
|
|
self.assertEqual(self.product1.value_svl, 15)
|
|
self.assertEqual(self.product1.quantity_svl, 1)
|
|
self.assertEqual(self.product1.standard_price, 15)
|
|
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 1)
|
|
|
|
def test_dropship_1(self):
|
|
move1 = self._make_in_move(self.product1, 1, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 1, unit_cost=20)
|
|
move3 = self._make_dropship_move(self.product1, 1, unit_cost=10)
|
|
|
|
self.assertEqual(self.product1.value_svl, 30)
|
|
self.assertEqual(self.product1.quantity_svl, 2)
|
|
self.assertEqual(self.product1.standard_price, 15)
|
|
|
|
def test_rounding_slv_1(self):
|
|
self._make_in_move(self.product1, 1, unit_cost=1.00)
|
|
self._make_in_move(self.product1, 1, unit_cost=1.00)
|
|
self._make_in_move(self.product1, 1, unit_cost=1.01)
|
|
|
|
self.assertAlmostEqual(self.product1.value_svl, 3.01)
|
|
|
|
move_out = self._make_out_move(self.product1, 3, create_picking=True)
|
|
|
|
self.assertIn('Rounding Adjustment: -0.01', move_out.stock_valuation_layer_ids.description)
|
|
|
|
self.assertEqual(self.product1.value_svl, 0)
|
|
self.assertEqual(self.product1.quantity_svl, 0)
|
|
self.assertEqual(self.product1.standard_price, 1.00)
|
|
|
|
def test_rounding_slv_2(self):
|
|
self._make_in_move(self.product1, 1, unit_cost=1.02)
|
|
self._make_in_move(self.product1, 1, unit_cost=1.00)
|
|
self._make_in_move(self.product1, 1, unit_cost=1.00)
|
|
|
|
self.assertAlmostEqual(self.product1.value_svl, 3.02)
|
|
|
|
move_out = self._make_out_move(self.product1, 3, create_picking=True)
|
|
|
|
self.assertIn('Rounding Adjustment: +0.01', move_out.stock_valuation_layer_ids.description)
|
|
|
|
self.assertEqual(self.product1.value_svl, 0)
|
|
self.assertEqual(self.product1.quantity_svl, 0)
|
|
self.assertEqual(self.product1.standard_price, 1.01)
|
|
|
|
def test_return_delivery_2(self):
|
|
self.product1.write({"standard_price": 1})
|
|
move1 = self._make_out_move(self.product1, 10, create_picking=True, force_assign=True)
|
|
self._make_in_move(self.product1, 10, unit_cost=2)
|
|
self._make_return(move1, 10)
|
|
|
|
self.assertEqual(self.product1.value_svl, 20)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
self.assertEqual(self.product1.standard_price, 2)
|
|
|
|
|
|
class TestStockValuationFIFO(TestStockValuationCommon):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
|
|
|
def test_normal_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
|
|
self.assertEqual(self.product1.value_svl, 100)
|
|
self.assertEqual(self.product1.quantity_svl, 5)
|
|
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 5)
|
|
|
|
def test_negative_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 30)
|
|
self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, -10)
|
|
move4 = self._make_in_move(self.product1, 10, unit_cost=30)
|
|
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 0)
|
|
move5 = self._make_in_move(self.product1, 10, unit_cost=40)
|
|
|
|
self.assertEqual(self.product1.value_svl, 400)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
|
|
def test_change_in_past_decrease_in_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_in_move(self.product1, 20, unit_cost=10)
|
|
move2 = self._make_out_move(self.product1, 10)
|
|
move1.move_line_ids.qty_done = 10
|
|
|
|
self.assertEqual(self.product1.value_svl, 0)
|
|
self.assertEqual(self.product1.quantity_svl, 0)
|
|
|
|
def test_change_in_past_decrease_in_2(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_in_move(self.product1, 20, unit_cost=10)
|
|
move2 = self._make_out_move(self.product1, 10)
|
|
move3 = self._make_out_move(self.product1, 10)
|
|
move1.move_line_ids.qty_done = 10
|
|
move4 = self._make_in_move(self.product1, 20, unit_cost=15)
|
|
|
|
self.assertEqual(self.product1.value_svl, 150)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
|
|
def test_change_in_past_increase_in_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=15)
|
|
move3 = self._make_out_move(self.product1, 20)
|
|
move1.move_line_ids.qty_done = 20
|
|
|
|
self.assertEqual(self.product1.value_svl, 100)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
|
|
def test_change_in_past_increase_in_2(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=12)
|
|
move3 = self._make_out_move(self.product1, 15)
|
|
move4 = self._make_out_move(self.product1, 20)
|
|
move5 = self._make_in_move(self.product1, 100, unit_cost=15)
|
|
move1.move_line_ids.qty_done = 20
|
|
|
|
self.assertEqual(self.product1.value_svl, 1375)
|
|
self.assertEqual(self.product1.quantity_svl, 95)
|
|
|
|
def test_change_in_past_increase_out_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_in_move(self.product1, 20, unit_cost=10)
|
|
move2 = self._make_out_move(self.product1, 10)
|
|
move3 = self._make_in_move(self.product1, 20, unit_cost=15)
|
|
move2.move_line_ids.qty_done = 25
|
|
|
|
self.assertEqual(self.product1.value_svl, 225)
|
|
self.assertEqual(self.product1.quantity_svl, 15)
|
|
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 15)
|
|
|
|
def test_change_in_past_decrease_out_1(self):
|
|
""" Decrease the qty_done of an outgoing stock.move.line will act like
|
|
an inventory adjustement and not a return. It will take the standard price
|
|
of the product in order to set the value and not the move's layers.
|
|
"""
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_in_move(self.product1, 20, unit_cost=10)
|
|
move2 = self._make_out_move(self.product1, 15)
|
|
move3 = self._make_in_move(self.product1, 20, unit_cost=15)
|
|
move2.move_line_ids.qty_done = 5
|
|
|
|
self.assertEqual(self.product1.value_svl, 490)
|
|
self.assertEqual(self.product1.quantity_svl, 35)
|
|
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 35)
|
|
|
|
def test_change_in_past_add_ml_out_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_in_move(self.product1, 20, unit_cost=10)
|
|
move2 = self._make_out_move(self.product1, 10)
|
|
move3 = self._make_in_move(self.product1, 20, unit_cost=15)
|
|
self.env['stock.move.line'].create({
|
|
'move_id': move2.id,
|
|
'product_id': move2.product_id.id,
|
|
'qty_done': 5,
|
|
'product_uom_id': move2.product_uom.id,
|
|
'location_id': move2.location_id.id,
|
|
'location_dest_id': move2.location_dest_id.id,
|
|
})
|
|
|
|
self.assertEqual(self.product1.value_svl, 350)
|
|
self.assertEqual(self.product1.quantity_svl, 25)
|
|
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 25)
|
|
|
|
def test_return_delivery_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_out_move(self.product1, 10, create_picking=True)
|
|
move3 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move4 = self._make_return(move2, 10)
|
|
|
|
self.assertEqual(self.product1.value_svl, 300)
|
|
self.assertEqual(self.product1.quantity_svl, 20)
|
|
|
|
def test_return_receipt_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10, create_picking=True)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_return(move1, 2)
|
|
|
|
self.assertEqual(self.product1.value_svl, 280)
|
|
self.assertEqual(self.product1.quantity_svl, 18)
|
|
|
|
def test_rereturn_receipt_1(self):
|
|
move1 = self._make_in_move(self.product1, 1, unit_cost=10, create_picking=True)
|
|
move2 = self._make_in_move(self.product1, 1, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 1)
|
|
move4 = self._make_return(move1, 1)
|
|
move5 = self._make_return(move4, 1)
|
|
|
|
self.assertEqual(self.product1.value_svl, 20)
|
|
self.assertEqual(self.product1.quantity_svl, 1)
|
|
|
|
def test_rereturn_delivery_1(self):
|
|
move1 = self._make_in_move(self.product1, 1, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 1, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 1, create_picking=True)
|
|
move4 = self._make_return(move3, 1)
|
|
move5 = self._make_return(move4, 1)
|
|
|
|
self.assertEqual(self.product1.value_svl, 10)
|
|
self.assertEqual(self.product1.quantity_svl, 1)
|
|
|
|
def test_dropship_1(self):
|
|
move1 = self._make_in_move(self.product1, 1, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 1, unit_cost=20)
|
|
move3 = self._make_dropship_move(self.product1, 1, unit_cost=10)
|
|
|
|
self.assertEqual(self.product1.value_svl, 30)
|
|
self.assertEqual(self.product1.quantity_svl, 2)
|
|
self.assertAlmostEqual(self.product1.standard_price, 15)
|
|
|
|
def test_return_delivery_2(self):
|
|
self._make_in_move(self.product1, 1, unit_cost=10)
|
|
self.product1.standard_price = 0
|
|
self._make_in_move(self.product1, 1, unit_cost=0)
|
|
|
|
self._make_out_move(self.product1, 1)
|
|
out_move02 = self._make_out_move(self.product1, 1, create_picking=True)
|
|
|
|
returned = self._make_return(out_move02, 1)
|
|
self.assertEqual(returned.stock_valuation_layer_ids.value, 0)
|
|
|
|
def test_return_delivery_3(self):
|
|
self.product1.write({"standard_price": 1})
|
|
move1 = self._make_out_move(self.product1, 10, create_picking=True, force_assign=True)
|
|
self._make_in_move(self.product1, 10, unit_cost=2)
|
|
self._make_return(move1, 10)
|
|
|
|
self.assertEqual(self.product1.value_svl, 20)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
|
|
def test_currency_precision_and_fifo_svl_value(self):
|
|
currency = self.env['res.currency'].create({
|
|
'name': 'Odoo',
|
|
'symbol': 'O',
|
|
'rounding': 1,
|
|
})
|
|
new_company = self.env['res.company'].create({
|
|
'name': 'Super Company',
|
|
'currency_id': currency.id,
|
|
})
|
|
|
|
old_company = self.env.user.company_id
|
|
try:
|
|
self.env.user.company_id = new_company
|
|
product = self.product1.with_company(new_company)
|
|
product.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', new_company.id)])
|
|
|
|
self._make_in_move(product, 0.5, loc_dest=warehouse.lot_stock_id, pick_type=warehouse.in_type_id, unit_cost=3)
|
|
self._make_out_move(product, 0.5, loc_src=warehouse.lot_stock_id, pick_type=warehouse.out_type_id)
|
|
|
|
self.assertEqual(product.value_svl, 0.0)
|
|
finally:
|
|
self.env.user.company_id = old_company
|
|
|
|
|
|
class TestStockValuationChangeCostMethod(TestStockValuationCommon):
|
|
def test_standard_to_fifo_1(self):
|
|
""" The accounting impact of this cost method change is neutral.
|
|
"""
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
self.product1.product_tmpl_id.standard_price = 10
|
|
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
move2 = self._make_in_move(self.product1, 10)
|
|
move3 = self._make_out_move(self.product1, 1)
|
|
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
|
self.assertEqual(self.product1.value_svl, 190)
|
|
self.assertEqual(self.product1.quantity_svl, 19)
|
|
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids), 5)
|
|
for svl in self.product1.stock_valuation_layer_ids.sorted()[-2:]:
|
|
self.assertEqual(svl.description, 'Costing method change for product category All: from standard to fifo.')
|
|
|
|
def test_standard_to_fifo_2(self):
|
|
""" We want the same result as `test_standard_to_fifo_1` but by changing the category of
|
|
`self.product1` to another one, not changing the current one.
|
|
"""
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
self.product1.product_tmpl_id.standard_price = 10
|
|
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
move2 = self._make_in_move(self.product1, 10)
|
|
move3 = self._make_out_move(self.product1, 1)
|
|
|
|
cat2 = self.env['product.category'].create({'name': 'fifo'})
|
|
cat2.property_cost_method = 'fifo'
|
|
self.product1.product_tmpl_id.categ_id = cat2
|
|
self.assertEqual(self.product1.value_svl, 190)
|
|
self.assertEqual(self.product1.quantity_svl, 19)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids), 5)
|
|
|
|
def test_avco_to_fifo(self):
|
|
""" The accounting impact of this cost method change is neutral.
|
|
"""
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 1)
|
|
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
|
self.assertEqual(self.product1.value_svl, 285)
|
|
self.assertEqual(self.product1.quantity_svl, 19)
|
|
|
|
def test_fifo_to_standard(self):
|
|
""" The accounting impact of this cost method change is not neutral as we will use the last
|
|
fifo price as the new standard price.
|
|
"""
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 1)
|
|
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
|
self.assertEqual(self.product1.value_svl, 289.94)
|
|
self.assertEqual(self.product1.quantity_svl, 19)
|
|
|
|
def test_fifo_to_avco(self):
|
|
""" The accounting impact of this cost method change is not neutral as we will use the last
|
|
fifo price as the new AVCO.
|
|
"""
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 1)
|
|
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
|
|
self.assertEqual(self.product1.value_svl, 289.94)
|
|
self.assertEqual(self.product1.quantity_svl, 19)
|
|
|
|
def test_avco_to_standard(self):
|
|
""" The accounting impact of this cost method change is neutral.
|
|
"""
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
|
|
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
|
move3 = self._make_out_move(self.product1, 1)
|
|
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
|
self.assertEqual(self.product1.value_svl, 285)
|
|
self.assertEqual(self.product1.quantity_svl, 19)
|
|
|
|
def test_standard_to_avco(self):
|
|
""" The accounting impact of this cost method change is neutral.
|
|
"""
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
self.product1.product_tmpl_id.standard_price = 10
|
|
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
move2 = self._make_in_move(self.product1, 10)
|
|
move3 = self._make_out_move(self.product1, 1)
|
|
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
|
|
self.assertEqual(self.product1.value_svl, 190)
|
|
self.assertEqual(self.product1.quantity_svl, 19)
|
|
|
|
|
|
class TestStockValuationChangeValuation(TestStockValuationCommon):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestStockValuationChangeValuation, cls).setUpClass()
|
|
cls.stock_input_account, cls.stock_output_account, cls.stock_valuation_account, cls.expense_account, cls.stock_journal = _create_accounting_data(cls.env)
|
|
cls.product1.categ_id.property_valuation = 'real_time'
|
|
cls.product1.write({
|
|
'property_account_expense_id': cls.expense_account.id,
|
|
})
|
|
cls.product1.categ_id.write({
|
|
'property_stock_account_input_categ_id': cls.stock_input_account.id,
|
|
'property_stock_account_output_categ_id': cls.stock_output_account.id,
|
|
'property_stock_valuation_account_id': cls.stock_valuation_account.id,
|
|
'property_stock_journal': cls.stock_journal.id,
|
|
})
|
|
|
|
def test_standard_manual_to_auto_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
self.product1.product_tmpl_id.standard_price = 10
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
|
|
self.assertEqual(self.product1.value_svl, 100)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 0)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids), 1)
|
|
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
|
|
|
|
self.assertEqual(self.product1.value_svl, 100)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
# An accounting entry should only be created for the replenish now that the category is perpetual.
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 1)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids), 3)
|
|
for svl in self.product1.stock_valuation_layer_ids.sorted()[-2:]:
|
|
self.assertEqual(svl.description, 'Valuation method change for product category All: from manual_periodic to real_time.')
|
|
|
|
def test_standard_manual_to_auto_2(self):
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
self.product1.product_tmpl_id.standard_price = 10
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
|
|
self.assertEqual(self.product1.value_svl, 100)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 0)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids), 1)
|
|
|
|
cat2 = self.env['product.category'].create({'name': 'standard auto'})
|
|
cat2.property_cost_method = 'standard'
|
|
cat2.property_valuation = 'real_time'
|
|
cat2.write({
|
|
'property_stock_account_input_categ_id': self.stock_input_account.id,
|
|
'property_stock_account_output_categ_id': self.stock_output_account.id,
|
|
'property_stock_valuation_account_id': self.stock_valuation_account.id,
|
|
'property_stock_journal': self.stock_journal.id,
|
|
})
|
|
|
|
# Try to change the product category with a `default_detailed_type` key in the context and
|
|
# check it doesn't break the account move generation.
|
|
self.product1.with_context(default_detailed_type='product').categ_id = cat2
|
|
self.assertEqual(self.product1.categ_id, cat2)
|
|
|
|
self.assertEqual(self.product1.value_svl, 100)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
# An accounting entry should only be created for the replenish now that the category is perpetual.
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 1)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids), 3)
|
|
|
|
def test_standard_auto_to_manual_1(self):
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
|
|
self.product1.product_tmpl_id.standard_price = 10
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
|
|
self.assertEqual(self.product1.value_svl, 100)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 1)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids), 1)
|
|
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
|
|
|
|
self.assertEqual(self.product1.value_svl, 100)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
# An accounting entry should only be created for the emptying now that the category is manual.
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 2)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids), 3)
|
|
|
|
def test_standard_auto_to_manual_2(self):
|
|
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
|
self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
|
|
self.product1.product_tmpl_id.standard_price = 10
|
|
move1 = self._make_in_move(self.product1, 10)
|
|
|
|
self.assertEqual(self.product1.value_svl, 100)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 1)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids), 1)
|
|
|
|
cat2 = self.env['product.category'].create({'name': 'fifo'})
|
|
cat2.property_cost_method = 'standard'
|
|
cat2.property_valuation = 'manual_periodic'
|
|
self.product1.with_context(debug=True).categ_id = cat2
|
|
|
|
self.assertEqual(self.product1.value_svl, 100)
|
|
self.assertEqual(self.product1.quantity_svl, 10)
|
|
# An accounting entry should only be created for the emptying now that the category is manual.
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 2)
|
|
self.assertEqual(len(self.product1.stock_valuation_layer_ids), 3)
|
|
|
|
@tagged('post_install', '-at_install')
|
|
class TestAngloSaxonAccounting(AccountTestInvoicingCommon):
|
|
@classmethod
|
|
def setUpClass(cls, chart_template_ref=None):
|
|
super().setUpClass(chart_template_ref=chart_template_ref)
|
|
cls.env.ref('base.EUR').active = True
|
|
cls.company_data['company'].anglo_saxon_accounting = True
|
|
cls.stock_location = cls.env['stock.location'].create({
|
|
'name': 'stock location',
|
|
'usage': 'internal',
|
|
})
|
|
cls.customer_location = cls.env['stock.location'].create({
|
|
'name': 'customer location',
|
|
'usage': 'customer',
|
|
})
|
|
cls.supplier_location = cls.env['stock.location'].create({
|
|
'name': 'supplier location',
|
|
'usage': 'supplier',
|
|
})
|
|
cls.warehouse_in = cls.env['stock.warehouse'].create({
|
|
'name': 'warehouse in',
|
|
'company_id': cls.company_data['company'].id,
|
|
'code': '1',
|
|
})
|
|
cls.warehouse_out = cls.env['stock.warehouse'].create({
|
|
'name': 'warehouse out',
|
|
'company_id': cls.company_data['company'].id,
|
|
'code': '2',
|
|
})
|
|
cls.picking_type_in = cls.env['stock.picking.type'].create({
|
|
'name': 'pick type in',
|
|
'sequence_code': '1',
|
|
'code': 'incoming',
|
|
'company_id': cls.company_data['company'].id,
|
|
'warehouse_id': cls.warehouse_in.id,
|
|
})
|
|
cls.picking_type_out = cls.env['stock.picking.type'].create({
|
|
'name': 'pick type in',
|
|
'sequence_code': '2',
|
|
'code': 'outgoing',
|
|
'company_id': cls.company_data['company'].id,
|
|
'warehouse_id': cls.warehouse_out.id,
|
|
})
|
|
cls.stock_input_account = cls.env['account.account'].create({
|
|
'name': 'Stock Input',
|
|
'code': 'StockIn',
|
|
'account_type': 'asset_current',
|
|
'reconcile': True,
|
|
})
|
|
cls.stock_output_account = cls.env['account.account'].create({
|
|
'name': 'Stock Output',
|
|
'code': 'StockOut',
|
|
'account_type': 'asset_current',
|
|
'reconcile': True,
|
|
})
|
|
cls.stock_valuation_account = cls.env['account.account'].create({
|
|
'name': 'Stock Valuation',
|
|
'code': 'StockValuation',
|
|
'account_type': 'asset_current',
|
|
'reconcile': True,
|
|
})
|
|
cls.expense_account = cls.env['account.account'].create({
|
|
'name': 'Expense Account',
|
|
'code': 'ExpenseAccount',
|
|
'account_type': 'expense',
|
|
'reconcile': True,
|
|
})
|
|
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
|
|
cls.product1 = cls.env['product.product'].create({
|
|
'name': 'product1',
|
|
'type': 'product',
|
|
'categ_id': cls.env.ref('product.product_category_all').id,
|
|
'property_account_expense_id': cls.expense_account.id,
|
|
})
|
|
cls.product1.categ_id.write({
|
|
'property_valuation': 'real_time',
|
|
'property_stock_account_input_categ_id': cls.stock_input_account.id,
|
|
'property_stock_account_output_categ_id': cls.stock_output_account.id,
|
|
'property_stock_valuation_account_id': cls.stock_valuation_account.id,
|
|
'property_stock_journal': cls.company_data['default_journal_misc'].id,
|
|
})
|
|
|
|
def _make_in_move(self, product, quantity, unit_cost=None, create_picking=False, loc_dest=None, pick_type=None):
|
|
""" Helper to create and validate a receipt move.
|
|
"""
|
|
unit_cost = unit_cost or product.standard_price
|
|
loc_dest = loc_dest or self.stock_location
|
|
pick_type = pick_type or self.picking_type_in
|
|
in_move = self.env['stock.move'].create({
|
|
'name': 'in %s units @ %s per unit' % (str(quantity), str(unit_cost)),
|
|
'product_id': product.id,
|
|
'location_id': self.supplier_location.id,
|
|
'location_dest_id': loc_dest.id,
|
|
'product_uom': self.uom_unit.id,
|
|
'product_uom_qty': quantity,
|
|
'price_unit': unit_cost,
|
|
'picking_type_id': pick_type.id,
|
|
})
|
|
|
|
if create_picking:
|
|
picking = self.env['stock.picking'].create({
|
|
'picking_type_id': in_move.picking_type_id.id,
|
|
'location_id': in_move.location_id.id,
|
|
'location_dest_id': in_move.location_dest_id.id,
|
|
})
|
|
in_move.write({'picking_id': picking.id})
|
|
|
|
in_move._action_confirm()
|
|
in_move._action_assign()
|
|
in_move.move_line_ids.qty_done = quantity
|
|
in_move._action_done()
|
|
|
|
return in_move.with_context(svl=True)
|
|
|
|
def test_avco_and_credit_note(self):
|
|
"""
|
|
When reversing an invoice that contains some anglo-saxo AML, the new anglo-saxo AML should have the same value
|
|
"""
|
|
# Required for `account_id` to be visible in the view
|
|
self.env.user.groups_id += self.env.ref('account.group_account_readonly')
|
|
self.product1.categ_id.property_cost_method = 'average'
|
|
|
|
self._make_in_move(self.product1, 2, unit_cost=10)
|
|
|
|
invoice_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
|
|
invoice_form.partner_id = self.env['res.partner'].create({'name': 'Super Client'})
|
|
with invoice_form.invoice_line_ids.new() as invoice_line_form:
|
|
invoice_line_form.product_id = self.product1
|
|
invoice_line_form.quantity = 2
|
|
invoice_line_form.price_unit = 25
|
|
invoice_line_form.account_id = self.company_data['default_journal_purchase'].default_account_id
|
|
invoice_line_form.tax_ids.clear()
|
|
invoice = invoice_form.save()
|
|
invoice.action_post()
|
|
|
|
self._make_in_move(self.product1, 2, unit_cost=20)
|
|
self.assertEqual(self.product1.standard_price, 15)
|
|
|
|
refund_wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice.ids).create({
|
|
'refund_method': 'refund',
|
|
'journal_id': invoice.journal_id.id,
|
|
})
|
|
action = refund_wizard.reverse_moves()
|
|
reverse_invoice = self.env['account.move'].browse(action['res_id'])
|
|
with Form(reverse_invoice) as reverse_invoice_form:
|
|
with reverse_invoice_form.invoice_line_ids.edit(0) as line:
|
|
line.quantity = 1
|
|
reverse_invoice.action_post()
|
|
|
|
anglo_lines = reverse_invoice.line_ids.filtered(lambda l: l.display_type == 'cogs')
|
|
self.assertEqual(len(anglo_lines), 2)
|
|
self.assertEqual(abs(anglo_lines[0].balance), 10)
|
|
self.assertEqual(abs(anglo_lines[1].balance), 10)
|