The default resupply feature can be configured by going on the route and activating it for the warehouse. This field is therefore useless and this commit remove it. The field 'resupply warehouses' is reworded an hidden when there is only one active warehouse PR:22085 TASK:38933
454 lines
22 KiB
Python
454 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(len(quant), 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(len(quants), 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(len(self.env['stock.quant']._gather(product, warehouse_distribution_namur.lot_stock_id)), 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(len(self.env['stock.quant']._gather(product, warehouse_distribution_wavre.lot_stock_id)), 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,
|
|
})
|