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
274 lines
13 KiB
Python
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)
|