summaryrefslogtreecommitdiff
path: root/addons/stock/tests/test_move.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/stock/tests/test_move.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/stock/tests/test_move.py')
-rw-r--r--addons/stock/tests/test_move.py4574
1 files changed, 4574 insertions, 0 deletions
diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py
new file mode 100644
index 00000000..eb52068e
--- /dev/null
+++ b/addons/stock/tests/test_move.py
@@ -0,0 +1,4574 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.exceptions import UserError
+from odoo.tests import Form
+from odoo.tests.common import SavepointCase
+
+
+class StockMove(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(StockMove, cls).setUpClass()
+ cls.stock_location = cls.env.ref('stock.stock_location_stock')
+ cls.customer_location = cls.env.ref('stock.stock_location_customers')
+ cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
+ cls.pack_location = cls.env.ref('stock.location_pack_zone')
+ cls.pack_location.active = True
+ cls.transit_location = cls.env['stock.location'].search([
+ ('company_id', '=', cls.env.company.id),
+ ('usage', '=', 'transit'),
+ ('active', '=', False)
+ ], limit=1)
+ cls.transit_location.active = True
+ cls.uom_unit = cls.env.ref('uom.product_uom_unit')
+ cls.uom_dozen = cls.env.ref('uom.product_uom_dozen')
+ cls.product = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.product_serial = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'tracking': 'serial',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.product_lot = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'tracking': 'lot',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.product_consu = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'consu',
+ 'categ_id': cls.env.ref('product.product_category_all').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_in_1(self):
+ """ Receive products from a supplier. Check that a move line is created and that the
+ reception correctly increase a single quant in stock.
+ """
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # fill the move line
+ move_line = move1.move_line_ids[0]
+ self.assertEqual(move_line.product_qty, 100.0)
+ self.assertEqual(move_line.qty_done, 0.0)
+ move_line.qty_done = 100.0
+
+ # validation
+ move1._action_done()
+ self.assertEqual(move1.state, 'done')
+ # no quants are created in the supplier location
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.supplier_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.supplier_location, allow_negative=True), -100.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 100.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.supplier_location)), 1.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ def test_in_2(self):
+ """ Receive 5 tracked products from a supplier. The create move line should have 5
+ reserved. If i assign the 5 items to lot1, the reservation should not change. Once
+ i validate, the reception correctly increase a single quant in stock.
+ """
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ move_line = move1.move_line_ids[0]
+ self.assertEqual(move_line.product_qty, 5)
+ move_line.lot_name = 'lot1'
+ move_line.qty_done = 5.0
+ self.assertEqual(move_line.product_qty, 5) # don't change reservation
+
+ move1._action_done()
+ self.assertEqual(move_line.product_qty, 0) # change reservation to 0 for done move
+ self.assertEqual(move1.state, 'done')
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.supplier_location), 0.0)
+ supplier_quants = self.gather_relevant(self.product_lot, self.supplier_location)
+ self.assertEqual(sum(supplier_quants.mapped('quantity')), -5.0)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location), 5.0)
+ self.assertEqual(len(self.gather_relevant(self.product_lot, self.supplier_location)), 1.0)
+ quants = self.gather_relevant(self.product_lot, self.stock_location)
+ self.assertEqual(len(quants), 1.0)
+ for quant in quants:
+ self.assertNotEqual(quant.in_date, False)
+
+ def test_in_3(self):
+ """ Receive 5 serial-tracked products from a supplier. The system should create 5 different
+ move line.
+ """
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 5)
+ move_line = move1.move_line_ids[0]
+ self.assertEqual(move1.reserved_availability, 5)
+
+ i = 0
+ for move_line in move1.move_line_ids:
+ move_line.lot_name = 'sn%s' % i
+ move_line.qty_done = 1
+ i += 1
+ self.assertEqual(move1.quantity_done, 5.0)
+ self.assertEqual(move1.product_qty, 5) # don't change reservation
+
+ move1._action_done()
+
+ self.assertEqual(move1.quantity_done, 5.0)
+ self.assertEqual(move1.product_qty, 5) # don't change reservation
+ self.assertEqual(move1.state, 'done')
+
+ # Quant balance should result with 5 quant in supplier and stock
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.supplier_location), 0.0)
+ supplier_quants = self.gather_relevant(self.product_serial, self.supplier_location)
+ self.assertEqual(sum(supplier_quants.mapped('quantity')), -5.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 5.0)
+
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.supplier_location)), 5.0)
+ quants = self.gather_relevant(self.product_serial, self.stock_location)
+ self.assertEqual(len(quants), 5.0)
+ for quant in quants:
+ self.assertNotEqual(quant.in_date, False)
+
+ def test_out_1(self):
+ """ Send products to a client. Check that a move line is created reserving products in
+ stock and that the delivery correctly remove the single quant in stock.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 100)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 100.0)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_out_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ # Should be a reserved quantity and thus a quant.
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ # fill the move line
+ move_line = move1.move_line_ids[0]
+ self.assertEqual(move_line.product_qty, 100.0)
+ self.assertEqual(move_line.qty_done, 0.0)
+ move_line.qty_done = 100.0
+
+ # validation
+ move1._action_done()
+ self.assertEqual(move1.state, 'done')
+ # Check there is one quant in customer location
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 100.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.customer_location)), 1.0)
+ # there should be no quant amymore in the stock location
+ 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)), 0.0)
+
+ def test_out_2(self):
+ """ Send a consumable product to a client. Check that a move line is created but
+ quants are not impacted.
+ """
+ # make some stock
+
+ self.product.type = 'consu'
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_out_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ # Should be a reserved quantity and thus a quant.
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 0.0)
+
+ # fill the move line
+ move_line = move1.move_line_ids[0]
+ self.assertEqual(move_line.product_qty, 100.0)
+ self.assertEqual(move_line.qty_done, 0.0)
+ move_line.qty_done = 100.0
+
+ # validation
+ move1._action_done()
+ self.assertEqual(move1.state, 'done')
+ # no quants are created in the customer location since it's a consumable
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.customer_location)), 0.0)
+ # there should be no quant amymore in the stock location
+ 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)), 0.0)
+
+ def test_mixed_tracking_reservation_1(self):
+ """ Send products tracked by lot to a customer. In your stock, there are tracked and
+ untracked quants. Two moves lines should be created: one for the tracked ones, another
+ for the untracked ones.
+ """
+ lot1 = self.env['stock.production.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_lot, self.stock_location, 2)
+ self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 3, lot_id=lot1)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location), 5.0)
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(len(move1.move_line_ids), 2)
+
+ def test_mixed_tracking_reservation_2(self):
+ """ Send products tracked by lot to a customer. In your stock, there are two tracked and
+ mulitple untracked quants. There should be as many move lines as there are quants
+ reserved. Edit the reserve move lines to set them to new serial numbers, the reservation
+ should stay. Validate and the final quantity in stock should be 0, not negative.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 2)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 4.0)
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 4.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(len(move1.move_line_ids), 4)
+ for ml in move1.move_line_ids:
+ self.assertEqual(ml.product_qty, 1.0)
+
+ # assign lot3 and lot 4 to both untracked move lines
+ lot3 = self.env['stock.production.lot'].create({
+ 'name': 'lot3',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot4 = self.env['stock.production.lot'].create({
+ 'name': 'lot4',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ untracked_move_line = move1.move_line_ids.filtered(lambda ml: not ml.lot_id)
+ untracked_move_line[0].lot_id = lot3
+ untracked_move_line[1].lot_id = lot4
+ for ml in move1.move_line_ids:
+ self.assertEqual(ml.product_qty, 1.0)
+
+ # no changes on quants, even if i made some move lines with a lot id whom reserved on untracked quants
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, strict=True)), 1.0) # with a qty of 2
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot1, strict=True).filtered(lambda q: q.lot_id)), 1.0)
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot2, strict=True).filtered(lambda q: q.lot_id)), 1.0)
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot3, strict=True).filtered(lambda q: q.lot_id)), 0)
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot4, strict=True).filtered(lambda q: q.lot_id)), 0)
+
+ move1.move_line_ids.write({'qty_done': 1.0})
+
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot2, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot3, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot4, strict=True), 0.0)
+
+ def test_mixed_tracking_reservation_3(self):
+ """ Send two products tracked by lot to a customer. In your stock, there two tracked quants
+ and two untracked. Once the move is validated, add move lines to also move the two untracked
+ ones and assign them serial numbers on the fly. The final quantity in stock should be 0, not
+ negative.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot2)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0)
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.write({'qty_done': 1.0})
+ move1._action_done()
+
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 2)
+ lot3 = self.env['stock.production.lot'].create({
+ 'name': 'lot3',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot4 = self.env['stock.production.lot'].create({
+ 'name': 'lot4',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 1,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ 'lot_id': lot3.id,
+ })
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 1,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ 'lot_id': lot4.id
+ })
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot2, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot3, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot4, strict=True), 0.0)
+
+ def test_mixed_tracking_reservation_4(self):
+ """ Send two products tracked by lot to a customer. In your stock, there two tracked quants
+ and on untracked. Once the move is validated, edit one of the done move line to change the
+ serial number to one that is not in stock. The original serial should go back to stock and
+ the untracked quant should be tracked on the fly and sent instead.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot2)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0)
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.write({'qty_done': 1.0})
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot2, strict=True), 0.0)
+
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1)
+ lot3 = self.env['stock.production.lot'].create({
+ 'name': 'lot3',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+
+ move1.move_line_ids[1].lot_id = lot3
+
+ 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, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot2, strict=True), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot3, strict=True), 0.0)
+
+ def test_mixed_tracking_reservation_5(self):
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_jenaimarre_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # create an untracked quant
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0)
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # create a new move line with a lot not assigned to any quant
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 1,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ 'lot_id': lot1.id
+ })
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(move1.reserved_availability, 0)
+
+ # validating the move line should move the lot, not create a negative quant in stock
+ move1._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location)), 0.0)
+
+ def test_mixed_tracking_reservation_6(self):
+ # create an untracked quant
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_jenaimarre_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+
+ move_line = move1.move_line_ids
+ move_line.lot_id = lot1
+ self.assertEqual(move_line.product_qty, 1.0)
+ move_line.lot_id = lot2
+ self.assertEqual(move_line.product_qty, 1.0)
+ move_line.qty_done = 1
+
+ # validating the move line should move the lot, not create a negative quant in stock
+ move1._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location)), 0.0)
+
+ def test_mixed_tracking_reservation_7(self):
+ """ Similar test_mixed_tracking_reservation_2 but creates first the tracked quant, then the
+ untracked ones. When adding a lot to the untracked move line, it should not decrease the
+ untracked quant then increase a non-existing tracked one that will fallback on the
+ untracked quant.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0)
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(len(move1.move_line_ids), 2)
+ for ml in move1.move_line_ids:
+ self.assertEqual(ml.product_qty, 1.0)
+
+ untracked_move_line = move1.move_line_ids.filtered(lambda ml: not ml.lot_id).lot_id = lot2
+ for ml in move1.move_line_ids:
+ self.assertEqual(ml.product_qty, 1.0)
+
+ move1.move_line_ids.write({'qty_done': 1.0})
+
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot2, strict=True), 0.0)
+ quants = self.gather_relevant(self.product_serial, self.stock_location)
+ self.assertEqual(len(quants), 0)
+
+ def test_mixed_tracking_reservation_8(self):
+ """ Send one product tracked by lot to a customer. In your stock, there are one tracked and
+ one untracked quant. Reserve the move, then edit the lot to one not present in stock. The
+ system will update the reservation and use the untracked quant. Now unreserve, no error
+ should happen
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # at first, we only make the tracked quant available in stock to make sure this one is selected
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot1)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_mixed_tracking_reservation_7',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(move1.move_line_ids.lot_id.id, lot1.id)
+
+ # change the lot_id to one not available in stock while an untracked quant is available
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1)
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ move1.move_line_ids.lot_id = lot2
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(move1.move_line_ids.lot_id.id, lot2.id)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 1.0)
+
+ # unreserve
+ move1._do_unreserve()
+
+ self.assertEqual(move1.reserved_availability, 0.0)
+ self.assertEqual(len(move1.move_line_ids), 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, strict=True), 2.0)
+
+ def test_putaway_1(self):
+ """ Receive products from a supplier. Check that putaway rules are rightly applied on
+ the receipt move line.
+ """
+ # This test will apply a putaway strategy on the stock location to put everything
+ # incoming in the sublocation shelf1.
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ # putaway from stock to shelf1
+ putaway = self.env['stock.putaway.rule'].create({
+ 'category_id': self.env.ref('product.product_category_all').id,
+ 'location_in_id': self.stock_location.id,
+ 'location_out_id': shelf1_location.id,
+ })
+ self.stock_location.write({
+ 'putaway_rule_ids': [(4, putaway.id, 0)]
+ })
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf1_location.id)
+
+ def test_putaway_2(self):
+ """ Receive products from a supplier. Check that putaway rules are rightly applied on
+ the receipt move line.
+ """
+ # This test will apply a putaway strategy by product on the stock location to put everything
+ # incoming in the sublocation shelf1.
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ # putaway from stock to shelf1
+ putaway = self.env['stock.putaway.rule'].create({
+ 'product_id': self.product.id,
+ 'location_in_id': self.stock_location.id,
+ 'location_out_id': shelf1_location.id,
+ })
+ self.stock_location.write({
+ 'putaway_rule_ids': [(4, putaway.id, 0)],
+ })
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_2',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf1_location.id)
+
+ def test_putaway_3(self):
+ """ Receive products from a supplier. Check that putaway rules are rightly applied on
+ the receipt move line.
+ """
+ # This test will apply both the putaway strategy by product and category. We check here
+ # that the putaway by product takes precedence.
+
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf2',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ putaway_category = self.env['stock.putaway.rule'].create({
+ 'category_id': self.env.ref('product.product_category_all').id,
+ 'location_in_id': self.supplier_location.id,
+ 'location_out_id': shelf1_location.id,
+ })
+ putaway_product = self.env['stock.putaway.rule'].create({
+ 'product_id': self.product.id,
+ 'location_in_id': self.supplier_location.id,
+ 'location_out_id': shelf2_location.id,
+ })
+ self.stock_location.write({
+ 'putaway_rule_ids': [(6, 0, [
+ putaway_category.id,
+ putaway_product.id
+ ])],
+ })
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_3',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf2_location.id)
+
+ def test_putaway_4(self):
+ """ Receive products from a supplier. Check that putaway rules are rightly applied on
+ the receipt move line.
+ """
+ # This test will apply both the putaway strategy by product and category. We check here
+ # that if a putaway by product is not matched, the fallback to the category is correctly
+ # done.
+
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf2',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ # putaway from stock to shelf1
+ putaway_category = self.env['stock.putaway.rule'].create({
+ 'category_id': self.env.ref('product.product_category_all').id,
+ 'location_in_id': self.stock_location.id,
+ 'location_out_id': shelf1_location.id,
+ })
+ putaway_product = self.env['stock.putaway.rule'].create({
+ 'product_id': self.product_consu.id,
+ 'location_in_id': self.stock_location.id,
+ 'location_out_id': shelf2_location.id,
+ })
+ self.stock_location.write({
+ 'putaway_rule_ids': [(6, 0, [
+ putaway_category.id,
+ putaway_product.id,
+ ])],
+ })
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_4',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf1_location.id)
+
+ def test_putaway_5(self):
+ """ Receive products from a supplier. Check that putaway rules are rightly applied on
+ the receipt move line.
+ """
+ # This test will apply putaway strategy by category.
+ # We check here that the putaway by category works when the category is
+ # set on parent category of the product.
+
+ shelf_location = self.env['stock.location'].create({
+ 'name': 'shelf',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ putaway = self.env['stock.putaway.rule'].create({
+ 'category_id': self.env.ref('product.product_category_all').id,
+ 'location_in_id': self.supplier_location.id,
+ 'location_out_id': shelf_location.id,
+ })
+ self.stock_location.write({
+ 'putaway_rule_ids': [(6, 0, [
+ putaway.id,
+ ])],
+ })
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_5',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf_location.id)
+
+ def test_putaway_6(self):
+ """ Receive products from a supplier. Check that putaway rules are rightly applied on
+ the receipt move line.
+ """
+ # This test will apply two putaway strategies by category. We check here
+ # that the most specific putaway takes precedence.
+
+ child_category = self.env['product.category'].create({
+ 'name': 'child_category',
+ 'parent_id': self.ref('product.product_category_all'),
+ })
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf2',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ putaway_category_all = self.env['stock.putaway.rule'].create({
+ 'category_id': self.env.ref('product.product_category_all').id,
+ 'location_in_id': self.supplier_location.id,
+ 'location_out_id': shelf1_location.id,
+ })
+ putaway_category_office_furn = self.env['stock.putaway.rule'].create({
+ 'category_id': child_category.id,
+ 'location_in_id': self.supplier_location.id,
+ 'location_out_id': shelf2_location.id,
+ })
+ self.stock_location.write({
+ 'putaway_rule_ids': [(6, 0, [
+ putaway_category_all.id,
+ putaway_category_office_furn.id,
+ ])],
+ })
+ self.product.categ_id = child_category
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_6',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf2_location.id)
+
+ def test_putaway_7(self):
+ """ Checks parents locations are also browsed when looking for putaways.
+
+ WH/Stock > WH/Stock/Floor1> WH/Stock/Floor1/Rack1 > WH/Stock/Floor1/Rack1/Shelf2
+ The putaway is on Floor1 to send to Shelf2
+ A move from supplier to Rack1 should send to shelf2
+ """
+ floor1 = self.env['stock.location'].create({
+ 'name': 'floor1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ rack1 = self.env['stock.location'].create({
+ 'name': 'rack1',
+ 'usage': 'internal',
+ 'location_id': floor1.id,
+ })
+ shelf2 = self.env['stock.location'].create({
+ 'name': 'shelf2',
+ 'usage': 'internal',
+ 'location_id': rack1.id,
+ })
+
+ # putaway floor1 -> shelf2
+ putaway = self.env['stock.putaway.rule'].create({
+ 'product_id': self.product.id,
+ 'location_in_id': floor1.id,
+ 'location_out_id': shelf2.id,
+ })
+ floor1.write({
+ 'putaway_rule_ids': [(4, putaway.id, 0)],
+ })
+
+ # stock move supplier -> rack1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_6',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': rack1.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf2.id)
+
+ def test_availability_1(self):
+ """ Check that the `availability` field on a move is correctly computed when there is
+ more than enough products in stock.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 150.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.supplier_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 150.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+ self.assertEqual(move1.availability, 100.0)
+
+ def test_availability_2(self):
+ """ Check that the `availability` field on a move is correctly computed when there is
+ not enough products in stock.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 50.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.supplier_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 50.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+ self.assertEqual(move1.availability, 50.0)
+
+ def test_availability_3(self):
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, 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=lot2)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_availability_3',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(move1.reserved_availability, 1.0)
+
+ def test_availability_4(self):
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 30.0)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_availability_4',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_availability_4',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+
+ # set 15 as quantity done for the first and 30 as the second
+ move1.move_line_ids.qty_done = 15
+ move2.move_line_ids.qty_done = 30
+
+ # validate the second, the first should be unreserved
+ move2._action_done()
+
+ self.assertEqual(move1.state, 'confirmed')
+ self.assertEqual(move1.move_line_ids.qty_done, 15)
+ self.assertEqual(move2.state, 'done')
+
+ stock_quants = self.gather_relevant(self.product, self.stock_location)
+ self.assertEqual(len(stock_quants), 0)
+ customer_quants = self.gather_relevant(self.product, self.customer_location)
+ self.assertEqual(customer_quants.quantity, 30)
+ self.assertEqual(customer_quants.reserved_quantity, 0)
+
+ def test_availability_5(self):
+ """ Check that rerun action assign only create new stock move
+ lines instead of adding quantity in existing one.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 2.0)
+ # move from shelf1
+ move = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 4.0,
+ })
+ move._action_confirm()
+ move._action_assign()
+
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 4.0)
+ move._action_assign()
+
+ self.assertEqual(len(move.move_line_ids), 4.0)
+
+ def test_availability_6(self):
+ """ Check that, in the scenario where a move is in a bigger uom than the uom of the quants
+ and this uom only allows entire numbers, we don't make a partial reservation when the
+ quantity available is not enough to reserve the move. Check also that it is not possible
+ to set `quantity_done` with a value not honouring the UOM's rounding.
+ """
+ # on the dozen uom, set the rounding set 1.0
+ self.uom_dozen.rounding = 1
+
+ # 6 units are available in stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 6.0)
+
+ # the move should not be reserved
+ move = self.env['stock.move'].create({
+ 'name': 'test_availability_6',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1,
+ })
+ move._action_confirm()
+ move._action_assign()
+ self.assertEqual(move.state, 'confirmed')
+
+ # the quants should be left untouched
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 6.0)
+
+ # make 8 units available, the move should again not be reservabale
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0)
+ move._action_assign()
+ self.assertEqual(move.state, 'confirmed')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 8.0)
+
+ # make 12 units available, this time the move should be reservable
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 4.0)
+ move._action_assign()
+ self.assertEqual(move.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ # Check it isn't possible to set any value to quantity_done
+ with self.assertRaises(UserError):
+ move.quantity_done = 0.1
+ move._action_done()
+
+ with self.assertRaises(UserError):
+ move.quantity_done = 1.1
+ move._action_done()
+
+ with self.assertRaises(UserError):
+ move.quantity_done = 0.9
+ move._action_done()
+
+ move.quantity_done = 1
+ move._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 12.0)
+
+ def test_availability_7(self):
+ """ Check that, in the scenario where a move is in a bigger uom than the uom of the quants
+ and this uom only allows entire numbers, we only reserve quantity honouring the uom's
+ rounding even if the quantity is set across multiple quants.
+ """
+ # on the dozen uom, set the rounding set 1.0
+ self.uom_dozen.rounding = 1
+
+ # make 12 quants of 1
+ for i in range(1, 13):
+ lot_id = self.env['stock.production.lot'].create({
+ 'name': 'lot%s' % str(i),
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot_id)
+
+ # the move should be reserved
+ move = self.env['stock.move'].create({
+ 'name': 'test_availability_7',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1,
+ })
+ move._action_confirm()
+ move._action_assign()
+ self.assertEqual(move.state, 'assigned')
+ self.assertEqual(len(move.move_line_ids.mapped('product_uom_id')), 1)
+ self.assertEqual(move.move_line_ids.mapped('product_uom_id'), self.uom_unit)
+
+ for move_line in move.move_line_ids:
+ move_line.qty_done = 1
+ move._action_done()
+
+ self.assertEqual(move.product_uom_qty, 1)
+ self.assertEqual(move.product_uom.id, self.uom_dozen.id)
+ self.assertEqual(move.state, 'done')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.customer_location), 12.0)
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.customer_location)), 12)
+
+ def test_availability_8(self):
+ """ Test the assignment mechanism when the product quantity is decreased on a partially
+ reserved stock move.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 3.0)
+ self.assertAlmostEqual(self.product.qty_available, 3.0)
+
+ move_partial = self.env['stock.move'].create({
+ 'name': 'test_partial',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+
+ move_partial._action_confirm()
+ move_partial._action_assign()
+ self.assertAlmostEqual(self.product.virtual_available, -2.0)
+ self.assertEqual(move_partial.state, 'partially_available')
+ move_partial.product_uom_qty = 3.0
+ move_partial._action_assign()
+ self.assertEqual(move_partial.state, 'assigned')
+
+ def test_availability_9(self):
+ """ Test the assignment mechanism when the product quantity is increase
+ on a receipt move.
+ """
+ move_receipt = self.env['stock.move'].create({
+ 'name': 'test_receipt_edit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ })
+
+ move_receipt._action_confirm()
+ move_receipt._action_assign()
+ self.assertEqual(move_receipt.state, 'assigned')
+ move_receipt.product_uom_qty = 3.0
+ move_receipt._action_assign()
+ self.assertEqual(move_receipt.state, 'assigned')
+ self.assertEqual(move_receipt.move_line_ids.product_uom_qty, 3)
+
+ def test_unreserve_1(self):
+ """ Check that unreserving a stock move sets the products reserved as available and
+ set the state back to confirmed.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 150.0)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.supplier_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 150.0)
+ self.assertEqual(move1.availability, 100.0)
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 50.0)
+
+ # unreserve
+ move1._do_unreserve()
+ self.assertEqual(len(move1.move_line_ids), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 150.0)
+ self.assertEqual(move1.state, 'confirmed')
+
+ def test_unreserve_2(self):
+ """ Check that unreserving a stock move sets the products reserved as available and
+ set the state back to confirmed even if they are in a pack.
+ """
+ package1 = self.env['stock.quant.package'].create({'name': 'test_unreserve_2_pack'})
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 150.0, package_id=package1)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.supplier_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 150.0)
+ self.assertEqual(move1.availability, 100.0)
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 50.0)
+
+ # unreserve
+ move1._do_unreserve()
+ self.assertEqual(len(move1.move_line_ids), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 150.0)
+ self.assertEqual(move1.state, 'confirmed')
+
+ def test_unreserve_3(self):
+ """ Similar to `test_unreserve_1` but checking the quants more in details.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_out_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ quants = self.gather_relevant(self.product, self.stock_location)
+ self.assertEqual(len(quants), 1.0)
+ self.assertEqual(quants.quantity, 2.0)
+ self.assertEqual(quants.reserved_quantity, 2.0)
+
+ move1._do_unreserve()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(len(quants), 1.0)
+ self.assertEqual(quants.quantity, 2.0)
+ self.assertEqual(quants.reserved_quantity, 0.0)
+ self.assertEqual(len(move1.move_line_ids), 0.0)
+
+ def test_unreserve_4(self):
+ """ Check the unreservation of a partially available stock move.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_out_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'partially_available')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ quants = self.gather_relevant(self.product, self.stock_location)
+ self.assertEqual(len(quants), 1.0)
+ self.assertEqual(quants.quantity, 2.0)
+ self.assertEqual(quants.reserved_quantity, 2.0)
+
+ move1._do_unreserve()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(len(quants), 1.0)
+ self.assertEqual(quants.quantity, 2.0)
+ self.assertEqual(quants.reserved_quantity, 0.0)
+ self.assertEqual(len(move1.move_line_ids), 0.0)
+
+ def test_unreserve_5(self):
+ """ Check the unreservation of a stock move reserved on multiple quants.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 3)
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 2,
+ })
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 5)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_unreserve_5',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ move1._do_unreserve()
+
+ quants = self.gather_relevant(self.product, self.stock_location)
+ self.assertEqual(len(quants), 2.0)
+ for quant in quants:
+ self.assertEqual(quant.reserved_quantity, 0)
+
+ def test_unreserve_6(self):
+ """ In a situation with a negative and a positive quant, reserve and unreserve.
+ """
+ q1 = self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': -10,
+ 'reserved_quantity': 0,
+ })
+
+ q2 = self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 30.0,
+ 'reserved_quantity': 10.0,
+ })
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_unreserve_6',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(move1.move_line_ids.product_qty, 10)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(q2.reserved_quantity, 20)
+
+ move1._do_unreserve()
+ self.assertEqual(move1.state, 'confirmed')
+ self.assertEqual(len(move1.move_line_ids), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0)
+ self.assertEqual(q2.reserved_quantity, 10)
+
+ def test_unreserve_7(self):
+ """ Check the unreservation of a stock move delete only stock move lines
+ without quantity done.
+ """
+ product = self.env['product.product'].create({
+ 'name': 'product',
+ 'tracking': 'serial',
+ 'type': 'product',
+ })
+
+ serial_numbers = self.env['stock.production.lot'].create([{
+ 'name': str(x),
+ 'product_id': product.id,
+ 'company_id': self.env.company.id,
+ } for x in range(5)])
+
+ for serial in serial_numbers:
+ self.env['stock.quant'].create({
+ 'product_id': product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ 'lot_id': serial.id,
+ 'reserved_quantity': 0.0,
+ })
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_unreserve_7',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 5.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 5)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product, self.stock_location), 0.0)
+
+ # Check state is changed even with 0 move lines unlinked
+ move1.move_line_ids.write({'qty_done': 1})
+ move1._do_unreserve()
+ self.assertEqual(len(move1.move_line_ids), 5)
+ self.assertEqual(move1.state, 'confirmed')
+ move1._action_assign()
+ # set a quantity done on the two first move lines
+ move1.move_line_ids.write({'qty_done': 0})
+ move1.move_line_ids[0].qty_done = 1
+ move1.move_line_ids[1].qty_done = 1
+
+ move1._do_unreserve()
+ self.assertEqual(move1.state, 'confirmed')
+ self.assertEqual(len(move1.move_line_ids), 2)
+ self.assertEqual(move1.move_line_ids.mapped('qty_done'), [1, 1])
+ self.assertEqual(move1.move_line_ids.mapped('product_uom_qty'), [0, 0])
+
+ def test_link_assign_1(self):
+ """ Test the assignment mechanism when two chained stock moves try to move one unit of an
+ untracked product.
+ """
+ # make some stock
+ 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)), 1.0)
+
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+
+ (move_stock_pack + move_pack_cust)._action_confirm()
+ move_stock_pack._action_assign()
+ move_stock_pack.move_line_ids[0].qty_done = 1.0
+ move_stock_pack._action_done()
+ self.assertEqual(len(move_pack_cust.move_line_ids), 1)
+ move_line = move_pack_cust.move_line_ids[0]
+ self.assertEqual(move_line.location_id.id, self.pack_location.id)
+ self.assertEqual(move_line.location_dest_id.id, self.customer_location.id)
+ self.assertEqual(move_pack_cust.state, 'assigned')
+
+ def test_link_assign_2(self):
+ """ Test the assignment mechanism when two chained stock moves try to move one unit of a
+ tracked product.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot1)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location, lot1)), 1.0)
+
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_2_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_2_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+
+ (move_stock_pack + move_pack_cust)._action_confirm()
+ move_stock_pack._action_assign()
+
+ move_line_stock_pack = move_stock_pack.move_line_ids[0]
+ self.assertEqual(move_line_stock_pack.lot_id.id, lot1.id)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location, lot1)), 1.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.pack_location, lot1)), 0.0)
+
+ move_line_stock_pack.qty_done = 1.0
+ move_stock_pack._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location, lot1)), 0.0)
+
+ move_line_pack_cust = move_pack_cust.move_line_ids[0]
+ self.assertEqual(move_line_pack_cust.lot_id.id, lot1.id)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.pack_location, lot_id=lot1), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.pack_location, lot1)), 1.0)
+
+ def test_link_assign_3(self):
+ """ Test the assignment mechanism when three chained stock moves (2 sources, 1 dest) try to
+ move multiple units of an untracked product.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ move_stock_pack_1 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_stock_pack_2 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move_stock_pack_1.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_stock_pack_2.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack_1.id, 0), (4, move_stock_pack_2.id, 0)]})
+
+ (move_stock_pack_1 + move_stock_pack_2 + move_pack_cust)._action_confirm()
+
+ # assign and fulfill the first move
+ move_stock_pack_1._action_assign()
+ self.assertEqual(move_stock_pack_1.state, 'assigned')
+ self.assertEqual(len(move_stock_pack_1.move_line_ids), 1)
+ move_stock_pack_1.move_line_ids[0].qty_done = 1.0
+ move_stock_pack_1._action_done()
+ self.assertEqual(move_stock_pack_1.state, 'done')
+
+ # the destination move should be partially available and have one move line
+ self.assertEqual(move_pack_cust.state, 'partially_available')
+ self.assertEqual(len(move_pack_cust.move_line_ids), 1)
+ # Should have 1 quant in stock_location and another in pack_location
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.pack_location)), 1.0)
+
+ move_stock_pack_2._action_assign()
+ self.assertEqual(move_stock_pack_2.state, 'assigned')
+ self.assertEqual(len(move_stock_pack_2.move_line_ids), 1)
+ move_stock_pack_2.move_line_ids[0].qty_done = 1.0
+ move_stock_pack_2._action_done()
+ self.assertEqual(move_stock_pack_2.state, 'done')
+
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.pack_location)), 1.0)
+
+ self.assertEqual(move_pack_cust.state, 'assigned')
+ self.assertEqual(len(move_pack_cust.move_line_ids), 1)
+ move_line_1 = move_pack_cust.move_line_ids[0]
+ self.assertEqual(move_line_1.location_id.id, self.pack_location.id)
+ self.assertEqual(move_line_1.location_dest_id.id, self.customer_location.id)
+ self.assertEqual(move_line_1.product_qty, 2.0)
+ self.assertEqual(move_pack_cust.state, 'assigned')
+
+ def test_link_assign_4(self):
+ """ Test the assignment mechanism when three chained stock moves (2 sources, 1 dest) try to
+ move multiple units of a tracked by lot product.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0, lot_id=lot1)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location, lot1)), 1.0)
+
+ move_stock_pack_1 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_stock_pack_2 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move_stock_pack_1.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_stock_pack_2.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack_1.id, 0), (4, move_stock_pack_2.id, 0)]})
+
+ (move_stock_pack_1 + move_stock_pack_2 + move_pack_cust)._action_confirm()
+
+ # assign and fulfill the first move
+ move_stock_pack_1._action_assign()
+ self.assertEqual(len(move_stock_pack_1.move_line_ids), 1)
+ self.assertEqual(move_stock_pack_1.move_line_ids[0].lot_id.id, lot1.id)
+ move_stock_pack_1.move_line_ids[0].qty_done = 1.0
+ move_stock_pack_1._action_done()
+
+ # the destination move should be partially available and have one move line
+ self.assertEqual(len(move_pack_cust.move_line_ids), 1)
+
+ move_stock_pack_2._action_assign()
+ self.assertEqual(len(move_stock_pack_2.move_line_ids), 1)
+ self.assertEqual(move_stock_pack_2.move_line_ids[0].lot_id.id, lot1.id)
+ move_stock_pack_2.move_line_ids[0].qty_done = 1.0
+ move_stock_pack_2._action_done()
+
+ self.assertEqual(len(move_pack_cust.move_line_ids), 1)
+ move_line_1 = move_pack_cust.move_line_ids[0]
+ self.assertEqual(move_line_1.location_id.id, self.pack_location.id)
+ self.assertEqual(move_line_1.location_dest_id.id, self.customer_location.id)
+ self.assertEqual(move_line_1.product_qty, 2.0)
+ self.assertEqual(move_line_1.lot_id.id, lot1.id)
+ self.assertEqual(move_pack_cust.state, 'assigned')
+
+ def test_link_assign_5(self):
+ """ Test the assignment mechanism when three chained stock moves (1 sources, 2 dest) try to
+ move multiple units of an untracked product.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0)
+
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move_pack_cust_1 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_pack_cust_2 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust_1.id, 0), (4, move_pack_cust_2.id, 0)]})
+ move_pack_cust_1.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+ move_pack_cust_2.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+
+ (move_stock_pack + move_pack_cust_1 + move_pack_cust_2)._action_confirm()
+
+ # assign and fulfill the first move
+ move_stock_pack._action_assign()
+ self.assertEqual(len(move_stock_pack.move_line_ids), 1)
+ move_stock_pack.move_line_ids[0].qty_done = 2.0
+ move_stock_pack._action_done()
+
+ # the destination moves should be available and have one move line
+ self.assertEqual(len(move_pack_cust_1.move_line_ids), 1)
+ self.assertEqual(len(move_pack_cust_2.move_line_ids), 1)
+
+ move_pack_cust_1.move_line_ids[0].qty_done = 1.0
+ move_pack_cust_2.move_line_ids[0].qty_done = 1.0
+ (move_pack_cust_1 + move_pack_cust_2)._action_done()
+
+ def test_link_assign_6(self):
+ """ Test the assignment mechanism when four chained stock moves (2 sources, 2 dest) try to
+ move multiple units of an untracked by lot product. This particular test case simulates a two
+ step receipts with backorder.
+ """
+ move_supp_stock_1 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_6_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ })
+ move_supp_stock_2 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_6_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move_stock_stock_1 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_6_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ })
+ move_stock_stock_1.write({'move_orig_ids': [(4, move_supp_stock_1.id, 0), (4, move_supp_stock_2.id, 0)]})
+ move_stock_stock_2 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_6_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ })
+ move_stock_stock_2.write({'move_orig_ids': [(4, move_supp_stock_1.id, 0), (4, move_supp_stock_2.id, 0)]})
+
+ (move_supp_stock_1 + move_supp_stock_2 + move_stock_stock_1 + move_stock_stock_2)._action_confirm()
+ move_supp_stock_1._action_assign()
+ self.assertEqual(move_supp_stock_1.state, 'assigned')
+ self.assertEqual(move_supp_stock_2.state, 'assigned')
+ self.assertEqual(move_stock_stock_1.state, 'waiting')
+ self.assertEqual(move_stock_stock_2.state, 'waiting')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ # do the fist move, it'll bring 3 units in stock location so only `move_stock_stock_1`
+ # should be assigned
+ move_supp_stock_1.move_line_ids.qty_done = 3.0
+ move_supp_stock_1._action_done()
+ self.assertEqual(move_supp_stock_1.state, 'done')
+ self.assertEqual(move_supp_stock_2.state, 'assigned')
+ self.assertEqual(move_stock_stock_1.state, 'assigned')
+ self.assertEqual(move_stock_stock_2.state, 'waiting')
+
+ def test_link_assign_7(self):
+ # on the dozen uom, set the rounding set 1.0
+ self.uom_dozen.rounding = 1
+
+ # 6 units are available in stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 6.0)
+
+ # create pickings and moves for a pick -> pack mto scenario
+ picking_stock_pack = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_internal').id,
+ })
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_assign_7',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking_stock_pack.id,
+ })
+ picking_pack_cust = self.env['stock.picking'].create({
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_7',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking_pack_cust.id,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+ (move_stock_pack + move_pack_cust)._action_confirm()
+
+ # the pick should not be reservable because of the rounding of the dozen
+ move_stock_pack._action_assign()
+ self.assertEqual(move_stock_pack.state, 'confirmed')
+ move_pack_cust._action_assign()
+ self.assertEqual(move_pack_cust.state, 'waiting')
+
+ # move the 6 units by adding an unreserved move line
+ move_stock_pack.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 6,
+ 'product_uom_qty': 0,
+ 'lot_id': False,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': move_stock_pack.location_id.id,
+ 'location_dest_id': move_stock_pack.location_dest_id.id,
+ 'picking_id': picking_stock_pack.id,
+ })]})
+
+ # the quantity done on the move should not respect the rounding of the move line
+ self.assertEqual(move_stock_pack.quantity_done, 0.5)
+
+ # create the backorder in the uom of the quants
+ backorder_wizard_dict = picking_stock_pack.button_validate()
+ backorder_wizard = Form(self.env[backorder_wizard_dict['res_model']].with_context(backorder_wizard_dict['context'])).save()
+ backorder_wizard.process()
+ self.assertEqual(move_stock_pack.state, 'done')
+ self.assertEqual(move_stock_pack.quantity_done, 0.5)
+ self.assertEqual(move_stock_pack.product_uom_qty, 0.5)
+
+ # the second move should not be reservable because of the rounding on the dozen
+ move_pack_cust._action_assign()
+ self.assertEqual(move_pack_cust.state, 'partially_available')
+ move_line_pack_cust = move_pack_cust.move_line_ids
+ self.assertEqual(move_line_pack_cust.product_uom_qty, 6)
+ self.assertEqual(move_line_pack_cust.product_uom_id.id, self.uom_unit.id)
+
+ # move a dozen on the backorder to see how we handle the extra move
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_stock_pack.id)])
+ backorder.move_lines.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_dozen.id,
+ 'qty_done': 1,
+ 'product_uom_qty': 0,
+ 'lot_id': False,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': backorder.location_id.id,
+ 'location_dest_id': backorder.location_dest_id.id,
+ 'picking_id': backorder.id,
+ })]})
+ backorder.button_validate()
+ backorder_move = backorder.move_lines
+ self.assertEqual(backorder_move.state, 'done')
+ self.assertEqual(backorder_move.quantity_done, 12.0)
+ self.assertEqual(backorder_move.product_uom_qty, 12.0)
+ self.assertEqual(backorder_move.product_uom, self.uom_unit)
+
+ # the second move should now be reservable
+ move_pack_cust._action_assign()
+ self.assertEqual(move_pack_cust.state, 'assigned')
+ self.assertEqual(move_line_pack_cust.product_uom_qty, 12)
+ self.assertEqual(move_line_pack_cust.product_uom_id.id, self.uom_unit.id)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, move_stock_pack.location_dest_id), 6)
+
+ def test_link_assign_8(self):
+ """ Set the rounding of the dozen to 1.0, create a chain of two move for a dozen, the product
+ concerned is tracked by serial number. Check that the flow is ok.
+ """
+ # on the dozen uom, set the rounding set 1.0
+ self.uom_dozen.rounding = 1
+
+ # 6 units are available in stock
+ for i in range(1, 13):
+ lot_id = self.env['stock.production.lot'].create({
+ 'name': 'lot%s' % str(i),
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot_id)
+
+ # create pickings and moves for a pick -> pack mto scenario
+ picking_stock_pack = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_internal').id,
+ })
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_assign_7',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking_stock_pack.id,
+ })
+ picking_pack_cust = self.env['stock.picking'].create({
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_7',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking_pack_cust.id,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+ (move_stock_pack + move_pack_cust)._action_confirm()
+
+ move_stock_pack._action_assign()
+ self.assertEqual(move_stock_pack.state, 'assigned')
+ move_pack_cust._action_assign()
+ self.assertEqual(move_pack_cust.state, 'waiting')
+
+ for ml in move_stock_pack.move_line_ids:
+ ml.qty_done = 1
+ picking_stock_pack.button_validate()
+ self.assertEqual(move_pack_cust.state, 'assigned')
+ for ml in move_pack_cust.move_line_ids:
+ self.assertEqual(ml.product_uom_qty, 1)
+ self.assertEqual(ml.product_uom_id.id, self.uom_unit.id)
+ self.assertTrue(bool(ml.lot_id.id))
+
+ def test_link_assign_9(self):
+ """ Create an uom "3 units" which is 3 times the units but without rounding. Create 3
+ quants in stock and two chained moves. The first move will bring the 3 quants but the
+ second only validate 2 and create a backorder for the last one. Check that the reservation
+ is correctly cleared up for the last one.
+ """
+ uom_3units = self.env['uom.uom'].create({
+ 'name': '3 units',
+ 'category_id': self.uom_unit.category_id.id,
+ 'factor_inv': 3,
+ 'rounding': 1,
+ 'uom_type': 'bigger',
+ })
+ for i in range(1, 4):
+ lot_id = self.env['stock.production.lot'].create({
+ 'name': 'lot%s' % str(i),
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot_id)
+
+ picking_stock_pack = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_internal').id,
+ })
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_assign_9',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': uom_3units.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking_stock_pack.id,
+ })
+ picking_pack_cust = self.env['stock.picking'].create({
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_0',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': uom_3units.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking_pack_cust.id,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+ (move_stock_pack + move_pack_cust)._action_confirm()
+
+ picking_stock_pack.action_assign()
+ for ml in picking_stock_pack.move_lines.move_line_ids:
+ ml.qty_done = 1
+ picking_stock_pack.button_validate()
+ self.assertEqual(picking_pack_cust.state, 'assigned')
+ for ml in picking_pack_cust.move_lines.move_line_ids:
+ if ml.lot_id.name != 'lot3':
+ ml.qty_done = 1
+ res_dict_for_back_order = picking_pack_cust.button_validate()
+ backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
+ backorder_wizard.process()
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_pack_cust.id)])
+ backordered_move = backorder.move_lines
+
+ # due to the rounding, the backordered quantity is 0.999 ; we shoudln't be able to reserve
+ # 0.999 on a tracked by serial number quant
+ backordered_move._action_assign()
+ self.assertEqual(backordered_move.reserved_availability, 0)
+
+ # force the serial number and validate
+ lot3 = self.env['stock.production.lot'].search([('name', '=', "lot3")])
+ backorder.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product_serial.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 1,
+ 'product_uom_qty': 0,
+ 'lot_id': lot3.id,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': backordered_move.location_id.id,
+ 'location_dest_id': backordered_move.location_dest_id.id,
+ 'move_id': backordered_move.id,
+ })]})
+
+ backorder.button_validate()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.customer_location), 3)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.pack_location), 0)
+
+ def test_link_assign_10(self):
+ """ Test the assignment mechanism with partial availability.
+ """
+ # make some stock:
+ # stock location: 2.0
+ # pack location: -1.0
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ move_out = self.env['stock.move'].create({
+ 'name': 'test_link_assign_out',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_out._action_confirm()
+ move_out._action_assign()
+ move_out.quantity_done = 1.0
+ move_out._action_done()
+ self.assertEqual(len(self.gather_relevant(self.product, self.pack_location)), 1.0)
+
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+
+ (move_stock_pack + move_pack_cust)._action_confirm()
+ move_stock_pack._action_assign()
+ move_stock_pack.quantity_done = 2.0
+ move_stock_pack._action_done()
+ self.assertEqual(len(move_pack_cust.move_line_ids), 1)
+
+ self.assertAlmostEqual(move_pack_cust.reserved_availability, 1.0)
+ self.assertEqual(move_pack_cust.state, 'partially_available')
+
+ def test_use_reserved_move_line_1(self):
+ """ Test that _free_reservation work when quantity is only available on
+ reserved move lines.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 10.0)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move2._action_confirm()
+ move2._action_assign()
+ move3 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 0.0,
+ 'quantity_done': 1.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3._action_done()
+ self.assertEqual(move3.state, 'done')
+ quant = self.env['stock.quant']._gather(self.product, self.stock_location)
+ self.assertEqual(quant.quantity, 9.0)
+ self.assertEqual(quant.reserved_quantity, 9.0)
+
+ def test_use_reserved_move_line_2(self):
+ # make 12 units available in stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 12.0)
+
+ # reserve 12 units
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_use_reserved_move_line_2_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 12,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ quant = self.env['stock.quant']._gather(self.product, self.stock_location)
+ self.assertEqual(quant.quantity, 12)
+ self.assertEqual(quant.reserved_quantity, 12)
+
+ # force a move of 1 dozen
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_use_reserved_move_line_2_2',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ self.assertEqual(move2.state, 'confirmed')
+ move2._set_quantity_done(1)
+ move2._action_done()
+
+ # mov1 should be unreserved and the quant should be unlinked
+ self.assertEqual(move1.state, 'confirmed')
+ quant = self.env['stock.quant']._gather(self.product, self.stock_location)
+ self.assertEqual(quant.quantity, 0)
+ self.assertEqual(quant.reserved_quantity, 0)
+
+ def test_use_unreserved_move_line_1(self):
+ """ Test that validating a stock move linked to an untracked product reserved by another one
+ correctly unreserves the other one.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
+
+ # prepare the conflicting move
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+
+ # reserve those move
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ move2._action_confirm()
+ move2._action_assign()
+ self.assertEqual(move2.state, 'confirmed')
+
+ # use the product from the first one
+ move2.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 1,
+ 'product_uom_qty': 0,
+ 'lot_id': False,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': move2.location_id.id,
+ 'location_dest_id': move2.location_dest_id.id,
+ })]})
+ move2._action_done()
+
+ # the first move should go back to confirmed
+ self.assertEqual(move1.state, 'confirmed')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ def test_use_unreserved_move_line_2(self):
+ """ Test that validating a stock move linked to a tracked product reserved by another one
+ correctly unreserves the other one.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot1)
+
+ # prepare the conflicting move
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+
+ # reserve those move
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 1.0)
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ move2._action_confirm()
+ move2._action_assign()
+ self.assertEqual(move2.state, 'confirmed')
+
+ # use the product from the first one
+ move2.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 1,
+ 'product_uom_qty': 0,
+ 'lot_id': lot1.id,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': move2.location_id.id,
+ 'location_dest_id': move2.location_dest_id.id,
+ })]})
+ move2._action_done()
+
+ # the first move should go back to confirmed
+ self.assertEqual(move1.state, 'confirmed')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 0.0)
+
+ def test_use_unreserved_move_line_3(self):
+ """ Test the behavior of `_free_reservation` when ran on a recordset of move lines where
+ some are assigned and some are force assigned. `_free_reservation` should not use an
+ already processed move line when looking for a move line candidate to unreserve.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_3',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.quantity_done = 1
+
+ # add a forced move line in `move1`
+ move1.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 2,
+ 'product_uom_qty': 0,
+ 'lot_id': False,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ })]})
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 3.0)
+
+ def test_use_unreserved_move_line_4(self):
+ product_01 = self.env['product.product'].create({
+ 'name': 'Product 01',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ product_02 = self.env['product.product'].create({
+ 'name': 'Product 02',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ self.env['stock.quant']._update_available_quantity(product_01, self.stock_location, 1)
+ self.env['stock.quant']._update_available_quantity(product_02, self.stock_location, 1)
+
+ customer = self.env['res.partner'].create({'name': 'SuperPartner'})
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': customer.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+
+ p01_move = self.env['stock.move'].create({
+ 'name': 'SuperMove01',
+ 'location_id': picking.location_id.id,
+ 'location_dest_id': picking.location_dest_id.id,
+ 'picking_id': picking.id,
+ 'product_id': product_01.id,
+ 'product_uom_qty': 1,
+ 'product_uom': product_01.uom_id.id,
+ })
+ p02_move = self.env['stock.move'].create({
+ 'name': 'SuperMove02',
+ 'location_id': picking.location_id.id,
+ 'location_dest_id': picking.location_dest_id.id,
+ 'picking_id': picking.id,
+ 'product_id': product_02.id,
+ 'product_uom_qty': 1,
+ 'product_uom': product_02.uom_id.id,
+ })
+
+ picking.action_confirm()
+ picking.action_assign()
+ p01_move.product_uom_qty = 0
+ picking.do_unreserve()
+ picking.action_assign()
+ p01_move.product_uom_qty = 1
+ self.assertEqual(p01_move.state, 'confirmed')
+
+ def test_edit_reserved_move_line_1(self):
+ """ Test that editing a stock move line linked to an untracked product correctly and
+ directly adapts the reservation. In this case, we edit the sublocation where we take the
+ product to another sublocation where a product is available.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.env['stock.quant']._update_available_quantity(self.product, shelf2_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ move1.move_line_ids.location_id = shelf2_location.id
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ def test_edit_reserved_move_line_2(self):
+ """ Test that editing a stock move line linked to a tracked product correctly and directly
+ adapts the reservation. In this case, we edit the lot to another available one.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot2)
+ 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, self.stock_location, lot_id=lot1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+
+ move1.move_line_ids.lot_id = lot2.id
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 0.0)
+
+ def test_edit_reserved_move_line_3(self):
+ """ Test that editing a stock move line linked to a packed product correctly and directly
+ adapts the reservation. In this case, we edit the package to another available one.
+ """
+ package1 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_3'})
+ package2 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_3'})
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, package_id=package1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, package_id=package2)
+ 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, self.stock_location, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package2), 1.0)
+
+ move1.move_line_ids.package_id = package2.id
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package2), 0.0)
+
+ def test_edit_reserved_move_line_4(self):
+ """ Test that editing a stock move line linked to an owned product correctly and directly
+ adapts the reservation. In this case, we edit the owner to another available one.
+ """
+ owner1 = self.env['res.partner'].create({'name': 'test_edit_reserved_move_line_4_1'})
+ owner2 = self.env['res.partner'].create({'name': 'test_edit_reserved_move_line_4_2'})
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, owner_id=owner1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, owner_id=owner2)
+ 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, self.stock_location, owner_id=owner1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner2), 1.0)
+
+ move1.move_line_ids.owner_id = owner2.id
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner2), 0.0)
+
+ def test_edit_reserved_move_line_5(self):
+ """ Test that editing a stock move line linked to a packed and tracked product correctly
+ and directly adapts the reservation. In this case, we edit the lot to another available one
+ that is not in a pack.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ package1 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_5'})
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot1, package_id=package1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot2)
+ 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, self.stock_location, lot_id=lot1, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1, package_id=package1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+ move_line = move1.move_line_ids[0]
+ move_line.write({'package_id': False, 'lot_id': lot2.id})
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 0.0)
+
+ def test_edit_reserved_move_line_6(self):
+ """ Test that editing a stock move line linked to an untracked product correctly and
+ directly adapts the reservation. In this case, we edit the sublocation where we take the
+ product to another sublocation where a product is NOT available.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(move1.move_line_ids.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ move1.move_line_ids.location_id = shelf2_location.id
+
+ self.assertEqual(move1.move_line_ids.state, 'confirmed')
+ self.assertEqual(move1.reserved_availability, 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+
+ def test_edit_reserved_move_line_7(self):
+ """ Send 5 tracked products to a client, but these products do not have any lot set in our
+ inventory yet: we only set them at delivery time. The created move line should have 5 items
+ without any lot set, if we edit to set them to lot1, the reservation should not change.
+ Validating the stock move should should not create a negative quant for this lot in stock
+ location.
+ # """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ # make some stock without assigning a lot id
+ self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 5)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ move_line = move1.move_line_ids[0]
+ self.assertEqual(move_line.product_qty, 5)
+ move_line.qty_done = 5.0
+ self.assertEqual(move_line.product_qty, 5) # don't change reservation
+ move_line.lot_id = lot1
+ self.assertEqual(move_line.product_qty, 5) # don't change reservation when assgning a lot now
+
+ move1._action_done()
+ self.assertEqual(move_line.product_qty, 0) # change reservation to 0 for done move
+ self.assertEqual(move1.state, 'done')
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_lot, self.stock_location)), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_lot, self.stock_location, lot_id=lot1, strict=True)), 0.0)
+
+ def test_edit_reserved_move_line_8(self):
+ """ Send 5 tracked products to a client, but some of these products do not have any lot set
+ in our inventory yet: we only set them at delivery time. Adding a lot_id on the move line
+ that does not have any should not change its reservation, and validating should not create
+ a negative quant for this lot in stock.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ # make some stock without assigning a lot id
+ self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 3)
+ self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 2, lot_id=lot1)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 2)
+
+ tracked_move_line = None
+ untracked_move_line = None
+ for move_line in move1.move_line_ids:
+ if move_line.lot_id:
+ tracked_move_line = move_line
+ else:
+ untracked_move_line = move_line
+
+ self.assertEqual(tracked_move_line.product_qty, 2)
+ tracked_move_line.qty_done = 2
+
+ self.assertEqual(untracked_move_line.product_qty, 3)
+ untracked_move_line.lot_id = lot2
+ self.assertEqual(untracked_move_line.product_qty, 3) # don't change reservation
+ untracked_move_line.qty_done = 3
+ self.assertEqual(untracked_move_line.product_qty, 3) # don't change reservation
+
+ move1._action_done()
+ self.assertEqual(untracked_move_line.product_qty, 0) # change reservation to 0 for done move
+ self.assertEqual(tracked_move_line.product_qty, 0) # change reservation to 0 for done move
+ self.assertEqual(move1.state, 'done')
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot2, strict=True), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_lot, self.stock_location)), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_lot, self.stock_location, lot_id=lot1, strict=True)), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_lot, self.stock_location, lot_id=lot2, strict=True)), 0.0)
+
+ def test_edit_done_move_line_1(self):
+ """ Test that editing a done stock move line linked to an untracked product correctly and
+ directly adapts the transfer. In this case, we edit the sublocation where we take the
+ product to another sublocation where a product is available.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.env['stock.quant']._update_available_quantity(self.product, shelf2_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+
+ # move from shelf1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ # edit once done, we actually moved from shelf2
+ move1.move_line_ids.location_id = shelf2_location.id
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ def test_edit_done_move_line_2(self):
+ """ Test that editing a done stock move line linked to a tracked product correctly and directly
+ adapts the transfer. In this case, we edit the lot to another available one.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot2)
+ 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, self.stock_location, lot_id=lot1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+
+ move1.move_line_ids.lot_id = lot2.id
+
+ # reserved_availability should always been 0 for done move.
+ self.assertEqual(move1.reserved_availability, 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 0.0)
+
+ def test_edit_done_move_line_3(self):
+ """ Test that editing a done stock move line linked to a packed product correctly and directly
+ adapts the transfer. In this case, we edit the package to another available one.
+ """
+ package1 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_3'})
+ package2 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_3'})
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, package_id=package1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, package_id=package2)
+ 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, self.stock_location, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package2), 1.0)
+
+ move1.move_line_ids.package_id = package2.id
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package2), 0.0)
+
+ def test_edit_done_move_line_4(self):
+ """ Test that editing a done stock move line linked to an owned product correctly and directly
+ adapts the transfer. In this case, we edit the owner to another available one.
+ """
+ owner1 = self.env['res.partner'].create({'name': 'test_edit_reserved_move_line_4_1'})
+ owner2 = self.env['res.partner'].create({'name': 'test_edit_reserved_move_line_4_2'})
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, owner_id=owner1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, owner_id=owner2)
+ 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, self.stock_location, owner_id=owner1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner2), 1.0)
+
+ move1.move_line_ids.owner_id = owner2.id
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner2), 0.0)
+
+ def test_edit_done_move_line_5(self):
+ """ Test that editing a done stock move line linked to a packed and tracked product correctly
+ and directly adapts the transfer. In this case, we edit the lot to another available one
+ that is not in a pack.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ package1 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_5'})
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot1, package_id=package1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot2)
+ 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, self.stock_location, lot_id=lot1, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1, package_id=package1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+ move_line = move1.move_line_ids[0]
+ move_line.write({'package_id': False, 'lot_id': lot2.id})
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 0.0)
+
+ def test_edit_done_move_line_6(self):
+ """ Test that editing a done stock move line linked to an untracked product correctly and
+ directly adapts the transfer. In this case, we edit the sublocation where we take the
+ product to another sublocation where a product is NOT available.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ move1.move_line_ids.location_id = shelf2_location.id
+
+ 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, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location, allow_negative=True), -1.0)
+
+ def test_edit_done_move_line_7(self):
+ """ Test that editing a done stock move line linked to an untracked product correctly and
+ directly adapts the transfer. In this case, we edit the sublocation where we take the
+ product to another sublocation where a product is NOT available because it has been reserved
+ by another move.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.env['stock.quant']._update_available_quantity(self.product, shelf2_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, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+
+ self.assertEqual(move2.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ move1.move_line_ids.location_id = shelf2_location.id
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(move2.state, 'confirmed')
+
+ def test_edit_done_move_line_8(self):
+ """ Test that editing a done stock move line linked to an untracked product correctly and
+ directly adapts the transfer. In this case, we increment the quantity done (and we do not
+ have more in stock.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ # move from shelf1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(move1.product_uom_qty, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ # edit once done, we actually moved 2 products
+ move1.move_line_ids.qty_done = 2
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location, allow_negative=True), -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)
+ self.assertEqual(move1.product_uom_qty, 2.0)
+
+ def test_edit_done_move_line_9(self):
+ """ Test that editing a done stock move line linked to an untracked product correctly and
+ directly adapts the transfer. In this case, we "cancel" the move by zeroing the qty done.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ # move from shelf1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(move1.product_uom_qty, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ # edit once done, we actually moved 2 products
+ move1.move_line_ids.qty_done = 0
+
+ self.assertEqual(move1.product_uom_qty, 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ def test_edit_done_move_line_10(self):
+ """ Edit the quantity done for an incoming move shoudld also remove the quant if there
+ are no product in stock.
+ """
+ # move from shelf1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10
+ move1._action_done()
+
+ quant = self.gather_relevant(self.product, self.stock_location)
+ self.assertEqual(len(quant), 1.0)
+
+ # edit once done, we actually moved 2 products
+ move1.move_line_ids.qty_done = 0
+
+ quant = self.gather_relevant(self.product, self.stock_location)
+ self.assertEqual(len(quant), 0.0)
+ self.assertEqual(move1.product_uom_qty, 0.0)
+
+ def test_edit_done_move_line_11(self):
+ """ Add a move line and check if the quant is updated
+ """
+ owner = self.env['res.partner'].create({'name': 'Jean'})
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'partner_id': owner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ # move from shelf1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ move1.move_line_ids.qty_done = 10
+ picking._action_done()
+ self.assertEqual(move1.product_uom_qty, 10.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0)
+ self.env['stock.move.line'].create({
+ 'picking_id': move1.move_line_ids.picking_id.id,
+ 'move_id': move1.move_line_ids.move_id.id,
+ 'product_id': move1.move_line_ids.product_id.id,
+ 'qty_done': move1.move_line_ids.qty_done,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.move_line_ids.location_id.id,
+ 'location_dest_id': move1.move_line_ids.location_dest_id.id,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 20.0)
+ move1.move_line_ids[1].qty_done = 5
+ self.assertEqual(move1.product_uom_qty, 15.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 15.0)
+
+ def test_edit_done_move_line_12(self):
+ """ Test that editing a done stock move line linked a tracked product correctly and directly
+ adapts the transfer. In this case, we edit the lot to another one, but the original move line
+ is not in the default product's UOM.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ package1 = self.env['stock.quant.package'].create({'name': 'test_edit_done_move_line_12'})
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1.move_line_ids.lot_id = lot1.id
+ move1._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1), 12.0)
+
+ # Change the done quantity from 1 dozen to two dozen
+ move1.move_line_ids.qty_done = 2
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1), 24.0)
+
+ def test_edit_done_move_line_13(self):
+ """ Test that editing a done stock move line linked to a packed and tracked product correctly
+ and directly adapts the transfer. In this case, we edit the lot to another available one
+ that we put in the same pack.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ package1 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_5'})
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1.move_line_ids.lot_id = lot1.id
+ move1.move_line_ids.result_package_id = package1.id
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1, package_id=package1), 1.0)
+
+ move1.move_line_ids.write({'lot_id': lot2.id})
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location), 1.0)
+ 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, package_id=package1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot2, package_id=package1), 1.0)
+
+ def test_immediate_validate_1(self):
+ """ In a picking with a single available move, clicking on validate without filling any
+ quantities should open a wizard asking to process all the reservation (so, the whole move).
+ """
+ partner = self.env['res.partner'].create({'name': 'Jean'})
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ res_dict = picking.button_validate()
+ self.assertEqual(res_dict.get('res_model'), 'stock.immediate.transfer')
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ wizard.process()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0)
+
+ def test_immediate_validate_2(self):
+ """ In a picking with a single partially available move, clicking on validate without
+ filling any quantities should open a wizard asking to process all the reservation (so, only
+ a part of the initial demand). Validating this wizard should open another one asking for
+ the creation of a backorder. If the backorder is created, it should contain the quantities
+ not processed.
+ """
+ partner = self.env['res.partner'].create({'name': 'Jean'})
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 5.0)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_2',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ # Only 5 products are reserved on the move of 10, click on `button_validate`.
+ res_dict = picking.button_validate()
+ self.assertEqual(res_dict.get('res_model'), 'stock.immediate.transfer')
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ res_dict_for_back_order = wizard.process()
+ self.assertEqual(res_dict_for_back_order.get('res_model'), 'stock.backorder.confirmation')
+ backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
+ # Chose to create a backorder.
+ backorder_wizard.process()
+
+ # Only 5 products should be processed on the initial move.
+ self.assertEqual(picking.move_lines.state, 'done')
+ self.assertEqual(picking.move_lines.quantity_done, 5.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)), 0.0)
+
+ # The backoder should contain a move for the other 5 produts.
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
+ self.assertEqual(len(backorder), 1.0)
+ self.assertEqual(backorder.move_lines.product_uom_qty, 5.0)
+
+ def test_immediate_validate_3(self):
+ """ In a picking with two moves, one partially available and one unavailable, clicking
+ on validate without filling any quantities should open a wizard asking to process all the
+ reservation (so, only a part of one of the moves). Validating this wizard should open
+ another one asking for the creation of a backorder. If the backorder is created, it should
+ contain the quantities not processed.
+ """
+ product5 = self.env['product.product'].create({
+ 'name': 'Product 5',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1)
+
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_internal').id,
+ })
+ product1_move = self.env['stock.move'].create({
+ 'name': 'product1_move',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100,
+ })
+ product5_move = self.env['stock.move'].create({
+ 'name': 'product3_move',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'picking_id': picking.id,
+ 'product_id': product5.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+
+ # product1_move should be partially available (1/100), product5_move should be totally
+ # unavailable (0/100)
+ self.assertEqual(product1_move.state, 'partially_available')
+ self.assertEqual(product5_move.state, 'confirmed')
+
+ action = picking.button_validate()
+ self.assertEqual(action.get('res_model'), 'stock.immediate.transfer')
+ wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
+ action = wizard.process()
+ self.assertTrue(isinstance(action, dict), 'Should open backorder wizard')
+ self.assertEqual(action.get('res_model'), 'stock.backorder.confirmation')
+ wizard = self.env[(action.get('res_model'))].browse(action.get('res_id')).with_context(action.get('context'))
+ wizard.process()
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
+ self.assertEqual(len(backorder), 1.0)
+
+ # The backorder should contain 99 product1 and 100 product5.
+ for backorder_move in backorder.move_lines:
+ if backorder_move.product_id.id == self.product.id:
+ self.assertEqual(backorder_move.product_qty, 99)
+ elif backorder_move.product_id.id == product5.id:
+ self.assertEqual(backorder_move.product_qty, 100)
+
+ def test_immediate_validate_4(self):
+ """ In a picking with a single available tracked by lot move, clicking on validate without
+ filling any quantities should pop up the immediate transfer wizard.
+ """
+ partner = self.env['res.partner'].create({'name': 'Jean'})
+ lot1 = self.env['stock.production.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_lot, self.stock_location, 5.0, lot_id=lot1)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ # move from shelf1
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_4',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ # No quantities filled, immediate transfer wizard should pop up.
+ immediate_trans_wiz_dict = picking.button_validate()
+ self.assertEqual(immediate_trans_wiz_dict.get('res_model'), 'stock.immediate.transfer')
+ immediate_trans_wiz = Form(self.env[immediate_trans_wiz_dict['res_model']].with_context(immediate_trans_wiz_dict['context'])).save()
+ immediate_trans_wiz.process()
+
+ self.assertEqual(picking.move_lines.quantity_done, 5.0)
+ # Check move_lines data
+ self.assertEqual(len(picking.move_lines.move_line_ids), 1)
+ self.assertEqual(picking.move_lines.move_line_ids.lot_id, lot1)
+ self.assertEqual(picking.move_lines.move_line_ids.qty_done, 5.0)
+ # Check quants data
+ 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)), 0.0)
+
+ def _create_picking_test_immediate_validate_5(self, picking_type_id, product_id):
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_type_id': picking_type_id.id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'move1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_id': picking.id,
+ 'picking_type_id': picking_type_id.id,
+ 'product_id': product_id.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+
+ picking.action_confirm()
+
+ for line in picking.move_line_ids:
+ line.qty_done = line.product_uom_qty
+
+ return picking
+
+ def test_immediate_validate_5(self):
+ """ In a receipt with a single tracked by serial numbers move, clicking on validate without
+ filling any quantities nor lot should open an UserError except if the picking type is
+ configured to allow otherwise.
+ """
+ picking_type_id = self.env.ref('stock.picking_type_in')
+ product_id = self.product_serial
+ self.assertTrue(picking_type_id.use_create_lots or picking_type_id.use_existing_lots)
+ self.assertEqual(product_id.tracking, 'serial')
+
+ picking = self._create_picking_test_immediate_validate_5(picking_type_id, product_id)
+ # should raise because no serial numbers were specified
+ self.assertRaises(UserError, picking.button_validate)
+
+ picking_type_id.use_create_lots = False
+ picking_type_id.use_existing_lots = False
+ picking = self._create_picking_test_immediate_validate_5(picking_type_id, product_id)
+ picking.button_validate()
+ self.assertEqual(picking.state, 'done')
+
+ def test_immediate_validate_6(self):
+ """ In a receipt picking with two moves, one tracked and one untracked, clicking on
+ validate without filling any quantities should displays an UserError as long as no quantity
+ done and lot_name is set on the tracked move. Now if the user validates the picking, the
+ wizard telling the user all reserved quantities will be processed will NOT be opened. This
+ wizard is only opene if no quantities were filled. So validating the picking at this state
+ will open another wizard asking for the creation of a backorder. Now, if the user processed
+ on the second move more than the reservation, a wizard will ask him to confirm.
+ """
+ picking_type = self.env.ref('stock.picking_type_in')
+ picking_type.use_create_lots = True
+ picking_type.use_existing_lots = False
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_type_id': picking_type.id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'product1_move',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1,
+ })
+ product3_move = self.env['stock.move'].create({
+ 'name': 'product3_move',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+
+ with self.assertRaises(UserError):
+ picking.button_validate()
+ product3_move.move_line_ids[0].qty_done = 1
+ with self.assertRaises(UserError):
+ picking.button_validate()
+ product3_move.move_line_ids[0].lot_name = '271828'
+ action = picking.button_validate() # should open backorder wizard
+
+ self.assertTrue(isinstance(action, dict), 'Should open backorder wizard')
+ self.assertEqual(action.get('res_model'), 'stock.backorder.confirmation')
+
+ def test_immediate_validate_7(self):
+ """ In a picking with a single unavailable move, clicking on validate without filling any
+ quantities should display an UserError telling the user he cannot process a picking without
+ any processed quantity.
+ """
+ partner = self.env['res.partner'].create({'name': 'Jean'})
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_2',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+
+ scrap = self.env['stock.scrap'].create({
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'scrap_qty': 5.0,
+ })
+ scrap.do_scrap()
+
+ # No products are reserved on the move of 10, click on `button_validate`.
+ with self.assertRaises(UserError):
+ picking.button_validate()
+
+ def test_immediate_validate_8(self):
+ """Validate three receipts at once."""
+ partner = self.env['res.partner'].create({'name': 'Pierre'})
+ receipt1 = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_8_1',
+ 'location_id': receipt1.location_id.id,
+ 'location_dest_id': receipt1.location_dest_id.id,
+ 'picking_id': receipt1.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ receipt1.action_confirm()
+ receipt2 = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_8_2',
+ 'location_id': receipt2.location_id.id,
+ 'location_dest_id': receipt2.location_dest_id.id,
+ 'picking_id': receipt2.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ receipt2.action_confirm()
+ receipt3 = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_8_3',
+ 'location_id': receipt3.location_id.id,
+ 'location_dest_id': receipt3.location_dest_id.id,
+ 'picking_id': receipt3.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ receipt3.action_confirm()
+
+ immediate_trans_wiz_dict = (receipt1 + receipt2).button_validate()
+ immediate_trans_wiz = Form(self.env[immediate_trans_wiz_dict['res_model']].with_context(immediate_trans_wiz_dict['context'])).save()
+ # The different transfers are displayed to the users.
+ self.assertTrue(immediate_trans_wiz.show_transfers)
+ # All transfers are processed by default
+ self.assertEqual(immediate_trans_wiz.immediate_transfer_line_ids.mapped('to_immediate'), [True, True])
+ # Only transfer receipt1
+ immediate_trans_wiz.immediate_transfer_line_ids.filtered(lambda line: line.picking_id == receipt2).to_immediate = False
+ immediate_trans_wiz.process()
+ self.assertEqual(receipt1.state, 'done')
+ self.assertEqual(receipt2.state, 'assigned')
+ # Transfer receipt2 and receipt3.
+ immediate_trans_wiz_dict = (receipt3 + receipt2).button_validate()
+ immediate_trans_wiz = Form(self.env[immediate_trans_wiz_dict['res_model']].with_context(immediate_trans_wiz_dict['context'])).save()
+ immediate_trans_wiz.process()
+ self.assertEqual(receipt2.state, 'done')
+ self.assertEqual(receipt3.state, 'done')
+
+ def test_set_quantity_done_1(self):
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_set_quantity_done_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_set_quantity_done_2',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ (move1 + move2)._action_confirm()
+ (move1 + move2).write({'quantity_done': 1})
+ self.assertEqual(move1.quantity_done, 1)
+ self.assertEqual(move2.quantity_done, 1)
+
+ def test_initial_demand_1(self):
+ """ Check that the initial demand is set to 0 when creating a move by hand, and
+ that changing the product on the move do not reset the initial demand.
+ """
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ })
+ self.assertEqual(move1.state, 'draft')
+ self.assertEqual(move1.product_uom_qty, 0)
+ move1.product_uom_qty = 100
+ move1.product_id = self.product_serial
+ move1.onchange_product_id()
+ self.assertEqual(move1.product_uom_qty, 100)
+
+ def test_scrap_1(self):
+ """ Check the created stock move and the impact on quants when we scrap a
+ storable product.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1)
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product.id,
+ 'product_uom_id':self.product.uom_id.id,
+ 'scrap_qty': 1,
+ })
+ scrap.do_scrap()
+ self.assertEqual(scrap.state, 'done')
+ move = scrap.move_id
+ self.assertEqual(move.state, 'done')
+ self.assertEqual(move.quantity_done, 1)
+ self.assertEqual(move.scrapped, True)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+
+ def test_scrap_2(self):
+ """ Check the created stock move and the impact on quants when we scrap a
+ consumable product.
+ """
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product_consu.id,
+ 'product_uom_id':self.product_consu.uom_id.id,
+ 'scrap_qty': 1,
+ })
+ self.assertEqual(scrap.name, 'New', 'Name should be New in draft state')
+ scrap.do_scrap()
+ self.assertTrue(scrap.name.startswith('SP/'), 'Sequence should be Changed after do_scrap')
+ self.assertEqual(scrap.state, 'done')
+ move = scrap.move_id
+ self.assertEqual(move.state, 'done')
+ self.assertEqual(move.quantity_done, 1)
+ self.assertEqual(move.scrapped, True)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_consu, self.stock_location), 0)
+
+ def test_scrap_3(self):
+ """ Scrap the product of a reserved move line. Check that the move line is
+ correctly deleted and that the associated stock move is not assigned anymore.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_scrap_3',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product.id,
+ 'product_uom_id':self.product.uom_id.id,
+ 'scrap_qty': 1,
+ })
+ scrap.do_scrap()
+ self.assertEqual(move1.state, 'confirmed')
+ self.assertEqual(len(move1.move_line_ids), 0)
+
+ def test_scrap_4(self):
+ """ Scrap the product of a picking. Then modify the
+ done linked stock move and ensure the scrap quantity is also
+ updated.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 10)
+ partner = self.env['res.partner'].create({'name': 'Kimberley'})
+ picking = self.env['stock.picking'].create({
+ 'name': 'A single picking with one move to scrap',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'A move to confirm and scrap its product',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking.id,
+ })
+ move1._action_confirm()
+
+ self.assertEqual(move1.state, 'confirmed')
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product.id,
+ 'product_uom_id': self.product.uom_id.id,
+ 'scrap_qty': 5,
+ 'picking_id': picking.id,
+ })
+
+ scrap.action_validate()
+ self.assertEqual(len(picking.move_lines), 2)
+ scrapped_move = picking.move_lines.filtered(lambda m: m.state == 'done')
+ self.assertTrue(scrapped_move, 'No scrapped move created.')
+ self.assertEqual(scrapped_move.scrap_ids.ids, [scrap.id], 'Wrong scrap linked to the move.')
+ self.assertEqual(scrap.scrap_qty, 5, 'Scrap quantity has been modified and is not correct anymore.')
+
+ scrapped_move.quantity_done = 8
+ self.assertEqual(scrap.scrap_qty, 8, 'Scrap quantity is not updated.')
+
+ def test_scrap_5(self):
+ """ Scrap the product of a reserved move line where the product is reserved in another
+ unit of measure. Check that the move line is correctly updated after the scrap.
+ """
+ # 4 units are available in stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 4)
+
+ # try to reserve a dozen
+ partner = self.env['res.partner'].create({'name': 'Kimberley'})
+ picking = self.env['stock.picking'].create({
+ 'name': 'A single picking with one move to scrap',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'A move to confirm and scrap its product',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking.id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.reserved_availability, 0.33)
+
+ # scrap a unit
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product.id,
+ 'product_uom_id': self.product.uom_id.id,
+ 'scrap_qty': 1,
+ 'picking_id': picking.id,
+ })
+ scrap.action_validate()
+
+ self.assertEqual(scrap.state, 'done')
+ self.assertEqual(move1.reserved_availability, 0.25)
+
+ def test_scrap_6(self):
+ """ Check that scrap correctly handle UoM. """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1)
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_dozen.id,
+ 'scrap_qty': 1,
+ })
+ warning_message = scrap.action_validate()
+ self.assertEqual(warning_message.get('res_model', 'Wrong Model'), 'stock.warn.insufficient.qty.scrap')
+ insufficient_qty_wizard = self.env['stock.warn.insufficient.qty.scrap'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'scrap_id': scrap.id,
+ 'quantity': 1,
+ 'product_uom_name': self.product.uom_id.name
+ })
+ insufficient_qty_wizard.action_done()
+ self.assertEqual(self.env['stock.quant']._gather(self.product, self.stock_location).quantity, -11)
+
+ def test_in_date_1(self):
+ """ Check that moving a tracked quant keeps the incoming date.
+ """
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.lot_name = 'lot1'
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ quant = self.gather_relevant(self.product_lot, self.stock_location)
+ self.assertEqual(len(quant), 1.0)
+ self.assertNotEqual(quant.in_date, False)
+
+ # Keep a reference to the initial incoming date in order to compare it later.
+ initial_incoming_date = quant.in_date
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 1
+ move2._action_done()
+
+ quant = self.gather_relevant(self.product_lot, self.pack_location)
+ self.assertEqual(len(quant), 1.0)
+ self.assertEqual(quant.in_date, initial_incoming_date)
+
+ def test_in_date_2(self):
+ """ Check that editing a done move line for a tracked product and changing its lot
+ correctly restores the original lot with its incoming date and remove the new lot
+ with its incoming date.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ # receive lot1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.lot_id = lot1
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ # receive lot2
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.lot_id = lot2
+ move2.move_line_ids.qty_done = 1
+ move2._action_done()
+
+ initial_in_date_lot2 = self.env['stock.quant'].search([
+ ('location_id', '=', self.stock_location.id),
+ ('product_id', '=', self.product_lot.id),
+ ('lot_id', '=', lot2.id),
+ ]).in_date
+
+ # Edit lot1's incoming date.
+ quant_lot1 = self.env['stock.quant'].search([
+ ('location_id', '=', self.stock_location.id),
+ ('product_id', '=', self.product_lot.id),
+ ('lot_id', '=', lot1.id),
+ ])
+ from odoo.fields import Datetime
+ from datetime import timedelta
+ initial_in_date_lot1 = Datetime.now() - timedelta(days=5)
+ quant_lot1.in_date = initial_in_date_lot1
+
+ # Move one quant to pack location
+ move3 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 1
+ move3._action_done()
+ quant_in_pack = self.env['stock.quant'].search([
+ ('product_id', '=', self.product_lot.id),
+ ('location_id', '=', self.pack_location.id),
+ ])
+ # As lot1 has an older date and FIFO is set by default, it's the one that should be
+ # in pack.
+ self.assertEqual(len(quant_in_pack), 1)
+ self.assertAlmostEqual(quant_in_pack.in_date, initial_in_date_lot1, delta=timedelta(seconds=1))
+ self.assertEqual(quant_in_pack.lot_id, lot1)
+
+ # Now, edit the move line and actually move the other lot
+ move3.move_line_ids.lot_id = lot2
+
+ # Check that lot1 correctly is back to stock with its right in_date
+ quant_lot1 = self.env['stock.quant'].search([
+ ('location_id.usage', '=', 'internal'),
+ ('product_id', '=', self.product_lot.id),
+ ('lot_id', '=', lot1.id),
+ ('quantity', '!=', 0),
+ ])
+ self.assertEqual(quant_lot1.location_id, self.stock_location)
+ self.assertAlmostEqual(quant_lot1.in_date, initial_in_date_lot1, delta=timedelta(seconds=1))
+
+ # Check that lo2 is in pack with is right in_date
+ quant_lot2 = self.env['stock.quant'].search([
+ ('location_id.usage', '=', 'internal'),
+ ('product_id', '=', self.product_lot.id),
+ ('lot_id', '=', lot2.id),
+ ('quantity', '!=', 0),
+ ])
+ self.assertEqual(quant_lot2.location_id, self.pack_location)
+ self.assertAlmostEqual(quant_lot2.in_date, initial_in_date_lot2, delta=timedelta(seconds=1))
+
+ def test_in_date_3(self):
+ """ Check that, when creating a move line on a done stock move, the lot and its incoming
+ date are correctly moved to the destination location.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ # receive lot1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.lot_id = lot1
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ # receive lot2
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.lot_id = lot2
+ move2.move_line_ids.qty_done = 1
+ move2._action_done()
+
+ initial_in_date_lot2 = self.env['stock.quant'].search([
+ ('location_id', '=', self.stock_location.id),
+ ('product_id', '=', self.product_lot.id),
+ ('lot_id', '=', lot2.id),
+ ('quantity', '!=', 0),
+ ]).in_date
+
+ # Edit lot1's incoming date.
+ quant_lot1 = self.env['stock.quant'].search([
+ ('location_id.usage', '=', 'internal'),
+ ('product_id', '=', self.product_lot.id),
+ ('lot_id', '=', lot1.id),
+ ('quantity', '!=', 0),
+ ])
+ from odoo.fields import Datetime
+ from datetime import timedelta
+ initial_in_date_lot1 = Datetime.now() - timedelta(days=5)
+ quant_lot1.in_date = initial_in_date_lot1
+
+ # Move one quant to pack location
+ move3 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 1
+ move3._action_done()
+
+ # Now, also move lot2
+ self.env['stock.move.line'].create({
+ 'move_id': move3.id,
+ 'product_id': move3.product_id.id,
+ 'qty_done': 1,
+ 'product_uom_id': move3.product_uom.id,
+ 'location_id': move3.location_id.id,
+ 'location_dest_id': move3.location_dest_id.id,
+ 'lot_id': lot2.id,
+ })
+
+ quants = self.env['stock.quant'].search([
+ ('location_id.usage', '=', 'internal'),
+ ('product_id', '=', self.product_lot.id),
+ ('quantity', '!=', 0),
+ ])
+ self.assertEqual(len(quants), 2)
+ for quant in quants:
+ if quant.lot_id == lot1:
+ self.assertAlmostEqual(quant.in_date, initial_in_date_lot1, delta=timedelta(seconds=1))
+ elif quant.lot_id == lot2:
+ self.assertAlmostEqual(quant.in_date, initial_in_date_lot2, delta=timedelta(seconds=1))
+
+ def test_edit_initial_demand_1(self):
+ """ Increase initial demand once everything is reserved and check if
+ the existing move_line is updated.
+ """
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.product_uom_qty = 15
+ # _action_assign is automatically called
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(move1.product_uom_qty, 15)
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ def test_edit_initial_demand_2(self):
+ """ Decrease initial demand once everything is reserved and check if
+ the existing move_line has been dropped after the updated and another
+ is created once the move is reserved.
+ """
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ move1.product_uom_qty = 5
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(move1.product_uom_qty, 5)
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ def test_initial_demand_3(self):
+ """ Increase the initial demand on a receipt picking, the system should automatically
+ reserve the new quantity.
+ """
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ 'immediate_transfer': True,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'quantity_done': 10.0,
+ 'picking_id': picking.id,
+ })
+ picking._autoconfirm_picking()
+ self.assertEqual(picking.state, 'assigned')
+ move1.quantity_done = 12
+ self.assertEqual(picking.state, 'assigned')
+
+ def test_initial_demand_4(self):
+ """ Increase the initial demand on a delivery picking, the system should not automatically
+ reserve the new quantity.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 12)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'picking_id': picking.id,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ self.assertEqual(picking.state, 'assigned')
+ move1.product_uom_qty = 12
+ self.assertEqual(picking.state, 'assigned') # actually, partially available
+ self.assertEqual(move1.state, 'partially_available')
+ picking.action_assign()
+ self.assertEqual(move1.state, 'assigned')
+
+ def test_change_product_type(self):
+ """ Changing type of an existing product will raise a user error if
+ - some move are reserved
+ - switching from a stockable product when qty_available is not zero
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 10)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_customer',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ with self.assertRaises(UserError):
+ self.product.type = 'consu'
+ move1._action_cancel()
+
+ with self.assertRaises(UserError):
+ self.product.type = 'consu'
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, -self.product.qty_available)
+ self.product.type = 'consu'
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_customer',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+
+ move2._action_confirm()
+ move2._action_assign()
+
+ with self.assertRaises(UserError):
+ self.product.type = 'product'
+ move2._action_cancel()
+ self.product.type = 'product'
+
+ def test_edit_done_picking_1(self):
+ """ Add a new move line in a done picking should generate an
+ associated move.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 12)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'picking_id': picking.id,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ move1.quantity_done = 10
+ picking._action_done()
+
+ self.assertEqual(len(picking.move_lines), 1, 'One move should exist for the picking.')
+ self.assertEqual(len(picking.move_line_ids), 1, 'One move line should exist for the picking.')
+
+ ml = self.env['stock.move.line'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 2.0,
+ 'picking_id': picking.id,
+ })
+
+ self.assertEqual(len(picking.move_lines), 2, 'The new move associated to the move line does not exist.')
+ self.assertEqual(len(picking.move_line_ids), 2, 'It should be 2 move lines for the picking.')
+ self.assertTrue(ml.move_id in picking.move_lines, 'Links are not correct between picking, moves and move lines.')
+ self.assertEqual(picking.state, 'done', 'Picking should still done after adding a new move line.')
+ self.assertTrue(all(move.state == 'done' for move in picking.move_lines), 'Wrong state for move.')
+
+ def test_put_in_pack_1(self):
+ """ Check that reserving a move and adding its move lines to
+ different packages work as expected.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ 'picking_id': picking.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+ move1.quantity_done = 1
+ picking.action_put_in_pack()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+ self.assertEqual(len(picking.move_line_ids), 2)
+ unpacked_ml = picking.move_line_ids.filtered(lambda ml: not ml.result_package_id)
+ self.assertEqual(unpacked_ml.product_qty, 1)
+ unpacked_ml.qty_done = 1
+ picking.action_put_in_pack()
+ self.assertEqual(len(picking.move_line_ids), 2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+ picking.button_validate()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 2)
+
+ def test_put_in_pack_2(self):
+ """Check that reserving moves without done quantity
+ adding in same package.
+ """
+ product1 = self.env['product.product'].create({
+ 'name': 'Product B',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1)
+ self.env['stock.quant']._update_available_quantity(product1, self.stock_location, 2)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking.id,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_transit_2',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ 'picking_id': picking.id,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, self.stock_location), 0)
+ picking.action_put_in_pack()
+ self.assertEqual(len(picking.move_line_ids), 2)
+ self.assertEqual(picking.move_line_ids[0].qty_done, 1, "Stock move line should have 1 quantity as a done quantity.")
+ self.assertEqual(picking.move_line_ids[1].qty_done, 2, "Stock move line should have 2 quantity as a done quantity.")
+ line1_result_package = picking.move_line_ids[0].result_package_id
+ line2_result_package = picking.move_line_ids[1].result_package_id
+ self.assertEqual(line1_result_package, line2_result_package, "Product and Product1 should be in a same package.")
+
+ def test_put_in_pack_3(self):
+ """Check that one reserving move without done quantity and
+ another reserving move with done quantity adding in different
+ package.
+ """
+ product1 = self.env['product.product'].create({
+ 'name': 'Product B',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1)
+ self.env['stock.quant']._update_available_quantity(product1, self.stock_location, 2)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_transit_2',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ 'picking_id': picking.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, self.stock_location), 0)
+ move1.quantity_done = 1
+ picking.action_put_in_pack()
+ move2.quantity_done = 2
+ picking.action_put_in_pack()
+ self.assertEqual(len(picking.move_line_ids), 2)
+ line1_result_package = picking.move_line_ids[0].result_package_id
+ line2_result_package = picking.move_line_ids[1].result_package_id
+ self.assertNotEqual(line1_result_package, line2_result_package, "Product and Product1 should be in a different package.")
+
+ def test_forecast_availability(self):
+ """ Make an outgoing picking in dozens for a product stored in units.
+ Check that reserved_availabity is expressed in move uom and forecast_availability is in product base uom
+ """
+ # create product
+ product = self.env['product.product'].create({
+ 'name': 'Product In Units',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(product, self.stock_location, 36.0)
+ # create picking
+ picking_out = self.env['stock.picking'].create({
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id})
+ move = self.env['stock.move'].create({
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 2.0,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id})
+ # confirm
+ picking_out.action_confirm()
+ # check availability
+ picking_out.action_assign()
+ # check reserved_availabity expressed in move uom
+ self.assertEqual(move.reserved_availability, 2)
+ # check forecast_availability expressed in product base uom
+ self.assertEqual(move.forecast_availability, 24)