diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py index 5322e13e05a..62f5861afba 100644 --- a/addons/stock/models/stock_inventory.py +++ b/addons/stock/models/stock_inventory.py @@ -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: diff --git a/addons/stock/tests/test_warehouse.py b/addons/stock/tests/test_warehouse.py index 048392f5085..94cca6b034e 100644 --- a/addons/stock/tests/test_warehouse.py +++ b/addons/stock/tests/test_warehouse.py @@ -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):