# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from contextlib import closing from datetime import datetime, timedelta from unittest.mock import patch from ast import literal_eval from odoo import Command, fields from odoo.addons.mail.tests.common import mail_new_test_user from odoo.exceptions import ValidationError from odoo.tests.common import Form, TransactionCase from odoo.exceptions import AccessError, UserError class StockQuant(TransactionCase): @classmethod def setUpClass(cls): super(StockQuant, cls).setUpClass() cls.demo_user = mail_new_test_user( cls.env, name='Pauline Poivraisselle', login='pauline', email='p.p@example.com', notification_type='inbox', groups='base.group_user', ) cls.stock_user = mail_new_test_user( cls.env, name='Pauline Poivraisselle', login='pauline2', email='p.p@example.com', notification_type='inbox', groups='stock.group_stock_user', ) cls.product = cls.env['product.product'].create({ 'name': 'Product A', 'type': 'product', }) cls.product_lot = cls.env['product.product'].create({ 'name': 'Product A', 'type': 'product', 'tracking': 'lot', }) cls.product_consu = cls.env['product.product'].create({ 'name': 'Product A', 'type': 'consu', }) cls.product_serial = cls.env['product.product'].create({ 'name': 'Product A', 'type': 'product', 'tracking': 'serial', }) cls.stock_location = cls.env['stock.location'].create({ 'name': 'stock_location', 'usage': 'internal', }) cls.stock_subloc3 = cls.env['stock.location'].create({ 'name': 'subloc3', 'usage': 'internal', 'location_id': cls.stock_location.id }) cls.stock_subloc2 = cls.env['stock.location'].create({ 'name': 'subloc2', 'usage': 'internal', 'location_id': cls.stock_location.id, }) def gather_relevant(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False): quants = self.env['stock.quant']._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) return quants.filtered(lambda q: not (q.quantity == 0 and q.reserved_quantity == 0)) def test_get_available_quantity_1(self): """ Quantity availability with only one quant in a location. """ self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 1.0, }) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0) def test_get_available_quantity_2(self): """ Quantity availability with multiple quants in a location. """ for i in range(3): self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 1.0, }) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 3.0) def test_get_available_quantity_3(self): """ Quantity availability with multiple quants (including negatives ones) in a location. """ for i in range(3): self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 1.0, }) self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': -3.0, }) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) def test_get_available_quantity_4(self): """ Quantity availability with no quants in a location. """ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) def test_get_available_quantity_5(self): """ Quantity availability with multiple partially reserved quants in a location. """ self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 10.0, 'reserved_quantity': 9.0, }) self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 1.0, 'reserved_quantity': 1.0, }) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0) def test_get_available_quantity_6(self): """ Quantity availability with multiple partially reserved quants in a location. """ self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 10.0, 'reserved_quantity': 20.0, }) self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 5.0, 'reserved_quantity': 0.0, }) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, allow_negative=True), -5.0) def test_get_available_quantity_7(self): """ Quantity availability with only one tracked quant in a location. """ lot1 = self.env['stock.lot'].create({ 'name': 'lot1', 'product_id': self.product_lot.id, 'company_id': self.env.company.id, }) self.env['stock.quant'].create({ 'product_id': self.product_lot.id, 'location_id': self.stock_location.id, 'quantity': 10.0, 'reserved_quantity': 20.0, 'lot_id': lot1.id, }) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1), 0.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1, allow_negative=True), -10.0) def test_get_available_quantity_8(self): """ Quantity availability with a consumable product. """ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_consu, self.stock_location), 0.0) self.assertEqual(len(self.gather_relevant(self.product_consu, self.stock_location)), 0) with self.assertRaises(ValidationError): self.env['stock.quant']._update_available_quantity(self.product_consu, self.stock_location, 1.0) def test_get_available_quantity_9(self): """ Quantity availability by a demo user with access rights/rules. """ self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 1.0, }) self.env = self.env(user=self.demo_user) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0) def test_increase_available_quantity_1(self): """ Increase the available quantity when no quants are already in a location. """ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0) def test_increase_available_quantity_2(self): """ Increase the available quantity when multiple quants are already in a location. """ for i in range(2): self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 1.0, }) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0) self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 3.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2) def test_increase_available_quantity_3(self): """ Increase the available quantity when a concurrent transaction is already increasing the reserved quanntity for the same product. """ quant = self.env['stock.quant'].search([('location_id', '=', self.stock_location.id)], limit=1) if not quant: self.skipTest('Cannot test concurrent transactions without demo data.') product = quant.product_id available_quantity = self.env['stock.quant']._get_available_quantity(product, self.stock_location, allow_negative=True) # opens a new cursor and SELECT FOR UPDATE the quant, to simulate another concurrent reserved # quantity increase with closing(self.registry.cursor()) as cr: cr.execute("SELECT id FROM stock_quant WHERE product_id=%s AND location_id=%s", (product.id, self.stock_location.id)) quant_id = cr.fetchone() cr.execute("SELECT 1 FROM stock_quant WHERE id=%s FOR UPDATE", quant_id) self.env['stock.quant']._update_available_quantity(product, self.stock_location, 1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(product, self.stock_location, allow_negative=True), available_quantity + 1) self.assertEqual(len(self.gather_relevant(product, self.stock_location, strict=True)), 2) def test_increase_available_quantity_4(self): """ Increase the available quantity when no quants are already in a location with a user without access right. """ self.env = self.env(user=self.demo_user) self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0) def test_increase_available_quantity_5(self): """ Increase the available quantity when no quants are already in stock. Increase a subLocation and check that quants are in this location. Also test inverse. """ stock_sub_location = self.stock_location.child_ids[0] product2 = self.env['product.product'].create({ 'name': 'Product B', 'type': 'product', }) self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0) self.env['stock.quant']._update_available_quantity(self.product, stock_sub_location, 1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, stock_sub_location), 1.0) self.env['stock.quant']._update_available_quantity(product2, stock_sub_location, 1.0) self.env['stock.quant']._update_available_quantity(product2, self.stock_location, 1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(product2, self.stock_location), 2.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(product2, stock_sub_location), 1.0) def test_increase_available_quantity_6(self): """ Increasing the available quantity in a view location should be forbidden. """ location1 = self.env['stock.location'].create({ 'name': 'viewloc1', 'usage': 'view', 'location_id': self.stock_location.id, }) with self.assertRaises(ValidationError): self.env['stock.quant']._update_available_quantity(self.product, location1, 1.0) def test_increase_available_quantity_7(self): """ Setting a location's usage as "view" should be forbidden if it already contains quant. """ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0) self.assertTrue(len(self.stock_location.quant_ids.ids) > 0) with self.assertRaises(UserError): self.stock_location.usage = 'view' def test_decrease_available_quantity_1(self): """ Decrease the available quantity when no quants are already in a location. """ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, -1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, allow_negative=True), -1.0) def test_decrease_available_quantity_2(self): """ Decrease the available quantity when multiple quants are already in a location. """ for i in range(2): self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 1.0, }) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2) self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, -1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1) def test_decrease_available_quantity_3(self): """ Decrease the available quantity when a concurrent transaction is already increasing the reserved quanntity for the same product. """ quant = self.env['stock.quant'].search([('location_id', '=', self.stock_location.id)], limit=1) if not quant: self.skipTest('Cannot test concurrent transactions without demo data.') product = quant.product_id available_quantity = self.env['stock.quant']._get_available_quantity(product, self.stock_location, allow_negative=True) # opens a new cursor and SELECT FOR UPDATE the quant, to simulate another concurrent reserved # quantity increase with closing(self.registry.cursor()) as cr: cr.execute("SELECT 1 FROM stock_quant WHERE id = %s FOR UPDATE", quant.ids) self.env['stock.quant']._update_available_quantity(product, self.stock_location, -1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(product, self.stock_location, allow_negative=True), available_quantity - 1) self.assertEqual(len(self.gather_relevant(product, self.stock_location, strict=True)), 2) def test_decrease_available_quantity_4(self): """ Decrease the available quantity that delete the quant. The active user should have read,write and unlink rights """ self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 1.0, }) self.env = self.env(user=self.demo_user) self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, -1.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 0) def test_increase_reserved_quantity_1(self): """ Increase the reserved quantity of quantity x when there's a single quant in a given location which has an available quantity of x. """ self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 10.0, }) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1) self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 10.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1) def test_increase_reserved_quantity_2(self): """ Increase the reserved quantity of quantity x when there's two quants in a given location which have an available quantity of x together. """ for i in range(2): self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 5.0, }) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2) self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 10.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2) def test_increase_reserved_quantity_3(self): """ Increase the reserved quantity of quantity x when there's multiple quants in a given location which have an available quantity of x together. """ self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 5.0, 'reserved_quantity': 2.0, }) self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 10.0, 'reserved_quantity': 12.0, }) self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 8.0, 'reserved_quantity': 3.0, }) self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 35.0, 'reserved_quantity': 12.0, }) # total quantity: 58 # total reserved quantity: 29 self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 29.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 4) self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 10.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 19.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 4) def test_increase_reserved_quantity_4(self): """ Increase the reserved quantity of quantity x when there's multiple quants in a given location which have an available quantity of x together. """ self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 5.0, 'reserved_quantity': 7.0, }) self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 12.0, 'reserved_quantity': 10.0, }) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2) reserved_quants = self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 10.0) self.assertFalse(reserved_quants) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) def test_increase_reserved_quantity_5(self): """ Decrease the available quantity when no quant are in a location. """ reserved_quants = self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 1.0) self.assertFalse(reserved_quants) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) def test_decrease_reserved_quantity_1(self): self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 10.0, 'reserved_quantity': 10.0, }) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1) self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, -10.0, strict=True) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0) self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1) def test_action_done_1(self): pack_location = self.env.ref('stock.location_pack_zone') pack_location.active = True self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0) self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 2.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, -2.0, strict=True) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0) self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, -2.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) self.env['stock.quant']._update_available_quantity(self.product, pack_location, 2.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, pack_location), 2.0) def test_mix_tracked_untracked_1(self): lot1 = self.env['stock.lot'].create({ 'name': 'lot1', 'product_id': self.product_serial.id, 'company_id': self.env.company.id, }) # add one tracked, one untracked self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0) self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot1) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 2.0) self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot1, strict=True) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 1.0) self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, -1.0, lot_id=lot1, strict=True) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 2.0) def test_access_rights_1(self): """ Directly update the quant with a user with or without stock access rights should not raise an AccessError only deletion will. """ quant = self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 1.0, }) self.env = self.env(user=self.demo_user) with self.assertRaises(AccessError): self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 1.0, }) with self.assertRaises(AccessError): quant.with_user(self.demo_user).write({'quantity': 2.0}) with self.assertRaises(UserError): quant.with_user(self.demo_user).unlink() self.env = self.env(user=self.stock_user) self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 1.0, }) quant.with_user(self.stock_user).with_context(inventory_mode=True).write({'quantity': 3.0}) with self.assertRaises(AccessError): quant.with_user(self.stock_user).unlink() def test_quant_in_date_1(self): """ Check that no incoming date is set when updating the quantity of an untracked quant. """ quantity, in_date = self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0) self.assertEqual(quantity, 1) self.assertNotEqual(in_date, None) def test_quant_in_date_1b(self): self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_location.id, 'quantity': 1.0, }) quantity, in_date = self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0) self.assertEqual(quantity, 3) self.assertNotEqual(in_date, None) def test_quant_in_date_2(self): """ Check that an incoming date is correctly set when updating the quantity of a tracked quant. """ lot1 = self.env['stock.lot'].create({ 'name': 'lot1', 'product_id': self.product_serial.id, 'company_id': self.env.company.id, }) quantity, in_date = self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot1) self.assertEqual(quantity, 1) self.assertNotEqual(in_date, None) def test_quant_in_date_3(self): """ Check that the FIFO strategies correctly applies when you have multiple lot received at different times for a tracked product. """ lot1 = self.env['stock.lot'].create({ 'name': 'lot1', 'product_id': self.product_serial.id, 'company_id': self.env.company.id, }) lot2 = self.env['stock.lot'].create({ 'name': 'lot2', 'product_id': self.product_serial.id, 'company_id': self.env.company.id, }) in_date_lot1 = datetime.now() in_date_lot2 = datetime.now() - timedelta(days=5) self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot1, in_date=in_date_lot1) self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot2, in_date=in_date_lot2) quants = self.env['stock.quant']._get_reserve_quantity(self.product_serial, self.stock_location, 1.0) # Default removal strategy is FIFO, so lot2 should be received as it was received earlier. self.assertEqual(quants[0][0].lot_id.id, lot2.id) def test_in_date_4(self): """ Check that the LIFO strategies correctly applies when you have multiple lot received at different times for a tracked product. """ lifo_strategy = self.env['product.removal'].search([('method', '=', 'lifo')]) self.stock_location.removal_strategy_id = lifo_strategy lot1 = self.env['stock.lot'].create({ 'name': 'lot1', 'product_id': self.product_serial.id, 'company_id': self.env.company.id, }) lot2 = self.env['stock.lot'].create({ 'name': 'lot2', 'product_id': self.product_serial.id, 'company_id': self.env.company.id, }) in_date_lot1 = datetime.now() in_date_lot2 = datetime.now() - timedelta(days=5) self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot1, in_date=in_date_lot1) self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot2, in_date=in_date_lot2) self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, 1) quants = self.env['stock.quant'].search([('product_id', '=', self.product_serial.id), ('location_id', '=', self.stock_location.id)]) # Removal strategy is LIFO, so lot1 should be received as it was received later. self.assertEqual(quants[0][0].lot_id.id, lot1.id) def test_quant_in_date_5(self): """ Receive the same lot at different times, once they're in the same location, the quants are merged and only the earliest incoming date is kept. """ lot1 = self.env['stock.lot'].create({ 'name': 'lot1', 'product_id': self.product_lot.id, 'company_id': self.env.company.id, }) from odoo.fields import Datetime in_date1 = Datetime.now() self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 1.0, lot_id=lot1, in_date=in_date1) quant = self.env['stock.quant'].search([ ('product_id', '=', self.product_lot.id), ('location_id', '=', self.stock_location.id), ]) self.assertEqual(len(quant), 1) self.assertEqual(quant.quantity, 1) self.assertEqual(quant.lot_id.id, lot1.id) self.assertEqual(quant.in_date, in_date1) in_date2 = Datetime.now() - timedelta(days=5) self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 1.0, lot_id=lot1, in_date=in_date2) quant = self.env['stock.quant'].search([ ('product_id', '=', self.product_lot.id), ('location_id', '=', self.stock_location.id), ]) self.assertEqual(len(quant), 1) self.assertEqual(quant.quantity, 2) self.assertEqual(quant.lot_id.id, lot1.id) self.assertEqual(quant.in_date, in_date2) def test_closest_removal_strategy_tracked(self): """ Check that the Closest location strategy correctly applies when you have multiple lot received at different locations for a tracked product. """ # Enable multi-locations to be able to set an origin location for delivery grp_multi_loc = self.env.ref('stock.group_stock_multi_locations') self.env.user.write({'groups_id': [Command.link(grp_multi_loc.id)]}) closest_strategy = self.env['product.removal'].search([('method', '=', 'closest')]) self.stock_location.removal_strategy_id = closest_strategy lot1 = self.env['stock.lot'].create({ 'name': 'lot1', 'product_id': self.product_serial.id, 'company_id': self.env.company.id, }) lot2 = self.env['stock.lot'].create({ 'name': 'lot2', 'product_id': self.product_serial.id, 'company_id': self.env.company.id, }) in_date = datetime.now() # Add a product from lot1 in stock_location/subloc3 self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_subloc3, 1.0, lot_id=lot1, in_date=in_date) # Add a product from lot2 in stock_location/subloc2 self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_subloc2, 1.0, lot_id=lot2, in_date=in_date) # Require one unit of the product for a delivery with Form(self.env['stock.picking']) as picking_form: picking_form.picking_type_id = self.env.ref('stock.picking_type_out') picking_form.location_id = self.stock_location with picking_form.move_ids_without_package.new() as move_form: move_form.product_id = self.product_serial move_form.product_uom_qty = 1 picking = picking_form.save() picking.action_confirm() # Default removal strategy is 'Closest location', so lot2 should be received as it was put in a closer location. (stock_location/subloc2 < stock_location/subloc3) self.assertEqual(picking.move_ids.lot_ids.id, lot2.id) def test_closest_removal_strategy_untracked(self): """ Check that the Closest location strategy correctly applies when you have multiple products received at different locations for untracked products.""" closest_strategy = self.env['product.removal'].search([('method', '=', 'closest')]) self.stock_location.removal_strategy_id = closest_strategy # Add 2 units of product into stock_location/subloc2 self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_subloc2.id, 'quantity': 2.0, }) # Add 3 units of product into stock_location/subloc3 self.env['stock.quant'].create({ 'product_id': self.product.id, 'location_id': self.stock_subloc3.id, 'quantity': 3.0 }) # Request 3 units of product, with 'Closest location' as removal strategy quants = self.env['stock.quant']._get_reserve_quantity(self.product, self.stock_location, 3) # The 2 in stock_location/subloc2 should be taken first, as the location name is smaller alphabetically self.assertEqual(quants[0][1], 2) self.assertEqual(quants[0][0].location_id, self.stock_subloc2) # The last one should then be taken in stock_location/subloc3 since the first location doesn't have enough products self.assertEqual(quants[1][1], 1) self.assertEqual(quants[1][0].location_id, self.stock_subloc3) def test_in_date_6(self): """ One P in stock, P is delivered. Later on, a stock adjustement adds one P. This test checks the date value of the related quant """ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0) move = self.env['stock.move'].create({ 'name': 'OUT 1 product', 'product_id': self.product.id, 'product_uom_qty': 1, 'product_uom': self.product.uom_id.id, 'location_id': self.stock_location.id, 'location_dest_id': self.ref('stock.stock_location_customers'), }) move._action_confirm() move._action_assign() move.quantity = 1 move.picked = True move._action_done() tomorrow = fields.Datetime.now() + timedelta(days=1) with patch.object(fields.Datetime, 'now', lambda: tomorrow): move = self.env['stock.move'].create({ 'name': 'IN 1 product', 'product_id': self.product.id, 'product_uom_qty': 1, 'product_uom': self.product.uom_id.id, 'location_id': self.ref('stock.stock_location_suppliers'), 'location_dest_id': self.stock_location.id, }) move._action_confirm() move._action_assign() move.quantity = 1 move.picked = True move._action_done() quant = self.env['stock.quant'].search([('product_id', '=', self.product.id), ('location_id', '=', self.stock_location.id), ('quantity', '>', 0)]) self.assertEqual(quant.in_date, tomorrow) def test_quant_creation(self): """ This test ensures that, after an internal transfer, the values of the created quand are correct """ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 10.0) move = self.env['stock.move'].create({ 'name': 'Move 1 product', 'product_id': self.product.id, 'product_uom_qty': 1, 'product_uom': self.product.uom_id.id, 'location_id': self.stock_location.id, 'location_dest_id': self.stock_subloc2.id, }) move._action_confirm() move._action_assign() move.quantity = 1 move.picked = True move._action_done() quant = self.gather_relevant(self.product, self.stock_subloc2) self.assertFalse(quant.inventory_quantity_set) def test_unpack_and_quants_merging(self): """ When unpacking a package, if there are already some quantities of the packed product in the stock, the quant of the on hand quantity and the one of the package should be merged """ stock_location = self.env['stock.warehouse'].search([], limit=1).lot_stock_id supplier_location = self.env.ref('stock.stock_location_suppliers') picking_type_in = self.env.ref('stock.picking_type_in') self.env['stock.quant']._update_available_quantity(self.product, stock_location, 1.0) picking = self.env['stock.picking'].create({ 'picking_type_id': picking_type_in.id, 'location_id': supplier_location.id, 'location_dest_id': stock_location.id, 'move_ids': [(0, 0, { 'name': 'In 10 x %s' % self.product.name, 'product_id': self.product.id, 'location_id': supplier_location.id, 'location_dest_id': stock_location.id, 'product_uom_qty': 10, 'product_uom': self.product.uom_id.id, })], 'state': 'draft', }) picking.action_confirm() package = self.env['stock.quant.package'].create({ 'name': 'Super Package', }) picking.move_ids.move_line_ids.write({ 'quantity': 10, 'result_package_id': package.id, }) picking.move_ids.picked = True picking.button_validate() package.unpack() quant = self.env['stock.quant'].search([('product_id', '=', self.product.id), ('on_hand', '=', True)]) self.assertEqual(len(quant), 1) # The quants merging is processed thanks to a SQL query (see StockQuant._merge_quants). # At that point, the ORM is not aware of the new value. So we need to invalidate the # cache to ensure that the value will be the newest quant.invalidate_recordset(['quantity']) self.assertEqual(quant.quantity, 11) def test_quant_display_name(self): """ Check the display name of a quant. """ self.env.user.groups_id += self.env.ref('stock.group_production_lot') sn1 = self.env['stock.lot'].create({ 'name': 'sn1', 'product_id': self.product_serial.id, 'company_id': self.env.company.id, }) lot1 = self.env['stock.lot'].create({ 'name': 'lot1', 'product_id': self.product_lot.id, 'company_id': self.env.company.id, }) self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0) self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 1.0, lot_id=lot1) self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=sn1) quants = self.stock_location.quant_ids for q in quants: if q.lot_id: self.assertEqual(q.display_name, '%s - %s' % (q.location_id.display_name, q.lot_id.name)) else: self.assertEqual(q.display_name, '%s' % (q.location_id.display_name)) def test_serial_constraint_with_package_and_return(self): """ Receive product with serial S Return it in a package Confirm a new receipt with S """ stock_location = self.env['stock.warehouse'].search([], limit=1).lot_stock_id supplier_location = self.env.ref('stock.stock_location_suppliers') picking_type_in = self.env.ref('stock.picking_type_in') receipt01 = self.env['stock.picking'].create({ 'picking_type_id': picking_type_in.id, 'location_id': supplier_location.id, 'location_dest_id': stock_location.id, 'move_ids': [(0, 0, { 'name': self.product_serial.name, 'product_id': self.product_serial.id, 'location_id': supplier_location.id, 'location_dest_id': stock_location.id, 'product_uom_qty': 1, 'product_uom': self.product_serial.uom_id.id, })], }) receipt01.action_confirm() receipt01.move_line_ids.write({ 'lot_name': 'Michel', 'quantity': 1.0 }) receipt01.button_validate() quant = self.env['stock.quant'].search([('product_id', '=', self.product_serial.id), ('location_id', '=', stock_location.id)]) wizard_form = Form(self.env['stock.return.picking'].with_context(active_ids=receipt01.ids, active_id=receipt01.ids[0], active_model='stock.picking')) wizard = wizard_form.save() wizard.product_return_moves.quantity = 1.0 stock_return_picking_action = wizard.create_returns() return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id']) return_pick.move_ids.move_line_ids.quantity = 1.0 return_pick.action_put_in_pack() return_pick.move_ids.picked = True return_pick._action_done() self.assertEqual(return_pick.move_line_ids.lot_id, quant.lot_id) self.assertTrue(return_pick.move_line_ids.result_package_id, quant.lot_id) receipt02 = self.env['stock.picking'].create({ 'picking_type_id': picking_type_in.id, 'location_id': supplier_location.id, 'location_dest_id': stock_location.id, 'move_ids': [(0, 0, { 'name': self.product_serial.name, 'product_id': self.product_serial.id, 'location_id': supplier_location.id, 'location_dest_id': stock_location.id, 'product_uom_qty': 1, 'product_uom': self.product_serial.uom_id.id, })], }) receipt02.action_confirm() receipt02.move_line_ids.write({ 'lot_name': 'Michel', 'quantity': 1.0 }) receipt02.button_validate() quant = self.env['stock.quant'].search([('product_id', '=', self.product_serial.id), ('location_id', '=', stock_location.id)]) self.assertEqual(len(quant), 1) self.assertEqual(quant.lot_id.name, 'Michel') def test_update_quant_with_forbidden_field(self): """ Test that updating a quant with a forbidden field raise an error. """ product = self.env['product.product'].create({ 'name': 'Product', 'type': 'product', 'tracking': 'serial', }) sn1 = self.env['stock.lot'].create({ 'name': 'SN1', 'product_id': product.id, }) self.env['stock.quant']._update_available_quantity(product, self.stock_subloc2, 1.0, lot_id=sn1) self.assertEqual(len(product.stock_quant_ids), 1) self.env['stock.quant']._update_available_quantity(product, self.stock_subloc3, 1.0, lot_id=sn1) self.assertEqual(len(product.stock_quant_ids), 2) quant_2 = product.stock_quant_ids[1] self.assertEqual(quant_2.with_context(inventory_mode=True).sn_duplicated, True) with self.assertRaises(UserError): quant_2.with_context(inventory_mode=True).write({'location_id': self.stock_subloc2}) def test_update_quant_with_forbidden_field_02(self): """ Test that updating the package from the quant raise an error but if the package is unpacked, the quant can be updated. """ package = self.env['stock.quant.package'].create({ 'name': 'Package', }) self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, package_id=package) quant = self.product.stock_quant_ids self.assertEqual(len(self.product.stock_quant_ids), 1) with self.assertRaises(UserError): quant.with_context(inventory_mode=True).write({'package_id': False}) package.with_context(inventory_mode=True).unpack() self.assertFalse(quant.exists()) self.assertFalse(self.product.stock_quant_ids.package_id) def test_relocate(self): """ Test the relocation wizard. """ def _get_relocate_wizard(quant_ids): relocate_wizard_dict = quant_ids.action_stock_quant_relocate() return Form(self.env[relocate_wizard_dict['res_model']].with_context(relocate_wizard_dict['context'])) self.env.user.write({'groups_id': [(4, self.env.ref('stock.group_tracking_lot').id)]}) package_01 = self.env['stock.quant.package'].create({}) package_02 = self.env['stock.quant.package'].create({}) self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 10, package_id=package_01) quant_a = self.env['stock.quant'].search([('product_id', '=', self.product.id)]) # testing assigning a package to a quant relocate_wizard = _get_relocate_wizard(quant_a) relocate_wizard.dest_package_id = package_02 relocate_wizard.save().action_relocate_quants() new_quant_a = self.env['stock.quant'].search([('product_id', '=', self.product.id), ('quantity', '=', 10)]) self.assertEqual(new_quant_a.package_id, package_02) # testing moving a packed quant to a new location relocate_wizard = _get_relocate_wizard(new_quant_a) self.assertEqual(relocate_wizard.is_partial_package, False) relocate_wizard.dest_location_id = self.stock_subloc2 relocate_wizard.save().action_relocate_quants() new_quant_a_bis = self.env['stock.quant'].search([('product_id', '=', self.product.id), ('quantity', '=', 10)]) self.assertEqual(new_quant_a_bis.location_id, self.stock_subloc2) self.assertEqual(new_quant_a_bis.package_id, package_02) # testing moving multiple packed quants to a new location with incomplete package product_b = self.env['product.product'].create({ 'name': 'product B', 'type': 'product' }) self.env['stock.quant']._update_available_quantity(product_b, self.stock_location, 10, package_id=package_01) product_c = self.env['product.product'].create({ 'name': 'product C', 'type': 'product' }) self.env['stock.quant']._update_available_quantity(product_c, self.stock_location, 10, package_id=package_01) quants_ab = self.env['stock.quant'].search([('product_id', 'in', (self.product.id, product_b.id)), ('quantity', '=', 10)]) relocate_wizard = _get_relocate_wizard(quants_ab) self.assertEqual(relocate_wizard.is_partial_package, True) relocate_wizard.dest_location_id = self.stock_subloc3 relocate_wizard.save().action_relocate_quants() new_quants_abc = self.env['stock.quant'].search([('product_id', 'in', (self.product.id, product_b.id, product_c.id)), ('quantity', '=', 10)], order='product_id') self.assertRecordValues(new_quants_abc, [ {'product_id': self.product.id, 'location_id': self.stock_subloc3.id, 'package_id': package_02.id}, {'product_id': product_b.id, 'location_id': self.stock_subloc3.id, 'package_id': False}, {'product_id': product_c.id, 'location_id': self.stock_location.id, 'package_id': package_01.id}, ]) ### CURRENT STATE # COMPANY A # product A (self.product): stock_subloc3, package_02 # product B: stock_subloc3, no package # product C: stock_location, package_01 ### testing blocks on relocating quants from different companies package_03 = self.env['stock.quant.package'].create({}) package_04 = self.env['stock.quant.package'].create({}) company_B = self.env['res.company'].create({ 'name': 'company B', 'currency_id': self.env.ref('base.USD').id }) location_company_B = self.env['stock.location'].create({ 'name': 'stock location company B', 'usage': 'internal', 'company_id': company_B.id }) product_a_company_B = self.env['product.product'].create({ 'name': 'product A company B', 'type': 'product', 'company_id': company_B.id }) product_b_company_B = self.env['product.product'].create({ 'name': 'product b company B', 'type': 'product', 'company_id': company_B.id }) self.env['stock.quant']._update_available_quantity(product_a_company_B, location_company_B, 10, package_id=package_03) self.env['stock.quant']._update_available_quantity(product_b_company_B, location_company_B, 10) # testing the available packs from company B quant_b_B = self.env['stock.quant'].search([('product_id', '=', product_b_company_B.id), ('quantity', '=', 10)]) relocate_wizard = _get_relocate_wizard(quant_b_B) self.assertEqual(relocate_wizard.dest_package_id.search(literal_eval(relocate_wizard.dest_package_id_domain)), package_03+package_04) # testing the available packs from company A with multiple quants quants_ab_A = self.env['stock.quant'].search([('product_id', 'in', (self.product.id, product_b.id)), ('quantity', '=', 10)]) relocate_wizard = _get_relocate_wizard(quants_ab_A) self.assertEqual(relocate_wizard.dest_package_id.search(literal_eval(relocate_wizard.dest_package_id_domain)), package_02+package_04) # testing the recomputation of available packages relocate_wizard.dest_location_id = self.stock_location self.assertEqual(relocate_wizard.dest_package_id.search(literal_eval(relocate_wizard.dest_package_id_domain)), package_01+package_04) # testing calling the wizard with quants from multiple companies quants_bab_AB = quant_b_B + quants_ab_A with self.assertRaises(UserError): _get_relocate_wizard(quants_bab_AB) def test_inventory_adjustment_package(self): """ With the changes implemented in _get_inventory_move_values(), we want to make sure that it correctly writes the package and destination package for inventory adjustments in _apply_inventory(). """ dummy_product = self.env['product.product'].create({'name': 'dummy product', 'type': 'product'}) dummy_package = self.env['stock.quant.package'].create({'name': 'dummy package'}) dummy_quant = self.env['stock.quant'].create({ 'product_id': dummy_product.id, 'location_id': self.stock_location.id, 'package_id': dummy_package.id, 'inventory_quantity': 42 }) dummy_quant.action_apply_inventory() creation_move_line = self.env['stock.move.line'].search([('product_id', '=', dummy_product.id)]) self.assertEqual(creation_move_line.package_id.id, False, "There should be no origin package") self.assertEqual(creation_move_line.result_package_id.id, dummy_package.id, "The destination package should be the dummy package") self.assertEqual(creation_move_line.location_dest_id.id, self.stock_location.id, "The destination location should be the stock location") dummy_quant.inventory_quantity = 0 dummy_quant.action_apply_inventory() destruction_move_line = self.env['stock.move.line'].search([('product_id', '=', dummy_product.id), ('id', '!=', creation_move_line.id)]) self.assertEqual(destruction_move_line.package_id.id, dummy_package.id, "The origin package should be the dummy package") self.assertEqual(destruction_move_line.result_package_id.id, False, "The destination package should be False") self.assertEqual(destruction_move_line.location_id.id, self.stock_location.id, "The origin location should be the stock location") self.assertEqual(destruction_move_line.location_dest_id.id, creation_move_line.location_id.id) self.assertEqual(dummy_quant.quantity, 0) class StockQuantRemovalStrategy(TransactionCase): def setUp(self): super().setUp() self.least_package_strategy = self.env['product.removal'].search( [('method', '=', 'least_packages')]) self.product = self.env['product.product'].create({ 'name': 'Product', 'type': 'product', }) self.product.categ_id.removal_strategy_id = self.least_package_strategy.id self.stock_location = self.env['stock.location'].create({ 'name': 'stock_location', 'usage': 'internal', }) def _generate_data(self, packages_data): move = self.env['stock.move'].create({ 'name': 'Test Least Package', 'product_id': self.product.id, 'product_uom': self.product.uom_id.id, 'location_id': self.ref('stock.stock_location_suppliers'), 'location_dest_id': self.stock_location.id, }) move._action_confirm() ml_vals_list = [] ml_common_vals = { 'move_id': move.id, 'product_id': self.product.id, 'product_uom_id': self.product.uom_id.id, 'location_id': self.ref('stock.stock_location_suppliers'), 'location_dest_id': self.stock_location.id, } packages = self.env['stock.quant.package'].create( [{}] * sum(p[1] for p in packages_data if p[0])) for package_size, number_of_packages in packages_data: if not package_size: ml_vals_list.append(dict(**ml_common_vals, **{ 'quantity': number_of_packages, })) continue for dummy in range(number_of_packages): package = packages[0] packages = packages[1:] ml_vals_list.append(dict(**ml_common_vals, **{ 'quantity': package_size, 'result_package_id': package.id, })) self.env['stock.move.line'].create(ml_vals_list) move.picked = True move._action_done() def test_least_package_removal_strategy_priority_to_package(self): """ Tests the least package removal strategy in a use case where only one package needs to be selected. It should only return the quantity of a single size 1000 package. """ packages_data = [ (False, 2000), (5, 10), (50, 10), (1000, 2), ] self._generate_data(packages_data) # Out 1000 should selecte a package with 1000 units inside move = self.env['stock.move'].create({ 'name': 'Test Least Package', 'product_id': self.product.id, 'product_uom': self.product.uom_id.id, 'location_id': self.stock_location.id, 'location_dest_id': self.ref('stock.stock_location_customers'), 'product_uom_qty': 1000, }) move._action_confirm() move._action_assign() self.assertEqual(len(move.move_line_ids), 1, 'Only one pack could be use') self.assertTrue( move.move_line_ids.package_id, 'A package should be selected, priority to package even if there is enough quantity without package' ) def test_least_package_removal_strategy_simple_usecase(self): """ Tests the least package removal strategy in a simple "typical" use case. It should return a minimal exact matching for the requested quantity. """ packages_data = [ (5, 10), (50, 10), (1000, 2), ] self._generate_data(packages_data) # Out 1000 should select a package with 1000 units inside move = self.env['stock.move'].create({ 'name': 'Test Least Package', 'product_id': self.product.id, 'product_uom': self.product.uom_id.id, 'location_id': self.stock_location.id, 'location_dest_id': self.ref('stock.stock_location_customers'), 'product_uom_qty': 1280, }) move._action_confirm() move._action_assign() self.assertEqual(len(move.move_line_ids), 12) self.assertRecordValues( move.move_line_ids, [{'quantity_product_uom': 1000}] + [{'quantity_product_uom': 50}] * 5 + [{'quantity_product_uom': 5}] * 6 ) def test_least_package_removal_strategy_not_possible(self): """ Tests the least package removal strategy in the case where an exact matching of packages is not possible for the requested amount. It should return the best leaf from the A* search. """ packages_data = [ (False, 2), (5, 2), (10, 5), ] self._generate_data(packages_data) move = self.env['stock.move'].create({ 'name': 'Test Least Package', 'product_id': self.product.id, 'product_uom': self.product.uom_id.id, 'location_id': self.stock_location.id, 'location_dest_id': self.ref('stock.stock_location_customers'), 'product_uom_qty': 13, }) move._action_confirm() move._action_assign() self.assertEqual(len(move.move_line_ids), 2) self.assertRecordValues( move.move_line_ids, [{'quantity_product_uom': 10}] + [{'quantity_product_uom': 3}] ) # Make sure it selects the smallest possible package as best leaf. self.assertEqual( move.move_line_ids[1].package_id.quant_ids.quantity, 5 ) def test_least_package_removal_strategy_not_enough(self): """ Tests the least package removal strategy in the case where not enough quantity is available to fill the requested amount. It should just return all the quantities in the domain. """ packages_data = [ (False, 2), (5, 2), (10, 5), ] self._generate_data(packages_data) move = self.env['stock.move'].create({ 'name': 'Test Least Package', 'product_id': self.product.id, 'product_uom': self.product.uom_id.id, 'location_id': self.stock_location.id, 'location_dest_id': self.ref('stock.stock_location_customers'), 'product_uom_qty': 90, }) move._action_confirm() move._action_assign() self.assertEqual(len(move.move_line_ids), 8) self.assertRecordValues( move.move_line_ids, [{'quantity_product_uom': 2}] + [{'quantity_product_uom': 10}] * 5 + [{'quantity_product_uom': 5}] * 2 ) def test_clean_quant_after_package_move(self): """ A product is at WH/Stock in a package PK. We deliver PK. The user should not find any quant at WH/Stock with PK anymore. """ package = self.env['stock.quant.package'].create({}) self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, package_id=package) move = self.env['stock.move'].create({ 'name': 'OUT 1 product', 'product_id': self.product.id, 'product_uom_qty': 1, 'product_uom': self.product.uom_id.id, 'location_id': self.stock_location.id, 'location_dest_id': self.ref('stock.stock_location_customers'), }) move._action_confirm() move._action_assign() move.move_line_ids.write({ 'result_package_id': package.id, 'quantity': 1, }) move.picked = True move._action_done() self.assertFalse(self.env['stock.quant'].search_count([ ('product_id', '=', self.product.id), ('package_id', '=', package.id), ('location_id', '=', self.stock_location.id), ]))