Odoo18-Base/addons/stock_account/tests/test_stockvaluationlayer.py
2025-01-06 10:57:38 +07:00

1411 lines
64 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 import Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.stock_account.tests.test_stockvaluation import _create_accounting_data
from odoo.exceptions import ValidationError
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',
'is_storable': True,
'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, lot_ids=False):
""" 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()
if lot_ids:
in_move.move_line_ids.unlink()
in_move.move_line_ids = [Command.create({
'location_id': self.supplier_location.id,
'location_dest_id': loc_dest.id,
'quantity': quantity / len(lot_ids),
'product_id': product.id,
'lot_id': lot.id,
}) for lot in lot_ids]
else:
in_move._action_assign()
in_move.picked = True
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, lot_ids=False):
""" 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,
})
if lot_ids:
out_move.move_line_ids.unlink()
out_move.move_line_ids = [Command.create({
'location_id': loc_src.id,
'location_dest_id': self.customer_location.id,
'quantity': quantity / len(lot_ids),
'product_id': product.id,
'lot_id': lot.id,
}) for lot in lot_ids]
else:
out_move.move_line_ids.quantity = quantity
out_move.picked = True
out_move._action_done()
self.days += 1
return out_move.with_context(svl=True)
def _make_dropship_move(self, product, quantity, unit_cost=None, lot_ids=False):
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()
if lot_ids:
dropshipped.move_line_ids = [Command.clear()]
dropshipped.move_line_ids = [Command.create({
'location_id': self.supplier_location.id,
'location_dest_id': self.customer_location.id,
'quantity': quantity / len(lot_ids),
'product_id': product.id,
'lot_id': lot.id,
}) for lot in lot_ids]
else:
dropshipped.move_line_ids.quantity = quantity
dropshipped.picked = True
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.action_create_returns()
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
return_pick.move_ids[0].move_line_ids[0].quantity = quantity_to_return
return_pick.move_ids[0].picked = True
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.quantity = 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.quantity = 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,
'quantity': 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.quantity = 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.quantity = 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,
'quantity': 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.quantity = 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',
'is_storable': True,
})
product2 = self.env['product.product'].create({
'name': 'p2',
'is_storable': True,
})
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.quantity = 2
in_move.picked = True
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'))
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
def test_change_qty_and_locations_of_done_sml(self):
sub_stock_loc = self.env['stock.location'].create({
'name': 'shelf1',
'usage': 'internal',
'location_id': self.stock_location.id,
})
move_in = self._make_in_move(self.product1, 25)
self.assertEqual(self.product1.value_svl, 250)
self.assertEqual(self.product1.qty_available, 25)
move_in.move_line_ids.write({
'location_dest_id': sub_stock_loc.id,
'quantity': 30,
})
self.assertEqual(self.product1.value_svl, 300)
self.assertEqual(self.product1.qty_available, 30)
sub_loc_quant = self.product1.stock_quant_ids.filtered(lambda q: q.location_id == sub_stock_loc)
self.assertEqual(sub_loc_quant.quantity, 30)
with self.assertRaises(ValidationError):
move_in.move_line_ids.location_id = self.stock_location
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.quantity = 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.quantity = 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,
'quantity': 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,
'quantity': 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.quantity = 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.quantity = 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_rounding_svl_3(self):
self._make_in_move(self.product1, 1000, unit_cost=0.17)
self._make_in_move(self.product1, 800, unit_cost=0.23)
self.assertEqual(self.product1.standard_price, 0.20)
self._make_out_move(self.product1, 1000, create_picking=True)
self._make_out_move(self.product1, 800, create_picking=True)
self.assertEqual(self.product1.value_svl, 0)
def test_rounding_svl_4(self):
"""
The first 2 In moves result in a rounded standard_price at 3.4943, which is rounded at 3.49.
This test ensures that no rounding error is generated with small out quantities.
"""
self.product1.categ_id.property_cost_method = 'average'
self._make_in_move(self.product1, 2, unit_cost=4.63)
self._make_in_move(self.product1, 5, unit_cost=3.04)
self.assertEqual(self.product1.standard_price, 3.49)
for _ in range(70):
self._make_out_move(self.product1, 0.1)
self.assertEqual(self.product1.quantity_svl, 0)
self.assertEqual(self.product1.value_svl, 0)
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)
def test_return_delivery_rounding(self):
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
self.product1.write({"standard_price": 1})
self._make_in_move(self.product1, 1, unit_cost=13.13)
self._make_in_move(self.product1, 1, unit_cost=12.20)
move3 = self._make_out_move(self.product1, 2, create_picking=True)
move4 = self._make_return(move3, 2)
self.assertAlmostEqual(abs(move3.stock_valuation_layer_ids[0].value), abs(move4.stock_valuation_layer_ids[0].value))
self.assertAlmostEqual(self.product1.value_svl, 25.33)
self.assertEqual(self.product1.quantity_svl, 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.quantity = 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.quantity = 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.quantity = 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.quantity = 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.quantity = 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 quantity 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.quantity = 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,
'quantity': 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)
@tagged('post_install', '-at_install', 'change_valuation')
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.write({
'property_valuation': 'real_time',
'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,
})
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_type` key in the context and
# check it doesn't break the account move generation.
self.product1.with_context(default_is_storable=True).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)
def test_return_delivery_fifo(self):
self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
self.env['decimal.precision'].search([
('name', '=', 'Product Price'),
]).digits = 4
self.product1.standard_price = 280.8475
move1 = self._make_out_move(self.product1, 4, create_picking=True, force_assign=True)
move2 = self._make_return(move1, 4)
for move in [move1, move2]:
self.assertEqual(len(move.stock_valuation_layer_ids), 1)
self.assertAlmostEqual(move.stock_valuation_layer_ids.unit_cost, self.product1.standard_price)
self.assertAlmostEqual(abs(move.stock_valuation_layer_ids.value), 1123.39)
@tagged('post_install', '-at_install')
class TestAngloSaxonAccounting(AccountTestInvoicingCommon, TestStockValuationCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
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',
'is_storable': True,
'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.quantity = quantity
in_move.picked = True
in_move._action_done()
return in_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.quantity = quantity
dropshipped.picked = True
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.action_create_returns()
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
return_pick.move_ids[0].move_line_ids[0].quantity = quantity_to_return
return_pick.move_ids.picked = True
return_pick._action_done()
return return_pick.move_ids
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({
'journal_id': invoice.journal_id.id,
})
action = refund_wizard.refund_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)
def test_return_delivery_storno(self):
""" When using STORNO accounting, reverse accounting moves should have negative values for credit/debit.
"""
self.env.company.account_storno = True
self.product1.categ_id.property_cost_method = 'fifo'
self._make_in_move(self.product1, 10, unit_cost=10)
out_move = self._make_out_move(self.product1, 10, create_picking=True)
return_move = self._make_return(out_move, 10)
valuation_line = out_move.account_move_ids.line_ids.filtered(lambda l: l.account_id == self.stock_valuation_account)
stock_out_line = out_move.account_move_ids.line_ids.filtered(lambda l: l.account_id == self.stock_output_account)
self.assertEqual(valuation_line.credit, 100)
self.assertEqual(valuation_line.debit, 0)
self.assertEqual(stock_out_line.credit, 0)
self.assertEqual(stock_out_line.debit, 100)
valuation_line = return_move.account_move_ids.line_ids.filtered(lambda l: l.account_id == self.stock_valuation_account)
stock_out_line = return_move.account_move_ids.line_ids.filtered(lambda l: l.account_id == self.stock_output_account)
self.assertEqual(valuation_line.credit, -100)
self.assertEqual(valuation_line.debit, 0)
self.assertEqual(stock_out_line.credit, 0)
self.assertEqual(stock_out_line.debit, -100)
def test_dropship_return_accounts_1(self):
"""
When returning a dropshipped move, make sure the correct accounts are used
"""
# pylint: disable=bad-whitespace
self.product1.categ_id.property_cost_method = 'fifo'
move1 = self._make_dropship_move(self.product1, 2, unit_cost=10)
move2 = self._make_return(move1, 2)
# First: Input -> Valuation
# Second: Valuation -> Output
origin_svls = move1.stock_valuation_layer_ids.sorted('quantity', reverse=True)
# First: Output -> Valuation
# Second: Valuation -> Input
return_svls = move2.stock_valuation_layer_ids.sorted('quantity', reverse=True)
self.assertEqual(len(origin_svls), 2)
self.assertEqual(len(return_svls), 2)
acc_in, acc_out, acc_valuation = self.stock_input_account, self.stock_output_account, self.stock_valuation_account
# Dropshipping should be: Input -> Output
self.assertRecordValues(origin_svls[0].account_move_id.line_ids, [
{'account_id': acc_in.id, 'debit': 0, 'credit': 20},
{'account_id': acc_valuation.id, 'debit': 20, 'credit': 0},
])
self.assertRecordValues(origin_svls[1].account_move_id.line_ids, [
{'account_id': acc_valuation.id, 'debit': 0, 'credit': 20},
{'account_id': acc_out.id, 'debit': 20, 'credit': 0},
])
# Return should be: Output -> Input
self.assertRecordValues(return_svls[0].account_move_id.line_ids, [
{'account_id': acc_out.id, 'debit': 0, 'credit': 20},
{'account_id': acc_valuation.id, 'debit': 20, 'credit': 0},
])
self.assertRecordValues(return_svls[1].account_move_id.line_ids, [
{'account_id': acc_valuation.id, 'debit': 0, 'credit': 20},
{'account_id': acc_in.id, 'debit': 20, 'credit': 0},
])
def test_dropship_return_accounts_2(self):
"""
When returning a dropshipped move, make sure the correct accounts are used
"""
# pylint: disable=bad-whitespace
self.product1.categ_id.property_cost_method = 'fifo'
move1 = self._make_dropship_move(self.product1, 2, unit_cost=10)
# return to WH/Stock
stock_return_picking = Form(self.env['stock.return.picking']\
.with_context(active_ids=[move1.picking_id.id], active_id=move1.picking_id.id, active_model='stock.picking'))
stock_return_picking = stock_return_picking.save()
stock_return_picking.product_return_moves.quantity = 2
stock_return_picking_action = stock_return_picking.action_create_returns()
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
return_pick.location_dest_id = self.stock_location
return_pick.move_ids[0].move_line_ids[0].quantity = 2
return_pick.move_ids[0].picked = True
return_pick._action_done()
move2 = return_pick.move_ids
# First: Input -> Valuation
# Second: Valuation -> Output
origin_svls = move1.stock_valuation_layer_ids.sorted('quantity', reverse=True)
# Only one: Output -> Valuation
return_svl = move2.stock_valuation_layer_ids
self.assertEqual(len(origin_svls), 2)
self.assertEqual(len(return_svl), 1)
acc_in, acc_out, acc_valuation = self.stock_input_account, self.stock_output_account, self.stock_valuation_account
# Dropshipping should be: Input -> Output
self.assertRecordValues(origin_svls[0].account_move_id.line_ids, [
{'account_id': acc_in.id, 'debit': 0, 'credit': 20},
{'account_id': acc_valuation.id, 'debit': 20, 'credit': 0},
])
self.assertRecordValues(origin_svls[1].account_move_id.line_ids, [
{'account_id': acc_valuation.id, 'debit': 0, 'credit': 20},
{'account_id': acc_out.id, 'debit': 20, 'credit': 0},
])
# Return should be: Output -> Valuation
self.assertRecordValues(return_svl.account_move_id.line_ids, [
{'account_id': acc_out.id, 'debit': 0, 'credit': 20},
{'account_id': acc_valuation.id, 'debit': 20, 'credit': 0},
])