Files
odoo_source/addons/stock/tests/test_warehouse.py
T
Simon Lejeune be9dce625c [FIX] stock: inventory adjustment and negative quants
It is still possible to have negative and positive quants in the same
location because of returns: if you send something to the customer that
is not there and you return it, you will still be able to reserve the
returned goods to send to another client.

Before, if you would do an inventory adjustment, it would not take into
account these returned quants and their negative counterpart, which made
them difficult to get out of the system.

This fix takes them into account by creating two movements for one
inventory line: move the positive counterpart to the inventory location
before getting back from this location the same quantity.

This way, even if you have 0 as quantity on hand but you have those 2
quants, it will eliminate them. (if you are increasing the stock, part
of the process might have done it automatically already).

Test cases contributed by @jir-odoo
2017-03-16 17:30:02 +01:00

274 lines
13 KiB
Python

# -*- coding: utf-8 -*-
from odoo.addons.stock.tests.common2 import TestStockCommon
class TestInventory(TestStockCommon):
def test_inventory_product(self):
# TDE NOTE: replaces test/inventory.yml present until saas-10
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.prepare_inventory()
# 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_done()
# 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')
self.assertEqual(inventory.move_ids.quant_ids.location_id, self.env.ref('stock.location_inventory')) # Inventory loss
self.assertEqual(inventory.move_ids.quant_ids.qty, 15.0)
self.assertEqual(inventory.move_ids.quant_ids.history_ids[0].product_qty, 50.0)
# Check quantity of product in various locations: current, its parent, brother and other
self.assertEqual(self.product_1.with_context(location=self.warehouse_1.lot_stock_id.id).qty_available, 35.0)
self.assertEqual(self.product_1.with_context(location=self.warehouse_1.lot_stock_id.location_id.id).qty_available, 35.0)
self.assertEqual(self.product_1.with_context(location=self.warehouse_1.view_location_id.id).qty_available, 35.0)
self.assertEqual(self.product_1.with_context(location=self.warehouse_1.wh_input_stock_loc_id.id).qty_available, 0.0)
self.assertEqual(self.product_1.with_context(location=self.env.ref('stock.stock_location_stock').id).qty_available, 0.0)
def test_inventory_wizard(self):
# 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: 1 quant for the product and the quantity
quant = self.env['stock.quant'].search([('id', 'not in', self.existing_quants.ids)])
self.assertEqual(len(quant), 1)
# print quant.name, quant.product_id, quant.location_id
# TDE TODO: expand this test
def test_basic_move(self):
# TDE NOTE: replaces test/move.yml present until saas-10, including onchanges
Move = self.env['stock.move'].sudo(self.user_stock_manager)
product = self.product_3.sudo(self.user_stock_manager)
# simulate create + onchange
customer_move = self._create_move(product, self.warehouse_1.lot_stock_id, self.env.ref('stock.stock_location_customers'), product_uom_qty=5)
# 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.action_done()
self.assertEqual(product.qty_available, -5.0)
self.assertEqual(product.with_context(location=self.env.ref('stock.stock_location_customers').id).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.action_done()
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()
self.assertEqual(product.qty_available, 10.0)
self.assertEqual(product.virtual_available, 8.0)
customer_move_2.action_done()
self.assertEqual(product.qty_available, 8.0)
self.assertEqual(product.with_context(location=self.env.ref('stock.stock_location_customers').id).qty_available, 7.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')
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.force_assign()
picking_out.do_transfer()
# Create return picking for all goods
default_data = self.env['stock.return.picking']\
.with_context(active_ids=picking_out.ids, active_id=picking_out.ids[0])\
.default_get([
'move_dest_exists',
'original_location_id',
'product_return_moves',
'parent_location_id',
'location_id',
])
return_wiz = self.env['stock.return.picking']\
.with_context(active_ids=picking_out.ids, active_id=picking_out.ids[0])\
.create(default_data)
res = return_wiz.create_returns()
return_pick = self.env['stock.picking'].browse(res['res_id'])
return_pick.action_assign()
return_pick.do_transfer()
# 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.prepare_inventory()
self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.")
self.assertEqual(inventory.line_ids.theoretical_qty, 0, "Theoretical quantity should be zero.")
inventory.action_done()
# The inventory adjustment should have created two moves
self.assertEqual(len(inventory.move_ids), 2)
quantity = inventory.move_ids.mapped('product_qty')
self.assertEqual(quantity, [1, 1], "Moves created with wrong quantity.")
location_ids = inventory.move_ids.mapped('location_id').ids
self.assertEqual(set(location_ids), {stock_location.id, 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)
self.assertEqual(quant.qty, 1)
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.force_assign()
picking_out.do_transfer()
# 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.prepare_inventory()
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_done()
# 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 no 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), 0)
class TestResupply(TestStockCommon):
def setUp(self):
super(TestResupply, self).setUp()
self.warehouse_2 = self.env['stock.warehouse'].create({
'name': 'Small Warehouse',
'code': 'SWH',
'default_resupply_wh_id': self.warehouse_1.id,
'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,
})
def test_resupply_from_wh(self):
# TDE NOTE: replaces tests/test_resupply.py, present until saas-10
OrderScheduler = self.env['procurement.order']
OrderScheduler.run_scheduler()
# we generated 2 procurements for product A: one on small wh and the other one on the transit location
procs = OrderScheduler.search([('product_id', '=', self.product_1.id)])
self.assertEqual(len(procs), 2)
proc1 = procs.filtered(lambda order: order.warehouse_id == self.warehouse_2)
self.assertEqual(proc1.state, 'running')
proc2 = procs.filtered(lambda order: order.warehouse_id == self.warehouse_1)
self.assertEqual(proc2.location_id.usage, 'transit')
self.assertNotEqual(proc2.state, 'exception')
proc2.run()
self.assertEqual(proc2.state, 'running')
self.assertTrue(proc2.rule_id)