[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
Cherry-pick of be9dce625c
This commit is contained in:
committed by
Christophe Simonis
parent
544353c865
commit
cb73aaf71b
@@ -325,31 +325,57 @@ class InventoryLine(models.Model):
|
||||
('owner_id', '=', self.partner_id.id),
|
||||
('package_id', '=', self.package_id.id)])
|
||||
|
||||
def _get_move_values(self, qty, location_id, location_dest_id):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('INV:') + (self.inventory_id.name or ''),
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom': self.product_uom_id.id,
|
||||
'product_uom_qty': qty,
|
||||
'date': self.inventory_id.date,
|
||||
'company_id': self.inventory_id.company_id.id,
|
||||
'inventory_id': self.inventory_id.id,
|
||||
'state': 'confirmed',
|
||||
'restrict_lot_id': self.prod_lot_id.id,
|
||||
'restrict_partner_id': self.partner_id.id,
|
||||
'location_id': location_id,
|
||||
'location_dest_id': location_dest_id,
|
||||
}
|
||||
|
||||
def _fixup_negative_quants(self):
|
||||
""" This will handle the irreconciable quants created by a force availability followed by a
|
||||
return. When generating the moves of an inventory line, we look for quants of this line's
|
||||
product created to compensate a force availability. If there are some and if the quant
|
||||
which it is propagated from is still in the same location, we move it to the inventory
|
||||
adjustment location before getting it back. Getting the quantity from the inventory
|
||||
location will allow the negative quant to be compensated.
|
||||
"""
|
||||
self.ensure_one()
|
||||
for quant in self._get_quants().filtered(lambda q: q.propagated_from_id.location_id.id == self.location_id.id):
|
||||
# send the quantity to the inventory adjustment location
|
||||
move_out_vals = self._get_move_values(quant.qty, self.location_id.id, self.product_id.property_stock_inventory.id)
|
||||
move_out = self.env['stock.move'].create(move_out_vals)
|
||||
self.env['stock.quant'].quants_reserve([(quant, quant.qty)], move_out)
|
||||
move_out.action_done()
|
||||
|
||||
# get back the quantity from the inventory adjustment location
|
||||
move_in_vals = self._get_move_values(quant.qty, self.product_id.property_stock_inventory.id, self.location_id.id)
|
||||
move_in = self.env['stock.move'].create(move_in_vals)
|
||||
move_in.action_done()
|
||||
|
||||
def _generate_moves(self):
|
||||
moves = self.env['stock.move']
|
||||
Quant = self.env['stock.quant']
|
||||
for line in self:
|
||||
line._fixup_negative_quants()
|
||||
|
||||
if float_utils.float_compare(line.theoretical_qty, line.product_qty, precision_rounding=line.product_id.uom_id.rounding) == 0:
|
||||
continue
|
||||
diff = line.theoretical_qty - line.product_qty
|
||||
vals = {
|
||||
'name': _('INV:') + (line.inventory_id.name or ''),
|
||||
'product_id': line.product_id.id,
|
||||
'product_uom': line.product_uom_id.id,
|
||||
'date': line.inventory_id.date,
|
||||
'company_id': line.inventory_id.company_id.id,
|
||||
'inventory_id': line.inventory_id.id,
|
||||
'state': 'confirmed',
|
||||
'restrict_lot_id': line.prod_lot_id.id,
|
||||
'restrict_partner_id': line.partner_id.id}
|
||||
if diff < 0: # found more than expected
|
||||
vals['location_id'] = line.product_id.property_stock_inventory.id
|
||||
vals['location_dest_id'] = line.location_id.id
|
||||
vals['product_uom_qty'] = abs(diff)
|
||||
vals = self._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id)
|
||||
else:
|
||||
vals['location_id'] = line.location_id.id
|
||||
vals['location_dest_id'] = line.product_id.property_stock_inventory.id
|
||||
vals['product_uom_qty'] = diff
|
||||
vals = self._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id)
|
||||
move = moves.create(vals)
|
||||
|
||||
if diff > 0:
|
||||
|
||||
@@ -103,6 +103,134 @@ class TestInventory(TestStockCommon):
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user