Files
odoo_source/addons/stock/tests/test_warehouse.py
T
Arnold Moyaux c0d7a05730 [REF] stock: _action_done and cache invalidation
`_action_done` could unlink a quant. This happens when moving a product from a
location to another one and makes null the quantity of this product in one of
theses locations. In this case, we unlink the quant because it is now useless
and could be confusing on reports based on quants.

The unwanted side effect is that unlinking a record will invalidate the cache. In
some pathologic cases, like making an inventory adjustment of 600 products
and reseting their quantity back to 0, the time of the operation is around 15
minutes. With this patch that tries to work carefully with the cache invalidation,
the same operations takes around 15 secondes.

This commit do not unlink quant anymore, instead it use a 'garbage
collector' on quants without quantity. The garbage collector is trigger
on the scheduler and when the user open the inventory view.
2018-09-18 14:53:03 +02:00

453 lines
22 KiB
Python

# -*- coding: utf-8 -*-
from odoo.addons.stock.tests.common2 import TestStockCommon
class TestWarehouse(TestStockCommon):
def test_inventory_product(self):
self.product_1.type = 'product'
inventory_wizard = self.env['stock.change.product.qty'].create({
'product_id': self.product_1.id,
'new_quantity': 50.0,
'location_id': self.warehouse_1.lot_stock_id.id,
})
inventory_wizard.change_product_qty()
inventory = self.env['stock.inventory'].sudo(self.user_stock_manager).create({
'name': 'Starting for product_1',
'filter': 'product',
'location_id': self.warehouse_1.lot_stock_id.id,
'product_id': self.product_1.id,
})
inventory.action_start()
# As done in common.py, there is already an inventory line existing
self.assertEqual(len(inventory.line_ids), 1)
self.assertEqual(inventory.line_ids.theoretical_qty, 50.0)
self.assertEqual(inventory.line_ids.product_id, self.product_1)
self.assertEqual(inventory.line_ids.product_uom_id, self.product_1.uom_id)
# Update the line, set to 35
inventory.line_ids.write({'product_qty': 35.0})
inventory.action_validate()
# Check related move and quants
self.assertIn(inventory.name, inventory.move_ids.name)
self.assertEqual(inventory.move_ids.product_qty, 15.0)
self.assertEqual(inventory.move_ids.location_id, self.warehouse_1.lot_stock_id)
self.assertEqual(inventory.move_ids.location_dest_id, self.env.ref('stock.location_inventory')) # Inventory loss
self.assertEqual(inventory.move_ids.state, 'done')
quants = self.env['stock.quant']._gather(self.product_1, self.env.ref('stock.location_inventory'))
self.assertEqual(len(quants), 1) # One quant created for inventory loss
# Check quantity of product in various locations: current, its parent, brother and other
self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.warehouse_1.lot_stock_id).quantity, 35.0)
self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.warehouse_1.lot_stock_id.location_id).quantity, 35.0)
self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.warehouse_1.view_location_id).quantity, 35.0)
self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.warehouse_1.wh_input_stock_loc_id).quantity, 0.0)
self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.env.ref('stock.stock_location_stock')).quantity, 0.0)
def test_inventory_wizard(self):
self.product_1.type = 'product'
inventory_wizard = self.env['stock.change.product.qty'].create({
'product_id': self.product_1.id,
'new_quantity': 50.0,
'location_id': self.warehouse_1.lot_stock_id.id,
})
inventory_wizard.change_product_qty()
# Check inventory performed in setup was effectivley performed
self.assertEqual(self.product_1.virtual_available, 50.0)
self.assertEqual(self.product_1.qty_available, 50.0)
# Check inventory obj details (1 inventory with 1 line, because 1 product change)
inventory = self.env['stock.inventory'].search([('id', 'not in', self.existing_inventories.ids)])
self.assertEqual(len(inventory), 1)
self.assertIn('INV: %s' % self.product_1.name, inventory.name)
self.assertEqual(len(inventory.line_ids), 1)
self.assertEqual(inventory.line_ids.product_id, self.product_1)
self.assertEqual(inventory.line_ids.product_qty, 50.0)
# Check associated quants: 2 quants for the product and the quantity (1 in stock, 1 in inventory adjustment)
quant = self.env['stock.quant'].search([('id', 'not in', self.existing_quants.ids)])
self.assertEqual(len(quant), 2)
# print quant.name, quant.product_id, quant.location_id
# TDE TODO: expand this test
def test_basic_move(self):
product = self.product_3.sudo(self.user_stock_manager)
product.type = 'product'
picking_out = self.env['stock.picking'].create({
'partner_id': self.env.ref('base.res_partner_2').id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': self.warehouse_1.lot_stock_id.id,
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
})
customer_move = self.env['stock.move'].create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 5,
'product_uom': product.uom_id.id,
'picking_id': picking_out.id,
'location_id': self.warehouse_1.lot_stock_id.id,
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
})
# simulate create + onchange
# test move values
self.assertEqual(customer_move.product_uom, product.uom_id)
self.assertEqual(customer_move.location_id, self.warehouse_1.lot_stock_id)
self.assertEqual(customer_move.location_dest_id, self.env.ref('stock.stock_location_customers'))
# confirm move, check quantity on hand and virtually available, without location context
customer_move._action_confirm()
self.assertEqual(product.qty_available, 0.0)
self.assertEqual(product.virtual_available, -5.0)
customer_move.quantity_done = 5
customer_move._action_done()
self.assertEqual(product.qty_available, -5.0)
# compensate negative quants by receiving products from supplier
receive_move = self._create_move(product, self.env.ref('stock.stock_location_suppliers'), self.warehouse_1.lot_stock_id, product_uom_qty=15)
receive_move._action_confirm()
receive_move.quantity_done = 15
receive_move._action_done()
product._compute_quantities()
self.assertEqual(product.qty_available, 10.0)
self.assertEqual(product.virtual_available, 10.0)
# new move towards customer
customer_move_2 = self._create_move(product, self.warehouse_1.lot_stock_id, self.env.ref('stock.stock_location_customers'), product_uom_qty=2)
customer_move_2._action_confirm()
product._compute_quantities()
self.assertEqual(product.qty_available, 10.0)
self.assertEqual(product.virtual_available, 8.0)
customer_move_2.quantity_done = 2.0
customer_move_2._action_done()
product._compute_quantities()
self.assertEqual(product.qty_available, 8.0)
def test_inventory_adjustment_and_negative_quants_1(self):
"""Make sure negative quants from returns get wiped out with an inventory adjustment"""
productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'})
stock_location = self.env.ref('stock.stock_location_stock')
customer_location = self.env.ref('stock.stock_location_customers')
# Create a picking out and force availability
picking_out = self.env['stock.picking'].create({
'partner_id': self.env.ref('base.res_partner_2').id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
})
self.env['stock.move'].create({
'name': productA.name,
'product_id': productA.id,
'product_uom_qty': 1,
'product_uom': productA.uom_id.id,
'picking_id': picking_out.id,
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
})
picking_out.action_confirm()
picking_out.move_lines.quantity_done = 1
picking_out.action_done()
quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)])
self.assertEqual(len(quant), 1)
stock_return_picking = self.env['stock.return.picking']\
.with_context(active_ids=picking_out.ids, active_id=picking_out.ids[0])\
.create({})
stock_return_picking.product_return_moves.quantity = 1.0
stock_return_picking_action = stock_return_picking.create_returns()
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
return_pick.action_assign()
return_pick.move_lines.quantity_done = 1
return_pick.action_done()
quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)])
self.assertEqual(sum(quant.mapped('quantity')), 0)
def test_inventory_adjustment_and_negative_quants_2(self):
"""Make sure negative quants get wiped out with an inventory adjustment"""
productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'})
stock_location = self.env.ref('stock.stock_location_stock')
customer_location = self.env.ref('stock.stock_location_customers')
location_loss = self.env.ref('stock.location_inventory')
# Create a picking out and force availability
picking_out = self.env['stock.picking'].create({
'partner_id': self.env.ref('base.res_partner_2').id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
})
self.env['stock.move'].create({
'name': productA.name,
'product_id': productA.id,
'product_uom_qty': 1,
'product_uom': productA.uom_id.id,
'picking_id': picking_out.id,
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
})
picking_out.action_confirm()
picking_out.move_lines.quantity_done = 1
picking_out.action_done()
# Make an inventory adjustment to set the quantity to 0
inventory = self.env['stock.inventory'].create({
'name': 'Starting for product_1',
'filter': 'product',
'location_id': stock_location.id,
'product_id': productA.id,
})
inventory.action_start()
self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.")
self.assertEqual(inventory.line_ids.theoretical_qty, -1, "Theoretical quantity should be -1.")
inventory.line_ids.product_qty = 0 # Put the quantity back to 0
inventory.action_validate()
# The inventory adjustment should have created one
self.assertEqual(len(inventory.move_ids), 1)
quantity = inventory.move_ids.mapped('product_qty')
self.assertEqual(quantity, [1], "Moves created with wrong quantity.")
location_ids = inventory.move_ids.mapped('location_id').ids
self.assertEqual(set(location_ids), {location_loss.id})
# There should be no quant in the stock location
quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)])
self.assertEqual(sum(quants.mapped('quantity')), 0)
# There should be one quant in the inventory loss location
quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)])
self.assertEqual(len(quant), 1)
def test_resupply_route(self):
""" Simulate a resupply chain between warehouses.
Stock -> transit -> Dist. -> transit -> Shop -> Customer
Create the move from Shop to Customer and ensure that all the pull
rules are triggered in order to complete the move chain to Stock.
"""
warehouse_stock = self.env['stock.warehouse'].create({
'name': 'Stock.',
'code': 'STK',
})
warehouse_distribution = self.env['stock.warehouse'].create({
'name': 'Dist.',
'code': 'DIST',
'resupply_wh_ids': [(6, 0, [warehouse_stock.id])]
})
warehouse_shop = self.env['stock.warehouse'].create({
'name': 'Shop',
'code': 'SHOP',
'resupply_wh_ids': [(6, 0, [warehouse_distribution.id])]
})
route_stock_to_dist = warehouse_distribution.resupply_route_ids
route_dist_to_shop = warehouse_shop.resupply_route_ids
# Change the procure_method on the pull rules between dist and shop
# warehouses. Since mto and resupply routes are both on product it will
# select one randomly between them and if it select the resupply it is
# 'make to stock' and it will not create the picking between stock and
# dist warehouses.
route_dist_to_shop.rule_ids.write({'procure_method': 'make_to_order'})
product = self.env['product.product'].create({
'name': 'Fakir',
'type': 'product',
'route_ids': [(4, route_id) for route_id in [route_stock_to_dist.id, route_dist_to_shop.id, self.env.ref('stock.route_warehouse0_mto').id]],
})
picking_out = self.env['stock.picking'].create({
'partner_id': self.env.ref('base.res_partner_2').id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': warehouse_shop.lot_stock_id.id,
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
})
self.env['stock.move'].create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product.uom_id.id,
'picking_id': picking_out.id,
'location_id': warehouse_shop.lot_stock_id.id,
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
'warehouse_id': warehouse_shop.id,
'procure_method': 'make_to_order',
})
picking_out.action_confirm()
moves = self.env['stock.move'].search([('product_id', '=', product.id)])
# Shop/Stock -> Customer
# Transit -> Shop/Stock
# Dist/Stock -> Transit
# Transit -> Dist/Stock
# Stock/Stock -> Transit
self.assertEqual(len(moves), 5, 'Invalid moves number.')
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_stock.lot_stock_id.id)]))
self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_distribution.lot_stock_id.id)]))
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_distribution.lot_stock_id.id)]))
self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_shop.lot_stock_id.id)]))
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_shop.lot_stock_id.id)]))
def test_mutiple_resupply_warehouse(self):
""" Simulate the following situation:
- 2 shops with stock are resupply by 2 distinct warehouses
- Shop Namur is resupply by the warehouse stock Namur
- Shop Wavre is resupply by the warehouse stock Wavre
- Simulate 2 moves for the same product but in different shop.
This test ensure that the move are supplied by the correct distribution
warehouse.
"""
customer_location = self.env.ref('stock.stock_location_customers')
warehouse_distribution_wavre = self.env['stock.warehouse'].create({
'name': 'Stock Wavre.',
'code': 'WV',
})
warehouse_shop_wavre = self.env['stock.warehouse'].create({
'name': 'Shop Wavre',
'code': 'SHWV',
'resupply_wh_ids': [(6, 0, [warehouse_distribution_wavre.id])]
})
warehouse_distribution_namur = self.env['stock.warehouse'].create({
'name': 'Stock Namur.',
'code': 'NM',
})
warehouse_shop_namur = self.env['stock.warehouse'].create({
'name': 'Shop Namur',
'code': 'SHNM',
'resupply_wh_ids': [(6, 0, [warehouse_distribution_namur.id])]
})
route_shop_namur = warehouse_shop_namur.resupply_route_ids
route_shop_wavre = warehouse_shop_wavre.resupply_route_ids
# The product contains the 2 resupply routes.
product = self.env['product.product'].create({
'name': 'Fakir',
'type': 'product',
'route_ids': [(4, route_id) for route_id in [route_shop_namur.id, route_shop_wavre.id, self.env.ref('stock.route_warehouse0_mto').id]],
})
# Add 1 quant in each distribution warehouse.
self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_wavre.lot_stock_id, 1.0)
self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_namur.lot_stock_id, 1.0)
# Create the move for the shop Namur. Should create a resupply from
# distribution warehouse Namur.
picking_out_namur = self.env['stock.picking'].create({
'partner_id': self.env.ref('base.res_partner_2').id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': warehouse_shop_namur.lot_stock_id.id,
'location_dest_id': customer_location.id,
})
self.env['stock.move'].create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product.uom_id.id,
'picking_id': picking_out_namur.id,
'location_id': warehouse_shop_namur.lot_stock_id.id,
'location_dest_id': customer_location.id,
'warehouse_id': warehouse_shop_namur.id,
'procure_method': 'make_to_order',
})
picking_out_namur.action_confirm()
# Validate the picking
# Dist. warehouse Namur -> transit Location -> Shop Namur
picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_namur.lot_stock_id.id)])
self.assertTrue(picking_stock_transit)
picking_stock_transit.action_assign()
picking_stock_transit.move_lines[0].quantity_done = 1.0
picking_stock_transit.action_done()
picking_transit_shop_namur = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_namur.lot_stock_id.id)])
self.assertTrue(picking_transit_shop_namur)
picking_transit_shop_namur.action_assign()
picking_transit_shop_namur.move_lines[0].quantity_done = 1.0
picking_transit_shop_namur.action_done()
picking_out_namur.action_assign()
picking_out_namur.move_lines[0].quantity_done = 1.0
picking_out_namur.action_done()
# Check that the correct quantity has been provided to customer
self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 1)
# Ensure there still no quants in distribution warehouse
self.assertEqual(sum(self.env['stock.quant']._gather(product, warehouse_distribution_namur.lot_stock_id).mapped('quantity')), 0)
# Create the move for the shop Wavre. Should create a resupply from
# distribution warehouse Wavre.
picking_out_wavre = self.env['stock.picking'].create({
'partner_id': self.env.ref('base.res_partner_2').id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': warehouse_shop_wavre.lot_stock_id.id,
'location_dest_id': customer_location.id,
})
self.env['stock.move'].create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product.uom_id.id,
'picking_id': picking_out_wavre.id,
'location_id': warehouse_shop_wavre.lot_stock_id.id,
'location_dest_id': customer_location.id,
'warehouse_id': warehouse_shop_wavre.id,
'procure_method': 'make_to_order',
})
picking_out_wavre.action_confirm()
# Validate the picking
# Dist. warehouse Wavre -> transit Location -> Shop Wavre
picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_wavre.lot_stock_id.id)])
self.assertTrue(picking_stock_transit)
picking_stock_transit.action_assign()
picking_stock_transit.move_lines[0].quantity_done = 1.0
picking_stock_transit.action_done()
picking_transit_shop_wavre = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_wavre.lot_stock_id.id)])
self.assertTrue(picking_transit_shop_wavre)
picking_transit_shop_wavre.action_assign()
picking_transit_shop_wavre.move_lines[0].quantity_done = 1.0
picking_transit_shop_wavre.action_done()
picking_out_wavre.action_assign()
picking_out_wavre.move_lines[0].quantity_done = 1.0
picking_out_wavre.action_done()
# Check that the correct quantity has been provided to customer
self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 2)
# Ensure there still no quants in distribution warehouse
self.assertEqual(sum(self.env['stock.quant']._gather(product, warehouse_distribution_wavre.lot_stock_id).mapped('quantity')), 0)
class TestResupply(TestStockCommon):
def setUp(self):
super(TestResupply, self).setUp()
self.warehouse_2 = self.env['stock.warehouse'].create({
'name': 'Small Warehouse',
'code': 'SWH',
'resupply_wh_ids': [(6, 0, [self.warehouse_1.id])]
})
# minimum stock rule for test product on this warehouse
self.env['stock.warehouse.orderpoint'].create({
'warehouse_id': self.warehouse_2.id,
'location_id': self.warehouse_2.lot_stock_id.id,
'product_id': self.product_1.id,
'product_min_qty': 10,
'product_max_qty': 100,
'product_uom': self.uom_unit.id,
})