In the web client, in a real use case, it's not possible to write on fields which are invisible, as it's not possible to write on fields which are readonly. This is a first step in the goal to change the behavior of the `groups=` attribute in the back-end views, to remove them for the view instead of making them invisible. This is mainly to reduce the diff of the revision that will introduce the mentioned above behavior change. As nodes with `groups=` will be removed from the view when the user doesn't have the group, it's no longer possible to set a value on a field having a `groups=` the user doesn't have in the `Form` test class, as the field will no longer be at all in the view. However, these unit tests shouldn't have been able to set values on invisible fields in the first place. This revision therefore aims to correct the unit tests setting value on fields which were invisible because the user executing the test was not part of the required group(s) for these fields to be visible in the view. closes odoo/odoo#94337 Related: odoo/enterprise#28936 Signed-off-by: Denis Ledoux (dle) <dle@odoo.com>
3538 lines
162 KiB
Python
3538 lines
162 KiB
Python
# -*- coding: utf-8 -*-
|
||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
||
from datetime import timedelta
|
||
|
||
from odoo.addons.stock.tests.common import TestStockCommon
|
||
from odoo.exceptions import UserError
|
||
|
||
from odoo.tests import Form
|
||
from odoo.tools import float_is_zero, float_compare
|
||
|
||
from datetime import datetime
|
||
from dateutil.relativedelta import relativedelta
|
||
|
||
class TestPickShip(TestStockCommon):
|
||
def create_pick_ship(self):
|
||
picking_client = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
|
||
dest = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 10,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': picking_client.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'state': 'waiting',
|
||
'procure_method': 'make_to_order',
|
||
})
|
||
|
||
picking_pick = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.pack_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 10,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': picking_pick.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.pack_location,
|
||
'move_dest_ids': [(4, dest.id)],
|
||
'state': 'confirmed',
|
||
})
|
||
return picking_pick, picking_client
|
||
|
||
def create_pick_pack_ship(self):
|
||
picking_ship = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
|
||
ship = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 1,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': picking_ship.id,
|
||
'location_id': self.output_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
|
||
picking_pack = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.pack_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
|
||
pack = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 1,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': picking_pack.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.output_location,
|
||
'move_dest_ids': [(4, ship.id)],
|
||
})
|
||
|
||
picking_pick = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.pack_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 1,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': picking_pick.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.pack_location,
|
||
'move_dest_ids': [(4, pack.id)],
|
||
'state': 'confirmed',
|
||
})
|
||
return picking_pick, picking_pack, picking_ship
|
||
|
||
def test_mto_moves(self):
|
||
"""
|
||
10 in stock, do pick->ship and check ship is assigned when pick is done, then backorder of ship
|
||
"""
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
location = self.env['stock.location'].browse(self.stock_location)
|
||
|
||
# make some stock
|
||
self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
|
||
picking_pick.action_assign()
|
||
picking_pick.move_ids[0].move_line_ids[0].qty_done = 10.0
|
||
picking_pick._action_done()
|
||
|
||
self.assertEqual(picking_client.state, 'assigned', 'The state of the client should be assigned')
|
||
|
||
# Now partially transfer the ship
|
||
picking_client.move_ids[0].move_line_ids[0].qty_done = 5
|
||
picking_client._action_done() # no new in order to create backorder
|
||
|
||
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_client.id)])
|
||
self.assertEqual(backorder.state, 'waiting', 'Backorder should be waiting for reservation')
|
||
|
||
def test_mto_moves_transfer(self):
|
||
"""
|
||
10 in stock, 5 in pack. Make sure it does not assign the 5 pieces in pack
|
||
"""
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
|
||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 5.0)
|
||
|
||
self.assertEqual(len(self.env['stock.quant']._gather(self.productA, stock_location)), 1.0)
|
||
self.assertEqual(len(self.env['stock.quant']._gather(self.productA, pack_location)), 1.0)
|
||
|
||
(picking_pick + picking_client).action_assign()
|
||
|
||
move_pick = picking_pick.move_ids
|
||
move_cust = picking_client.move_ids
|
||
self.assertEqual(move_pick.state, 'assigned')
|
||
self.assertEqual(picking_pick.state, 'assigned')
|
||
self.assertEqual(move_cust.state, 'waiting')
|
||
self.assertEqual(picking_client.state, 'waiting', 'The picking should not assign what it does not have')
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 0.0)
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 5.0)
|
||
|
||
move_pick.move_line_ids[0].qty_done = 10.0
|
||
picking_pick._action_done()
|
||
|
||
self.assertEqual(move_pick.state, 'done')
|
||
self.assertEqual(picking_pick.state, 'done')
|
||
self.assertEqual(move_cust.state, 'assigned')
|
||
self.assertEqual(picking_client.state, 'assigned', 'The picking should not assign what it does not have')
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 0.0)
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 5.0)
|
||
self.assertEqual(sum(self.env['stock.quant']._gather(self.productA, stock_location).mapped('quantity')), 0.0)
|
||
self.assertEqual(len(self.env['stock.quant']._gather(self.productA, pack_location)), 1.0)
|
||
|
||
def test_mto_moves_return(self):
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
|
||
|
||
picking_pick.action_assign()
|
||
picking_pick.move_ids[0].move_line_ids[0].qty_done = 10.0
|
||
picking_pick._action_done()
|
||
self.assertEqual(picking_pick.state, 'done')
|
||
self.assertEqual(picking_client.state, 'assigned')
|
||
|
||
# return a part of what we've done
|
||
stock_return_picking_form = Form(self.env['stock.return.picking']
|
||
.with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0],
|
||
active_model='stock.picking'))
|
||
stock_return_picking = stock_return_picking_form.save()
|
||
stock_return_picking = stock_return_picking_form.save()
|
||
stock_return_picking.product_return_moves.quantity = 2.0 # Return only 2
|
||
stock_return_picking_action = stock_return_picking.create_returns()
|
||
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||
return_pick.move_ids[0].move_line_ids[0].qty_done = 2.0
|
||
return_pick._action_done()
|
||
# the client picking should not be assigned anymore, as we returned partially what we took
|
||
self.assertEqual(picking_client.state, 'confirmed')
|
||
|
||
def test_mto_moves_extra_qty(self):
|
||
""" Ensure that a move in MTO will support an extra quantity. The extra
|
||
move should be created in MTS and should not be merged in the initial
|
||
move if it's in MTO. It should also avoid to trigger the rules.
|
||
"""
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.productA.write({'route_ids': [(4, self.env.ref('stock.route_warehouse0_mto').id)]})
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
|
||
picking_pick.action_assign()
|
||
picking_pick.move_ids[0].move_line_ids[0].qty_done = 15.0
|
||
picking_pick._action_done()
|
||
self.assertEqual(picking_pick.state, 'done')
|
||
self.assertEqual(picking_client.state, 'assigned')
|
||
|
||
picking_client.move_ids[0].move_line_ids[0].qty_done = 15.0
|
||
picking_client.move_ids._action_done()
|
||
self.assertEqual(len(picking_client.move_ids), 2)
|
||
move_ids = picking_client.move_ids.sorted()
|
||
self.assertEqual(move_ids.mapped('procure_method'), ['make_to_order', 'make_to_stock'])
|
||
self.assertEqual(move_ids.mapped('product_uom_qty'), [10.0, 5.0])
|
||
|
||
def test_mto_moves_return_extra(self):
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
|
||
|
||
picking_pick.action_assign()
|
||
picking_pick.move_ids[0].move_line_ids[0].qty_done = 10.0
|
||
picking_pick._action_done()
|
||
self.assertEqual(picking_pick.state, 'done')
|
||
self.assertEqual(picking_client.state, 'assigned')
|
||
|
||
# return more than we've done
|
||
stock_return_picking_form = Form(self.env['stock.return.picking']
|
||
.with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0],
|
||
active_model='stock.picking'))
|
||
stock_return_picking = stock_return_picking_form.save()
|
||
stock_return_picking.product_return_moves.quantity = 12.0 # Return 2 extra
|
||
stock_return_picking_action = stock_return_picking.create_returns()
|
||
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||
|
||
# Verify the extra move has been merged with the original move
|
||
self.assertAlmostEqual(return_pick.move_ids.product_uom_qty, 12.0)
|
||
self.assertAlmostEqual(return_pick.move_ids.quantity_done, 0.0)
|
||
self.assertAlmostEqual(return_pick.move_ids.reserved_availability, 10.0)
|
||
|
||
def test_mto_resupply_cancel_ship(self):
|
||
""" This test simulates a pick pack ship with a resupply route
|
||
set. Pick and pack are validated, ship is cancelled. This test
|
||
ensure that new picking are not created from the cancelled
|
||
ship after the scheduler task. The supply route is only set in
|
||
order to make the scheduler run without mistakes (no next
|
||
activity).
|
||
"""
|
||
picking_pick, picking_pack, picking_ship = self.create_pick_pack_ship()
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
warehouse_1.write({'delivery_steps': 'pick_pack_ship'})
|
||
warehouse_2 = self.env['stock.warehouse'].create({
|
||
'name': 'Small Warehouse',
|
||
'code': 'SWH'
|
||
})
|
||
warehouse_1.write({
|
||
'resupply_wh_ids': [(6, 0, [warehouse_2.id])]
|
||
})
|
||
resupply_route = self.env['stock.route'].search([('supplier_wh_id', '=', warehouse_2.id), ('supplied_wh_id', '=', warehouse_1.id)])
|
||
self.assertTrue(resupply_route)
|
||
self.productA.write({'route_ids': [(4, resupply_route.id), (4, self.env.ref('stock.route_warehouse0_mto').id)]})
|
||
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
|
||
|
||
picking_pick.action_assign()
|
||
picking_pick.move_ids[0].move_line_ids[0].qty_done = 10.0
|
||
picking_pick._action_done()
|
||
|
||
picking_pack.action_assign()
|
||
picking_pack.move_ids[0].move_line_ids[0].qty_done = 10.0
|
||
picking_pack._action_done()
|
||
|
||
picking_ship.action_cancel()
|
||
picking_ship.move_ids.write({'procure_method': 'make_to_order'})
|
||
|
||
self.env['procurement.group'].run_scheduler()
|
||
next_activity = self.env['mail.activity'].search([('res_model', '=', 'product.template'), ('res_id', '=', self.productA.product_tmpl_id.id)])
|
||
self.assertEqual(picking_ship.state, 'cancel')
|
||
self.assertFalse(next_activity, 'If a next activity has been created if means that scheduler failed\
|
||
and the end of this test do not have sense.')
|
||
self.assertEqual(len(picking_ship.move_ids.mapped('move_orig_ids')), 0,
|
||
'Scheduler should not create picking pack and pick since ship has been manually cancelled.')
|
||
|
||
def test_no_backorder_1(self):
|
||
""" Check the behavior of doing less than asked in the picking pick and chosing not to
|
||
create a backorder. In this behavior, the second picking should obviously only be able to
|
||
reserve what was brought, but its initial demand should stay the same and the system will
|
||
ask the user will have to consider again if he wants to create a backorder or not.
|
||
"""
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
location = self.env['stock.location'].browse(self.stock_location)
|
||
|
||
# make some stock
|
||
self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
|
||
picking_pick.action_assign()
|
||
picking_pick.move_ids[0].move_line_ids[0].qty_done = 5.0
|
||
|
||
# create a backorder
|
||
picking_pick._action_done()
|
||
picking_pick_backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_pick.id)])
|
||
self.assertEqual(picking_pick_backorder.state, 'confirmed')
|
||
self.assertEqual(picking_pick_backorder.move_ids.product_qty, 5.0)
|
||
|
||
self.assertEqual(picking_client.state, 'assigned')
|
||
|
||
# cancel the backorder
|
||
picking_pick_backorder.action_cancel()
|
||
self.assertEqual(picking_client.state, 'assigned')
|
||
|
||
def test_edit_done_chained_move(self):
|
||
""" Let’s say two moves are chained: the first is done and the second is assigned.
|
||
Editing the move line of the first move should impact the reservation of the second one.
|
||
"""
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
location = self.env['stock.location'].browse(self.stock_location)
|
||
|
||
# make some stock
|
||
self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
|
||
picking_pick.action_assign()
|
||
picking_pick.move_ids[0].move_line_ids[0].qty_done = 10.0
|
||
picking_pick._action_done()
|
||
|
||
self.assertEqual(picking_pick.state, 'done', 'The state of the pick should be done')
|
||
self.assertEqual(picking_client.state, 'assigned', 'The state of the client should be assigned')
|
||
self.assertEqual(picking_pick.move_ids.quantity_done, 10.0, 'Wrong quantity_done for pick move')
|
||
self.assertEqual(picking_client.move_ids.product_qty, 10.0, 'Wrong initial demand for client move')
|
||
self.assertEqual(picking_client.move_ids.reserved_availability, 10.0, 'Wrong quantity already reserved for client move')
|
||
|
||
picking_pick.move_ids[0].move_line_ids[0].qty_done = 5.0
|
||
self.assertEqual(picking_pick.state, 'done', 'The state of the pick should be done')
|
||
self.assertEqual(picking_client.state, 'assigned', 'The state of the client should be partially available')
|
||
self.assertEqual(picking_pick.move_ids.quantity_done, 5.0, 'Wrong quantity_done for pick move')
|
||
self.assertEqual(picking_client.move_ids.product_qty, 10.0, 'Wrong initial demand for client move')
|
||
self.assertEqual(picking_client.move_ids.reserved_availability, 5.0, 'Wrong quantity already reserved for client move')
|
||
|
||
# Check if run action_assign does not crash
|
||
picking_client.action_assign()
|
||
|
||
def test_edit_done_chained_move_with_lot(self):
|
||
""" Let’s say two moves are chained: the first is done and the second is assigned.
|
||
Editing the lot on the move line of the first move should impact the reservation of the second one.
|
||
"""
|
||
self.productA.tracking = 'lot'
|
||
lot1 = self.env['stock.lot'].create({
|
||
'name': 'lot1',
|
||
'product_id': self.productA.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
lot2 = self.env['stock.lot'].create({
|
||
'name': 'lot2',
|
||
'product_id': self.productA.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
location = self.env['stock.location'].browse(self.stock_location)
|
||
|
||
# make some stock
|
||
self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
|
||
picking_pick.action_assign()
|
||
picking_pick.move_ids[0].move_line_ids[0].write({
|
||
'qty_done': 10.0,
|
||
'lot_id': lot1.id,
|
||
})
|
||
picking_pick._action_done()
|
||
|
||
self.assertEqual(picking_pick.state, 'done', 'The state of the pick should be done')
|
||
self.assertEqual(picking_client.state, 'assigned', 'The state of the client should be assigned')
|
||
self.assertEqual(picking_pick.move_ids.quantity_done, 10.0, 'Wrong quantity_done for pick move')
|
||
self.assertEqual(picking_client.move_ids.product_qty, 10.0, 'Wrong initial demand for client move')
|
||
self.assertEqual(picking_client.move_ids.move_line_ids.lot_id, lot1, 'Wrong lot for client move line')
|
||
self.assertEqual(picking_client.move_ids.reserved_availability, 10.0, 'Wrong quantity already reserved for client move')
|
||
|
||
picking_pick.move_ids[0].move_line_ids[0].lot_id = lot2.id
|
||
self.assertEqual(picking_pick.state, 'done', 'The state of the pick should be done')
|
||
self.assertEqual(picking_client.state, 'assigned', 'The state of the client should be partially available')
|
||
self.assertEqual(picking_pick.move_ids.quantity_done, 10.0, 'Wrong quantity_done for pick move')
|
||
self.assertEqual(picking_client.move_ids.product_qty, 10.0, 'Wrong initial demand for client move')
|
||
self.assertEqual(picking_client.move_ids.move_line_ids.lot_id, lot2, 'Wrong lot for client move line')
|
||
self.assertEqual(picking_client.move_ids.reserved_availability, 10.0, 'Wrong quantity already reserved for client move')
|
||
|
||
# Check if run action_assign does not crash
|
||
picking_client.action_assign()
|
||
|
||
def test_chained_move_with_uom(self):
|
||
""" Create pick ship with a different uom than the once used for quant.
|
||
Check that reserved quantity and flow work correctly.
|
||
"""
|
||
picking_client = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
dest = self.MoveObj.create({
|
||
'name': self.gB.name,
|
||
'product_id': self.gB.id,
|
||
'product_uom_qty': 5,
|
||
'product_uom': self.uom_kg.id,
|
||
'picking_id': picking_client.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'state': 'waiting',
|
||
})
|
||
|
||
picking_pick = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.pack_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
|
||
self.MoveObj.create({
|
||
'name': self.gB.name,
|
||
'product_id': self.gB.id,
|
||
'product_uom_qty': 5,
|
||
'product_uom': self.uom_kg.id,
|
||
'picking_id': picking_pick.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.pack_location,
|
||
'move_dest_ids': [(4, dest.id)],
|
||
'state': 'confirmed',
|
||
})
|
||
location = self.env['stock.location'].browse(self.stock_location)
|
||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||
|
||
# make some stock
|
||
self.env['stock.quant']._update_available_quantity(self.gB, location, 10000.0)
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.gB, pack_location), 0.0)
|
||
picking_pick.action_assign()
|
||
picking_pick.move_ids[0].move_line_ids[0].qty_done = 5.0
|
||
picking_pick._action_done()
|
||
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.gB, location), 5000.0)
|
||
self.assertEqual(self.env['stock.quant']._gather(self.gB, pack_location).quantity, 5000.0)
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.gB, pack_location), 0.0)
|
||
self.assertEqual(picking_client.state, 'assigned')
|
||
self.assertEqual(picking_client.move_ids.reserved_availability, 5.0)
|
||
|
||
def test_pick_ship_return(self):
|
||
""" Create pick and ship. Bring it ot the customer and then return
|
||
it to stock. This test check the state and the quantity after each move in
|
||
order to ensure that it is correct.
|
||
"""
|
||
picking_pick, picking_ship = self.create_pick_ship()
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||
customer_location = self.env['stock.location'].browse(self.customer_location)
|
||
self.productA.tracking = 'lot'
|
||
lot = self.env['stock.lot'].create({
|
||
'product_id': self.productA.id,
|
||
'name': '123456789',
|
||
'company_id': self.env.company.id,
|
||
})
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0, lot_id=lot)
|
||
|
||
picking_pick.action_assign()
|
||
picking_pick.move_ids[0].move_line_ids[0].qty_done = 10.0
|
||
picking_pick._action_done()
|
||
self.assertEqual(picking_pick.state, 'done')
|
||
self.assertEqual(picking_ship.state, 'assigned')
|
||
|
||
picking_ship.action_assign()
|
||
picking_ship.move_ids[0].move_line_ids[0].qty_done = 10.0
|
||
picking_ship._action_done()
|
||
|
||
customer_quantity = self.env['stock.quant']._get_available_quantity(self.productA, customer_location, lot_id=lot)
|
||
self.assertEqual(customer_quantity, 10, 'It should be one product in customer')
|
||
|
||
""" First we create the return picking for pick pinking.
|
||
Since we do not have created the return between customer and
|
||
output. This return should not be available and should only have
|
||
picking pick as origin move.
|
||
"""
|
||
stock_return_picking_form = Form(self.env['stock.return.picking']
|
||
.with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0],
|
||
active_model='stock.picking'))
|
||
stock_return_picking = stock_return_picking_form.save()
|
||
stock_return_picking.product_return_moves.quantity = 10.0
|
||
stock_return_picking_action = stock_return_picking.create_returns()
|
||
return_pick_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||
|
||
self.assertEqual(return_pick_picking.state, 'waiting')
|
||
|
||
stock_return_picking_form = Form(self.env['stock.return.picking']
|
||
.with_context(active_ids=picking_ship.ids, active_id=picking_ship.ids[0],
|
||
active_model='stock.picking'))
|
||
stock_return_picking = stock_return_picking_form.save()
|
||
stock_return_picking.product_return_moves.quantity = 10.0
|
||
stock_return_picking_action = stock_return_picking.create_returns()
|
||
return_ship_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||
|
||
self.assertEqual(return_ship_picking.state, 'assigned', 'Return ship picking should automatically be assigned')
|
||
""" We created the return for ship picking. The origin/destination
|
||
link between return moves should have been created during return creation.
|
||
"""
|
||
self.assertTrue(return_ship_picking.move_ids in return_pick_picking.move_ids.mapped('move_orig_ids'),
|
||
'The pick return picking\'s moves should have the ship return picking\'s moves as origin')
|
||
|
||
self.assertTrue(return_pick_picking.move_ids in return_ship_picking.move_ids.mapped('move_dest_ids'),
|
||
'The ship return picking\'s moves should have the pick return picking\'s moves as destination')
|
||
|
||
return_ship_picking.move_ids[0].move_line_ids[0].write({
|
||
'qty_done': 10.0,
|
||
'lot_id': lot.id,
|
||
})
|
||
return_ship_picking._action_done()
|
||
self.assertEqual(return_ship_picking.state, 'done')
|
||
self.assertEqual(return_pick_picking.state, 'assigned')
|
||
|
||
customer_quantity = self.env['stock.quant']._get_available_quantity(self.productA, customer_location, lot_id=lot)
|
||
self.assertEqual(customer_quantity, 0, 'It should be one product in customer')
|
||
|
||
pack_quantity = self.env['stock.quant']._get_available_quantity(self.productA, pack_location, lot_id=lot)
|
||
self.assertEqual(pack_quantity, 0, 'It should be one product in pack location but is reserved')
|
||
|
||
# Should use previous move lot.
|
||
return_pick_picking.move_ids[0].move_line_ids[0].qty_done = 10.0
|
||
return_pick_picking._action_done()
|
||
self.assertEqual(return_pick_picking.state, 'done')
|
||
|
||
stock_quantity = self.env['stock.quant']._get_available_quantity(self.productA, stock_location, lot_id=lot)
|
||
self.assertEqual(stock_quantity, 10, 'The product is not back in stock')
|
||
|
||
def test_pick_pack_ship_return(self):
|
||
""" This test do a pick pack ship delivery to customer and then
|
||
return it to stock. Once everything is done, this test will check
|
||
if all the link orgini/destination between moves are correct.
|
||
"""
|
||
picking_pick, picking_pack, picking_ship = self.create_pick_pack_ship()
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.productA.tracking = 'serial'
|
||
lot = self.env['stock.lot'].create({
|
||
'product_id': self.productA.id,
|
||
'name': '123456789',
|
||
'company_id': self.env.company.id,
|
||
})
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 1.0, lot_id=lot)
|
||
|
||
picking_pick.action_assign()
|
||
picking_pick.move_ids[0].move_line_ids[0].qty_done = 1.0
|
||
picking_pick._action_done()
|
||
|
||
picking_pack.action_assign()
|
||
picking_pack.move_ids[0].move_line_ids[0].qty_done = 1.0
|
||
picking_pack._action_done()
|
||
|
||
picking_ship.action_assign()
|
||
picking_ship.move_ids[0].move_line_ids[0].qty_done = 1.0
|
||
picking_ship._action_done()
|
||
|
||
stock_return_picking_form = Form(self.env['stock.return.picking']
|
||
.with_context(active_ids=picking_ship.ids, active_id=picking_ship.ids[0],
|
||
active_model='stock.picking'))
|
||
stock_return_picking = stock_return_picking_form.save()
|
||
stock_return_picking.product_return_moves.quantity = 1.0
|
||
stock_return_picking_action = stock_return_picking.create_returns()
|
||
return_ship_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||
|
||
return_ship_picking.move_ids[0].move_line_ids[0].write({
|
||
'qty_done': 1.0,
|
||
'lot_id': lot.id,
|
||
})
|
||
return_ship_picking._action_done()
|
||
|
||
stock_return_picking_form = Form(self.env['stock.return.picking']
|
||
.with_context(active_ids=picking_pack.ids, active_id=picking_pack.ids[0],
|
||
active_model='stock.picking'))
|
||
stock_return_picking = stock_return_picking_form.save()
|
||
stock_return_picking.product_return_moves.quantity = 1.0
|
||
stock_return_picking_action = stock_return_picking.create_returns()
|
||
return_pack_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||
|
||
return_pack_picking.move_ids[0].move_line_ids[0].qty_done = 1.0
|
||
return_pack_picking._action_done()
|
||
|
||
stock_return_picking_form = Form(self.env['stock.return.picking']
|
||
.with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0],
|
||
active_model='stock.picking'))
|
||
stock_return_picking = stock_return_picking_form.save()
|
||
stock_return_picking.product_return_moves.quantity = 1.0
|
||
stock_return_picking_action = stock_return_picking.create_returns()
|
||
return_pick_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||
|
||
return_pick_picking.move_ids[0].move_line_ids[0].qty_done = 1.0
|
||
return_pick_picking._action_done()
|
||
|
||
# Now that everything is returned we will check if the return moves are correctly linked between them.
|
||
# +--------------------------------------------------------------------------------------------------------+
|
||
# | -- picking_pick(1) --> -- picking_pack(2) --> -- picking_ship(3) -->
|
||
# | Stock Pack Output Customer
|
||
# | <--- return pick(6) -- <--- return pack(5) -- <--- return ship(4) --
|
||
# +--------------------------------------------------------------------------------------------------------+
|
||
# Recaps of final link (MO = move_orig_ids, MD = move_dest_ids)
|
||
# picking_pick(1) : MO = (), MD = (2,6)
|
||
# picking_pack(2) : MO = (1), MD = (3,5)
|
||
# picking ship(3) : MO = (2), MD = (4)
|
||
# return ship(4) : MO = (3), MD = (5)
|
||
# return pack(5) : MO = (2, 4), MD = (6)
|
||
# return pick(6) : MO = (1, 5), MD = ()
|
||
|
||
self.assertEqual(len(picking_pick.move_ids.move_orig_ids), 0, 'Picking pick should not have origin moves')
|
||
self.assertEqual(set(picking_pick.move_ids.move_dest_ids.ids), set((picking_pack.move_ids | return_pick_picking.move_ids).ids))
|
||
|
||
self.assertEqual(set(picking_pack.move_ids.move_orig_ids.ids), set(picking_pick.move_ids.ids))
|
||
self.assertEqual(set(picking_pack.move_ids.move_dest_ids.ids), set((picking_ship.move_ids | return_pack_picking.move_ids).ids))
|
||
|
||
self.assertEqual(set(picking_ship.move_ids.move_orig_ids.ids), set(picking_pack.move_ids.ids))
|
||
self.assertEqual(set(picking_ship.move_ids.move_dest_ids.ids), set(return_ship_picking.move_ids.ids))
|
||
|
||
self.assertEqual(set(return_ship_picking.move_ids.move_orig_ids.ids), set(picking_ship.move_ids.ids))
|
||
self.assertEqual(set(return_ship_picking.move_ids.move_dest_ids.ids), set(return_pack_picking.move_ids.ids))
|
||
|
||
self.assertEqual(set(return_pack_picking.move_ids.move_orig_ids.ids), set((picking_pack.move_ids | return_ship_picking.move_ids).ids))
|
||
self.assertEqual(set(return_pack_picking.move_ids.move_dest_ids.ids), set(return_pick_picking.move_ids.ids))
|
||
|
||
self.assertEqual(set(return_pick_picking.move_ids.move_orig_ids.ids), set((picking_pick.move_ids | return_pack_picking.move_ids).ids))
|
||
self.assertEqual(len(return_pick_picking.move_ids.move_dest_ids), 0)
|
||
|
||
def test_merge_move_mto_mts(self):
|
||
""" Create 2 moves of the same product in the same picking with
|
||
one in 'MTO' and the other one in 'MTS'. The moves shouldn't be merged
|
||
"""
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 3,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': picking_client.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'origin': 'MPS',
|
||
'procure_method': 'make_to_stock',
|
||
})
|
||
picking_client.action_confirm()
|
||
self.assertEqual(len(picking_client.move_ids), 2, 'Moves should not be merged')
|
||
|
||
def test_mto_cancel_move_line(self):
|
||
""" Create a pick ship situation. Then process the pick picking
|
||
with a backorder. Then try to unlink the move line created on
|
||
the ship and check if the picking and move state are updated.
|
||
Then validate the backorder and unlink the ship move lines in
|
||
order to check again if the picking and state are updated.
|
||
"""
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
location = self.env['stock.location'].browse(self.stock_location)
|
||
|
||
# make some stock
|
||
self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
|
||
picking_pick.move_ids.quantity_done = 5.0
|
||
backorder_wizard_values = picking_pick.button_validate()
|
||
backorder_wizard = self.env[(backorder_wizard_values.get('res_model'))].browse(backorder_wizard_values.get('res_id')).with_context(backorder_wizard_values['context'])
|
||
backorder_wizard.process()
|
||
|
||
self.assertTrue(picking_client.move_line_ids, 'A move line should be created.')
|
||
self.assertEqual(picking_client.move_line_ids.reserved_uom_qty, 5, 'The move line should have 5 unit reserved.')
|
||
|
||
# Directly delete the move lines on the picking. (Use show detail operation on picking type)
|
||
# Should do the same behavior than unreserve
|
||
picking_client.move_line_ids.unlink()
|
||
|
||
self.assertEqual(picking_client.move_ids.state, 'waiting', 'The move state should be waiting since nothing is reserved and another origin move still in progess.')
|
||
self.assertEqual(picking_client.state, 'waiting', 'The picking state should not be ready anymore.')
|
||
|
||
picking_client.action_assign()
|
||
|
||
back_order = self.env['stock.picking'].search([('backorder_id', '=', picking_pick.id)])
|
||
back_order.move_ids.quantity_done = 5
|
||
back_order.button_validate()
|
||
|
||
self.assertEqual(picking_client.move_ids.reserved_availability, 10, 'The total quantity should be reserved since everything is available.')
|
||
picking_client.move_line_ids.unlink()
|
||
|
||
self.assertEqual(picking_client.move_ids.state, 'confirmed', 'The move should be confirmed since all the origin moves are processed.')
|
||
self.assertEqual(picking_client.state, 'confirmed', 'The picking should be confirmed since all the moves are confirmed.')
|
||
|
||
def test_unreserve(self):
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
|
||
self.assertEqual(picking_pick.state, 'confirmed')
|
||
picking_pick.do_unreserve()
|
||
self.assertEqual(picking_pick.state, 'confirmed')
|
||
location = self.env['stock.location'].browse(self.stock_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
|
||
picking_pick.action_assign()
|
||
self.assertEqual(picking_pick.state, 'assigned')
|
||
picking_pick.do_unreserve()
|
||
self.assertEqual(picking_pick.state, 'confirmed')
|
||
|
||
self.assertEqual(picking_client.state, 'waiting')
|
||
picking_client.do_unreserve()
|
||
self.assertEqual(picking_client.state, 'waiting')
|
||
|
||
def test_return_location(self):
|
||
""" In a pick ship scenario, send two items to the customer, then return one in the ship
|
||
location and one in a return location that is located in another warehouse.
|
||
"""
|
||
pick_location = self.env['stock.location'].browse(self.stock_location)
|
||
pick_location.return_location = True
|
||
|
||
return_warehouse = self.env['stock.warehouse'].create({'name': 'return warehouse', 'code': 'rw'})
|
||
return_location = self.env['stock.location'].create({
|
||
'name': 'return internal',
|
||
'usage': 'internal',
|
||
'location_id': return_warehouse.view_location_id.id
|
||
})
|
||
|
||
self.env['stock.quant']._update_available_quantity(self.productA, pick_location, 10.0)
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
|
||
# send the items to the customer
|
||
picking_pick.action_assign()
|
||
picking_pick.move_ids[0].move_line_ids[0].qty_done = 10.0
|
||
picking_pick._action_done()
|
||
picking_client.move_ids[0].move_line_ids[0].qty_done = 10.0
|
||
picking_client._action_done()
|
||
|
||
# return half in the pick location
|
||
stock_return_picking_form = Form(self.env['stock.return.picking']
|
||
.with_context(active_ids=picking_client.ids, active_id=picking_client.ids[0],
|
||
active_model='stock.picking'))
|
||
return1 = stock_return_picking_form.save()
|
||
return1.product_return_moves.quantity = 5.0
|
||
return1.location_id = pick_location.id
|
||
return_to_pick_picking_action = return1.create_returns()
|
||
|
||
return_to_pick_picking = self.env['stock.picking'].browse(return_to_pick_picking_action['res_id'])
|
||
return_to_pick_picking.move_ids[0].move_line_ids[0].qty_done = 5.0
|
||
return_to_pick_picking._action_done()
|
||
|
||
# return the remainig products in the return warehouse
|
||
stock_return_picking_form = Form(self.env['stock.return.picking']
|
||
.with_context(active_ids=picking_client.ids, active_id=picking_client.ids[0],
|
||
active_model='stock.picking'))
|
||
return2 = stock_return_picking_form.save()
|
||
return2.product_return_moves.quantity = 5.0
|
||
return2.location_id = return_location.id
|
||
return_to_return_picking_action = return2.create_returns()
|
||
|
||
return_to_return_picking = self.env['stock.picking'].browse(return_to_return_picking_action['res_id'])
|
||
return_to_return_picking.move_ids[0].move_line_ids[0].qty_done = 5.0
|
||
return_to_return_picking._action_done()
|
||
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pick_location), 5.0)
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, return_location), 5.0)
|
||
self.assertEqual(len(self.env['stock.quant'].search([('product_id', '=', self.productA.id), ('quantity', '!=', 0)])), 2)
|
||
|
||
def test_return_lot(self):
|
||
""" With two distinct deliveries for the same product tracked by lot, ensure that the
|
||
return of the second picking suggest the lot from the picking returned.
|
||
"""
|
||
self.productA.tracking = 'lot'
|
||
lot1 = self.env['stock.lot'].create({
|
||
'name': 'lot1',
|
||
'product_id': self.productA.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
lot2 = self.env['stock.lot'].create({
|
||
'name': 'lot2',
|
||
'product_id': self.productA.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
lot3 = self.env['stock.lot'].create({
|
||
'name': 'lot3',
|
||
'product_id': self.productA.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 7.0, lot_id=lot1)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 7.0, lot_id=lot2)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 7.0, lot_id=lot3)
|
||
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
picking_pick.action_confirm()
|
||
picking_pick.action_assign()
|
||
for move_line in picking_pick.move_line_ids:
|
||
move_line.qty_done = move_line.reserved_uom_qty
|
||
picking_pick._action_done()
|
||
picking_client.action_confirm()
|
||
picking_client.action_assign()
|
||
for move_line in picking_client.move_line_ids:
|
||
move_line.qty_done = move_line.reserved_uom_qty
|
||
picking_client._action_done()
|
||
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
picking_pick.action_confirm()
|
||
picking_pick.action_assign()
|
||
for move_line in picking_pick.move_line_ids:
|
||
move_line.qty_done = move_line.reserved_uom_qty
|
||
picking_pick._action_done()
|
||
picking_client.action_confirm()
|
||
picking_client.action_assign()
|
||
for move_line in picking_client.move_line_ids:
|
||
move_line.qty_done = move_line.reserved_uom_qty
|
||
picking_client._action_done()
|
||
|
||
# Following FIFO strategy, First picking should have empty lot1 and took 3 of lot2.
|
||
# So the second picking contains 4 lot2 and 6 lot3
|
||
self.assertEqual(picking_client.move_line_ids[0].lot_id, lot2)
|
||
self.assertEqual(picking_client.move_line_ids[0].qty_done, 4)
|
||
self.assertEqual(picking_client.move_line_ids[1].lot_id, lot3)
|
||
self.assertEqual(picking_client.move_line_ids[1].qty_done, 6)
|
||
|
||
stock_return_picking_form = Form(self.env['stock.return.picking'].with_context(
|
||
active_ids=picking_client.ids, active_id=picking_client.ids[0], active_model='stock.picking'))
|
||
stock_return_picking = stock_return_picking_form.save()
|
||
stock_return_picking.product_return_moves.quantity = 10.0
|
||
return_pick = self.env['stock.picking'].browse(stock_return_picking.create_returns()['res_id'])
|
||
|
||
self.assertEqual(len(return_pick.move_line_ids), 2)
|
||
self.assertEqual(return_pick.move_line_ids[0].lot_id, lot2)
|
||
self.assertEqual(return_pick.move_line_ids[0].reserved_uom_qty, 4)
|
||
self.assertEqual(return_pick.move_line_ids[1].lot_id, lot3)
|
||
self.assertEqual(return_pick.move_line_ids[1].reserved_uom_qty, 6)
|
||
self.assertEqual(return_pick.picking_type_id, picking_client.location_id.warehouse_id.return_type_id)
|
||
|
||
class TestSinglePicking(TestStockCommon):
|
||
def test_backorder_1(self):
|
||
""" Check the good behavior of creating a backorder for an available stock move.
|
||
"""
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
|
||
# make some stock
|
||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2)
|
||
|
||
# assign
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
|
||
|
||
# valid with backorder creation
|
||
delivery_order.move_ids[0].move_line_ids[0].qty_done = 1
|
||
delivery_order._action_done()
|
||
self.assertNotEqual(delivery_order.date_done, False)
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 1.0)
|
||
|
||
backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
|
||
self.assertEqual(backorder.state, 'confirmed')
|
||
backorder.action_assign()
|
||
self.assertEqual(backorder.state, 'assigned')
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
|
||
|
||
def test_backorder_2(self):
|
||
""" Check the good behavior of creating a backorder for a partially available stock move.
|
||
"""
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
|
||
# make some stock
|
||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 1)
|
||
|
||
# assign to partially available
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
|
||
|
||
# valid with backorder creation
|
||
delivery_order.move_ids[0].move_line_ids[0].qty_done = 1
|
||
delivery_order._action_done()
|
||
self.assertNotEqual(delivery_order.date_done, False)
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
|
||
|
||
backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
|
||
self.assertEqual(backorder.state, 'confirmed')
|
||
|
||
def test_backorder_3(self):
|
||
""" Check the good behavior of creating a backorder for an available move on a picking with
|
||
two available moves.
|
||
"""
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productB.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productB.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
|
||
# make some stock
|
||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2)
|
||
|
||
# assign to partially available
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
|
||
delivery_order.move_ids[0].move_line_ids[0].qty_done = 2
|
||
delivery_order._action_done()
|
||
|
||
backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
|
||
self.assertEqual(backorder.state, 'confirmed')
|
||
|
||
def test_backorder_4(self):
|
||
""" Check the good behavior if no backorder are created
|
||
for a picking with a missing product.
|
||
"""
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productB.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productB.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
|
||
# Update available quantities for each products
|
||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2)
|
||
self.env['stock.quant']._update_available_quantity(self.productB, pack_location, 2)
|
||
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
|
||
# Process only one product without creating a backorder
|
||
delivery_order.move_ids[0].move_line_ids[0].qty_done = 2
|
||
res_dict = delivery_order.button_validate()
|
||
backorder_wizard = Form(self.env['stock.backorder.confirmation'].with_context(res_dict['context'])).save()
|
||
backorder_wizard.process_cancel_backorder()
|
||
|
||
# No backorder should be created and the move corresponding to the missing product should be cancelled
|
||
backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
|
||
self.assertFalse(backorder)
|
||
self.assertEqual(delivery_order.state, 'done')
|
||
self.assertEqual(delivery_order.move_ids[1].state, 'cancel')
|
||
|
||
def test_assign_deadline(self):
|
||
""" Check if similar items with shorter deadline are prioritized. """
|
||
delivery_order = self.PickingObj.create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
# Avoid to merge move3 and move4 for the test case
|
||
self.env['ir.config_parameter'].create({
|
||
'key': 'stock.merge_only_same_date',
|
||
'value': True
|
||
})
|
||
move1 = self.MoveObj.create({
|
||
'name': "move1",
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 4,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'date_deadline': datetime.now() + relativedelta(days=1)
|
||
})
|
||
move2 = self.MoveObj.create({
|
||
'name': "move2",
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 4,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'date_deadline': datetime.now() + relativedelta(days=2)
|
||
})
|
||
move3 = self.MoveObj.create({
|
||
'name': "move3",
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 1,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'date': datetime.now() + relativedelta(days=10)
|
||
})
|
||
move4 = self.MoveObj.create({
|
||
'name': "move4",
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 1,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'date': datetime.now() + relativedelta(days=0)
|
||
})
|
||
|
||
# make some stock
|
||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||
self.StockQuantObj._update_available_quantity(self.productA, pack_location, 2)
|
||
|
||
# assign to partially available
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
|
||
self.assertEqual(move1.reserved_availability, 2, "Earlier deadline should have reserved quantity")
|
||
self.assertEqual(move2.reserved_availability, 0, "Later deadline should not have reserved quantity")
|
||
|
||
# add new stock
|
||
self.StockQuantObj._update_available_quantity(self.productA, pack_location, 2)
|
||
delivery_order.action_assign()
|
||
self.assertEqual(move1.reserved_availability, 4, "Earlier deadline should have reserved quantity")
|
||
self.assertEqual(move2.reserved_availability, 0, "Later deadline should not have reserved quantity")
|
||
|
||
self.StockQuantObj._update_available_quantity(self.productA, pack_location, 1)
|
||
delivery_order.action_assign()
|
||
self.assertEqual(move1.reserved_availability, 4, "Earlier deadline should have reserved quantity")
|
||
self.assertEqual(move2.reserved_availability, 1, "Move with deadline should take priority")
|
||
self.assertEqual(move3.reserved_availability, 0, "Move without deadline should not have reserved quantity")
|
||
self.assertEqual(move4.reserved_availability, 0, "Move without deadline should not have reserved quantity")
|
||
|
||
self.StockQuantObj._update_available_quantity(self.productA, pack_location, 4)
|
||
delivery_order.action_assign()
|
||
self.assertEqual(move1.reserved_availability, 4, "Earlier deadline should have reserved quantity")
|
||
self.assertEqual(move2.reserved_availability, 4, "Move with deadline should take priority")
|
||
self.assertEqual(move3.reserved_availability, 0, "Latest move without deadline should not have reserved quantity")
|
||
self.assertEqual(move4.reserved_availability, 1, "Earlier move without deadline should take the priority")
|
||
|
||
def test_extra_move_1(self):
|
||
""" Check the good behavior of creating an extra move in a delivery order. This usecase
|
||
simulates the delivery of 2 item while the initial stock move had to move 1 and there's
|
||
only 1 in stock.
|
||
"""
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
move1 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 1,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
|
||
# make some stock
|
||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 1)
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 1.0)
|
||
|
||
# assign to available
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
|
||
|
||
# valid with backorder creation
|
||
delivery_order.move_ids[0].move_line_ids[0].qty_done = 2
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
|
||
delivery_order._action_done()
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location, allow_negative=True), -1.0)
|
||
|
||
self.assertEqual(move1.product_qty, 2.0)
|
||
self.assertEqual(move1.quantity_done, 2.0)
|
||
self.assertEqual(move1.reserved_availability, 0.0)
|
||
self.assertEqual(move1.move_line_ids.reserved_qty, 0.0) # change reservation to 0 for done move
|
||
self.assertEqual(sum(move1.move_line_ids.mapped('qty_done')), 2.0)
|
||
self.assertEqual(move1.state, 'done')
|
||
|
||
def test_extra_move_2(self):
|
||
""" Check the good behavior of creating an extra move in a delivery order. This usecase
|
||
simulates the delivery of 3 item while the initial stock move had to move 1 and there's
|
||
only 1 in stock.
|
||
"""
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
move1 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 1,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
|
||
# make some stock
|
||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 1)
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 1.0)
|
||
|
||
# assign to available
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
|
||
|
||
# valid with backorder creation
|
||
delivery_order.move_ids[0].move_line_ids[0].qty_done = 3
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
|
||
delivery_order._action_done()
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location, allow_negative=True), -2.0)
|
||
|
||
self.assertEqual(move1.product_qty, 3.0)
|
||
self.assertEqual(move1.quantity_done, 3.0)
|
||
self.assertEqual(move1.reserved_availability, 0.0)
|
||
self.assertEqual(move1.move_line_ids.reserved_qty, 0.0) # change reservation to 0 for done move
|
||
self.assertEqual(sum(move1.move_line_ids.mapped('qty_done')), 3.0)
|
||
self.assertEqual(move1.state, 'done')
|
||
|
||
def test_extra_move_3(self):
|
||
""" Check the good behavior of creating an extra move in a receipt. This usecase simulates
|
||
the receipt of 2 item while the initial stock move had to move 1.
|
||
"""
|
||
receipt = self.env['stock.picking'].create({
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
'picking_type_id': self.picking_type_in,
|
||
})
|
||
move1 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 1,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
})
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
|
||
# assign to available
|
||
receipt.action_confirm()
|
||
receipt.action_assign()
|
||
self.assertEqual(receipt.state, 'assigned')
|
||
|
||
# valid with backorder creation
|
||
receipt.move_ids[0].move_line_ids[0].qty_done = 2
|
||
receipt._action_done()
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 2.0)
|
||
|
||
self.assertEqual(move1.product_qty, 2.0)
|
||
self.assertEqual(move1.quantity_done, 2.0)
|
||
self.assertEqual(move1.reserved_availability, 0.0)
|
||
self.assertEqual(move1.move_line_ids.reserved_qty, 0.0) # change reservation to 0 for done move
|
||
self.assertEqual(sum(move1.move_line_ids.mapped('qty_done')), 2.0)
|
||
self.assertEqual(move1.state, 'done')
|
||
|
||
def test_extra_move_4(self):
|
||
""" Create a picking with similar moves (created after
|
||
confirmation). Action done should propagate all the extra
|
||
quantity and only merge extra moves in their original moves.
|
||
"""
|
||
delivery = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 5,
|
||
'quantity_done': 10,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 5)
|
||
delivery.action_confirm()
|
||
delivery.action_assign()
|
||
|
||
delivery.write({
|
||
'move_ids': [(0, 0, {
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 0,
|
||
'quantity_done': 10,
|
||
'state': 'assigned',
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
})]
|
||
})
|
||
delivery._action_done()
|
||
self.assertEqual(len(delivery.move_ids), 2, 'Move should not be merged together')
|
||
for move in delivery.move_ids:
|
||
self.assertEqual(move.quantity_done, move.product_uom_qty, 'Initial demand should be equals to quantity done')
|
||
|
||
def test_extra_move_5(self):
|
||
""" Create a picking a move that is problematic with
|
||
rounding (5.95 - 5.5 = 0.4500000000000002). Ensure that
|
||
initial demand is corrct afer action_done and backoder
|
||
are not created.
|
||
"""
|
||
delivery = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
product = self.kgB
|
||
self.MoveObj.create({
|
||
'name': product.name,
|
||
'product_id': product.id,
|
||
'product_uom_qty': 5.5,
|
||
'quantity_done': 5.95,
|
||
'product_uom': product.uom_id.id,
|
||
'picking_id': delivery.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.env['stock.quant']._update_available_quantity(product, stock_location, 5.5)
|
||
delivery.action_confirm()
|
||
delivery.action_assign()
|
||
delivery._action_done()
|
||
self.assertEqual(delivery.move_ids.product_uom_qty, 5.95, 'Move initial demand should be 5.95')
|
||
|
||
back_order = self.env['stock.picking'].search([('backorder_id', '=', delivery.id)])
|
||
self.assertFalse(back_order, 'There should be no back order')
|
||
|
||
def test_recheck_availability_1(self):
|
||
""" Check the good behavior of check availability. I create a DO for 2 unit with
|
||
only one in stock. After the first check availability, I should have 1 reserved
|
||
product with one move line. After adding a second unit in stock and recheck availability.
|
||
The DO should have 2 reserved unit, be in available state and have only one move line.
|
||
"""
|
||
self.env['stock.quant']._update_available_quantity(self.productA, self.env['stock.location'].browse(self.stock_location), 1.0)
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
move1 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
# Check State
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(move1.state, 'partially_available')
|
||
|
||
# Check reserved quantity
|
||
self.assertEqual(move1.reserved_availability, 1.0)
|
||
self.assertEqual(len(move1.move_line_ids), 1)
|
||
self.assertEqual(move1.move_line_ids.reserved_qty, 1)
|
||
|
||
inventory_quant = self.env['stock.quant'].create({
|
||
'location_id': self.stock_location,
|
||
'product_id': self.productA.id,
|
||
'inventory_quantity': 2
|
||
})
|
||
inventory_quant.action_apply_inventory()
|
||
delivery_order.action_assign()
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(move1.state, 'assigned')
|
||
|
||
# Check reserved quantity
|
||
self.assertEqual(move1.reserved_availability, 2.0)
|
||
self.assertEqual(len(move1.move_line_ids), 1)
|
||
self.assertEqual(move1.move_line_ids.reserved_qty, 2)
|
||
|
||
def test_recheck_availability_2(self):
|
||
""" Same check than test_recheck_availability_1 but with lot this time.
|
||
If the new product has the same lot that already reserved one, the move lines
|
||
reserved quantity should be updated.
|
||
Otherwise a new move lines with the new lot should be added.
|
||
"""
|
||
self.productA.tracking = 'lot'
|
||
lot1 = self.env['stock.lot'].create({
|
||
'name': 'lot1',
|
||
'product_id': self.productA.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 1.0, lot_id=lot1)
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
move1 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
# Check State
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(move1.state, 'partially_available')
|
||
|
||
# Check reserved quantity
|
||
self.assertEqual(move1.reserved_availability, 1.0)
|
||
self.assertEqual(len(move1.move_line_ids), 1)
|
||
self.assertEqual(move1.move_line_ids.reserved_qty, 1)
|
||
|
||
inventory_quant = self.env['stock.quant'].create({
|
||
'location_id': self.stock_location,
|
||
'product_id': self.productA.id,
|
||
'inventory_quantity': 2,
|
||
'lot_id': lot1.id
|
||
})
|
||
inventory_quant.action_apply_inventory()
|
||
delivery_order.action_assign()
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(move1.state, 'assigned')
|
||
|
||
# Check reserved quantity
|
||
self.assertEqual(move1.reserved_availability, 2.0)
|
||
self.assertEqual(len(move1.move_line_ids), 1)
|
||
self.assertEqual(move1.move_line_ids.lot_id.id, lot1.id)
|
||
self.assertEqual(move1.move_line_ids.reserved_qty, 2)
|
||
|
||
def test_recheck_availability_3(self):
|
||
""" Same check than test_recheck_availability_2 but with different lots.
|
||
"""
|
||
self.productA.tracking = 'lot'
|
||
lot1 = self.env['stock.lot'].create({
|
||
'name': 'lot1',
|
||
'product_id': self.productA.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
lot2 = self.env['stock.lot'].create({
|
||
'name': 'lot2',
|
||
'product_id': self.productA.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 1.0, lot_id=lot1)
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
move1 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
# Check State
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(move1.state, 'partially_available')
|
||
|
||
# Check reserved quantity
|
||
self.assertEqual(move1.reserved_availability, 1.0)
|
||
self.assertEqual(len(move1.move_line_ids), 1)
|
||
self.assertEqual(move1.move_line_ids.reserved_qty, 1)
|
||
|
||
inventory_quant = self.env['stock.quant'].create({
|
||
'location_id': self.stock_location,
|
||
'product_id': self.productA.id,
|
||
'inventory_quantity': 1,
|
||
'lot_id': lot2.id
|
||
})
|
||
inventory_quant.action_apply_inventory()
|
||
delivery_order.action_assign()
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(move1.state, 'assigned')
|
||
|
||
# Check reserved quantity
|
||
self.assertEqual(move1.reserved_availability, 2.0)
|
||
self.assertEqual(len(move1.move_line_ids), 2)
|
||
move_lines = move1.move_line_ids.sorted()
|
||
self.assertEqual(move_lines[0].lot_id.id, lot1.id)
|
||
self.assertEqual(move_lines[1].lot_id.id, lot2.id)
|
||
|
||
def test_recheck_availability_4(self):
|
||
""" Same check than test_recheck_availability_2 but with serial number this time.
|
||
Serial number reservation should always create a new move line.
|
||
"""
|
||
self.productA.tracking = 'serial'
|
||
serial1 = self.env['stock.lot'].create({
|
||
'name': 'serial1',
|
||
'product_id': self.productA.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
serial2 = self.env['stock.lot'].create({
|
||
'name': 'serial2',
|
||
'product_id': self.productA.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 1.0, lot_id=serial1)
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
move1 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
# Check State
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(move1.state, 'partially_available')
|
||
|
||
# Check reserved quantity
|
||
self.assertEqual(move1.reserved_availability, 1.0)
|
||
self.assertEqual(len(move1.move_line_ids), 1)
|
||
self.assertEqual(move1.move_line_ids.reserved_qty, 1)
|
||
|
||
inventory_quant = self.env['stock.quant'].create({
|
||
'location_id': self.stock_location,
|
||
'product_id': self.productA.id,
|
||
'inventory_quantity': 1,
|
||
'lot_id': serial2.id
|
||
})
|
||
inventory_quant.action_apply_inventory()
|
||
delivery_order.action_assign()
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(move1.state, 'assigned')
|
||
|
||
# Check reserved quantity
|
||
self.assertEqual(move1.reserved_availability, 2.0)
|
||
self.assertEqual(len(move1.move_line_ids), 2)
|
||
move_lines = move1.move_line_ids.sorted()
|
||
self.assertEqual(move_lines[0].lot_id.id, serial1.id)
|
||
self.assertEqual(move_lines[1].lot_id.id, serial2.id)
|
||
|
||
def test_use_create_lot_use_existing_lot_1(self):
|
||
""" Check the behavior of a picking when `use_create_lot` and `use_existing_lot` are
|
||
set to False and there's a move for a tracked product.
|
||
"""
|
||
self.env['stock.picking.type']\
|
||
.browse(self.picking_type_out)\
|
||
.write({
|
||
'use_create_lots': False,
|
||
'use_existing_lots': False,
|
||
})
|
||
self.productA.tracking = 'lot'
|
||
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'picking_type_id': self.picking_type_out,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
|
||
delivery_order.action_confirm()
|
||
delivery_order.move_ids.quantity_done = 2
|
||
# do not set a lot_id or lot_name, it should work
|
||
delivery_order._action_done()
|
||
|
||
def test_use_create_lot_use_existing_lot_2(self):
|
||
""" Check the behavior of a picking when `use_create_lot` and `use_existing_lot` are
|
||
set to True and there's a move for a tracked product.
|
||
"""
|
||
self.env['stock.picking.type']\
|
||
.browse(self.picking_type_out)\
|
||
.write({
|
||
'use_create_lots': True,
|
||
'use_existing_lots': True,
|
||
})
|
||
self.productA.tracking = 'lot'
|
||
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'picking_type_id': self.picking_type_out,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
|
||
delivery_order.action_confirm()
|
||
delivery_order.move_ids.quantity_done = 2
|
||
move_line = delivery_order.move_ids.move_line_ids
|
||
|
||
# not lot_name set, should raise
|
||
with self.assertRaises(UserError):
|
||
delivery_order._action_done()
|
||
|
||
# enter a new lot name, should work
|
||
move_line.lot_name = 'newlot'
|
||
delivery_order._action_done()
|
||
|
||
def test_use_create_lot_use_existing_lot_3(self):
|
||
""" Check the behavior of a picking when `use_create_lot` is set to True and
|
||
`use_existing_lot` is set to False and there's a move for a tracked product.
|
||
"""
|
||
self.env['stock.picking.type']\
|
||
.browse(self.picking_type_out)\
|
||
.write({
|
||
'use_create_lots': True,
|
||
'use_existing_lots': False,
|
||
})
|
||
self.productA.tracking = 'lot'
|
||
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'picking_type_id': self.picking_type_out,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
|
||
delivery_order.action_confirm()
|
||
delivery_order.move_ids.quantity_done = 2
|
||
move_line = delivery_order.move_ids.move_line_ids
|
||
|
||
# not lot_name set, should raise
|
||
with self.assertRaises(UserError):
|
||
delivery_order._action_done()
|
||
|
||
# enter a new lot name, should work
|
||
move_line.lot_name = 'newlot'
|
||
delivery_order._action_done()
|
||
|
||
def test_use_create_lot_use_existing_lot_4(self):
|
||
""" Check the behavior of a picking when `use_create_lot` is set to False and
|
||
`use_existing_lot` is set to True and there's a move for a tracked product.
|
||
"""
|
||
self.env['stock.picking.type']\
|
||
.browse(self.picking_type_out)\
|
||
.write({
|
||
'use_create_lots': False,
|
||
'use_existing_lots': True,
|
||
})
|
||
self.productA.tracking = 'lot'
|
||
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'picking_type_id': self.picking_type_out,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
|
||
delivery_order.action_confirm()
|
||
delivery_order.move_ids.quantity_done = 2
|
||
move_line = delivery_order.move_ids.move_line_ids
|
||
|
||
# not lot_name set, should raise
|
||
with self.assertRaises(UserError):
|
||
delivery_order._action_done()
|
||
|
||
# creating a lot from the view should raise
|
||
with self.assertRaises(UserError):
|
||
self.env['stock.lot']\
|
||
.with_context(active_picking_id=delivery_order.id)\
|
||
.create({
|
||
'name': 'lot1',
|
||
'product_id': self.productA.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
|
||
# enter an existing lot_id, should work
|
||
lot1 = self.env['stock.lot'].create({
|
||
'name': 'lot1',
|
||
'product_id': self.productA.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
move_line.lot_id = lot1
|
||
delivery_order._action_done()
|
||
|
||
def test_merge_moves_1(self):
|
||
receipt = self.env['stock.picking'].create({
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
'picking_type_id': self.picking_type_in,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 3,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 5,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 1,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productB.name,
|
||
'product_id': self.productB.id,
|
||
'product_uom_qty': 5,
|
||
'product_uom': self.productB.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
})
|
||
receipt.action_confirm()
|
||
self.assertEqual(len(receipt.move_ids), 2, 'Moves were not merged')
|
||
self.assertEqual(receipt.move_ids.filtered(lambda m: m.product_id == self.productA).product_uom_qty, 9, 'Merged quantity is not correct')
|
||
self.assertEqual(receipt.move_ids.filtered(lambda m: m.product_id == self.productB).product_uom_qty, 5, 'Merge should not impact product B reserved quantity')
|
||
|
||
def test_merge_moves_2(self):
|
||
receipt = self.env['stock.picking'].create({
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
'picking_type_id': self.picking_type_in,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 3,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
'origin': 'MPS'
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 5,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
'origin': 'PO0001'
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 3,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
'origin': 'MPS'
|
||
})
|
||
receipt.action_confirm()
|
||
self.assertEqual(len(receipt.move_ids), 1, 'Moves were not merged')
|
||
self.assertEqual(receipt.move_ids.origin.count('MPS'), 1, 'Origin not merged together or duplicated')
|
||
self.assertEqual(receipt.move_ids.origin.count('PO0001'), 1, 'Origin not merged together or duplicated')
|
||
|
||
def test_merge_moves_3(self):
|
||
""" Create 2 moves without initial_demand and already a
|
||
quantity done. Check that we still have only 2 moves after
|
||
validation.
|
||
"""
|
||
receipt = self.env['stock.picking'].create({
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
'picking_type_id': self.picking_type_in,
|
||
})
|
||
move_1 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 0,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
'origin': 'MPS'
|
||
})
|
||
move_2 = self.MoveObj.create({
|
||
'name': self.productB.name,
|
||
'product_id': self.productB.id,
|
||
'product_uom_qty': 0,
|
||
'product_uom': self.productB.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
'origin': 'PO0001'
|
||
})
|
||
move_1.quantity_done = 5
|
||
move_2.quantity_done = 5
|
||
receipt.button_validate()
|
||
self.assertEqual(len(receipt.move_ids), 2, 'Moves were not merged')
|
||
|
||
def test_merge_chained_moves(self):
|
||
""" Imagine multiple step delivery. Two different receipt picking for the same product should only generate
|
||
1 picking from input to QC and another from QC to stock. The link at the end should follow this scheme.
|
||
Move receipt 1 \
|
||
Move Input-> QC - Move QC -> Stock
|
||
Move receipt 2 /
|
||
"""
|
||
warehouse = self.env['stock.warehouse'].create({
|
||
'name': 'TEST WAREHOUSE',
|
||
'code': 'TEST1',
|
||
'reception_steps': 'three_steps',
|
||
})
|
||
receipt1 = self.env['stock.picking'].create({
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': warehouse.wh_input_stock_loc_id.id,
|
||
'picking_type_id': warehouse.in_type_id.id,
|
||
})
|
||
move_receipt_1 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 5,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt1.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': warehouse.wh_input_stock_loc_id.id,
|
||
})
|
||
receipt2 = self.env['stock.picking'].create({
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': warehouse.wh_input_stock_loc_id.id,
|
||
'picking_type_id': warehouse.in_type_id.id,
|
||
})
|
||
move_receipt_2 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 3,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt2.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': warehouse.wh_input_stock_loc_id.id,
|
||
})
|
||
receipt1.action_confirm()
|
||
receipt2.action_confirm()
|
||
|
||
# Check following move has been created and grouped in one picking.
|
||
self.assertTrue(move_receipt_1.move_dest_ids, 'No move created from push rules')
|
||
self.assertTrue(move_receipt_2.move_dest_ids, 'No move created from push rules')
|
||
self.assertEqual(move_receipt_1.move_dest_ids.picking_id, move_receipt_2.move_dest_ids.picking_id, 'Destination moves should be in the same picking')
|
||
|
||
# Check link for input move are correct.
|
||
input_move = move_receipt_2.move_dest_ids
|
||
self.assertEqual(len(input_move.move_dest_ids), 1)
|
||
self.assertEqual(set(input_move.move_orig_ids.ids), set((move_receipt_2 | move_receipt_1).ids),
|
||
'Move from input to QC should be merged and have the two receipt moves as origin.')
|
||
self.assertEqual(move_receipt_1.move_dest_ids, input_move)
|
||
self.assertEqual(move_receipt_2.move_dest_ids, input_move)
|
||
|
||
# Check link for quality check move are also correct.
|
||
qc_move = input_move.move_dest_ids
|
||
self.assertEqual(len(qc_move), 1)
|
||
self.assertTrue(qc_move.move_orig_ids == input_move, 'Move between QC and stock should only have the input move as origin')
|
||
|
||
def test_merge_chained_moves_multi_confirm(self):
|
||
""" Imagine multiple step delivery. A receipt picking for the same product should by add to
|
||
a existing picking from input to QC and another from QC to stock.
|
||
This existing picking is confirm in the same time (not possible in stock, but can be with batch picking)
|
||
and have some move to merge.
|
||
"""
|
||
warehouse = self.env['stock.warehouse'].create({
|
||
'name': 'TEST WAREHOUSE',
|
||
'code': 'TEST1',
|
||
'reception_steps': 'three_steps',
|
||
})
|
||
receipt1 = self.env['stock.picking'].create({
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': warehouse.wh_input_stock_loc_id.id,
|
||
'picking_type_id': warehouse.in_type_id.id,
|
||
})
|
||
move_receipt_1 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 5,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt1.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': warehouse.wh_input_stock_loc_id.id,
|
||
})
|
||
receipt2 = self.env['stock.picking'].create({
|
||
'location_id': warehouse.wh_input_stock_loc_id.id,
|
||
'location_dest_id': warehouse.wh_qc_stock_loc_id.id,
|
||
'picking_type_id': warehouse.int_type_id.id,
|
||
})
|
||
move1_receipt_2 = self.MoveObj.create({
|
||
'name': self.productB.name,
|
||
'product_id': self.productB.id,
|
||
'product_uom_qty': 1,
|
||
'product_uom': self.productB.uom_id.id,
|
||
'picking_id': receipt2.id,
|
||
'location_id': warehouse.wh_input_stock_loc_id.id,
|
||
'location_dest_id': warehouse.wh_qc_stock_loc_id.id,
|
||
})
|
||
move2_receipt_2 = self.MoveObj.create({
|
||
'name': self.productB.name,
|
||
'product_id': self.productB.id,
|
||
'product_uom_qty': 2,
|
||
'product_uom': self.productB.uom_id.id,
|
||
'picking_id': receipt2.id,
|
||
'location_id': warehouse.wh_input_stock_loc_id.id,
|
||
'location_dest_id': warehouse.wh_qc_stock_loc_id.id,
|
||
})
|
||
(receipt1 | receipt2).action_confirm()
|
||
|
||
# Check following move has been created
|
||
self.assertTrue(move_receipt_1.move_dest_ids, 'No move created from push rules')
|
||
self.assertTrue((move1_receipt_2 | move2_receipt_2).exists().move_dest_ids, 'No move created from push rules')
|
||
self.assertEqual(len((move1_receipt_2 | move2_receipt_2).exists()), 1, 'Move has been merged with the other one')
|
||
self.assertEqual(move_receipt_1.move_dest_ids.picking_id, receipt2, 'Dest Move of receipt1 should be in the receipt2')
|
||
|
||
# Check no move is still in draft
|
||
self.assertTrue("draft" not in (receipt1 | receipt2).move_ids.mapped("state"))
|
||
|
||
# Check the content of the pickings
|
||
self.assertEqual(receipt1.move_ids.mapped("product_uom_qty"), [5])
|
||
self.assertEqual(receipt2.move_ids.filtered(lambda m: m.product_id == self.productB).mapped("product_uom_qty"), [3])
|
||
self.assertEqual(receipt2.move_ids.filtered(lambda m: m.product_id == self.productA).mapped("product_uom_qty"), [5])
|
||
|
||
def test_empty_moves_validation_1(self):
|
||
""" Use button validate on a picking that contains only moves
|
||
without initial demand and without quantity done should be
|
||
impossible and raise a usererror.
|
||
"""
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 0,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productB.name,
|
||
'product_id': self.productB.id,
|
||
'product_uom_qty': 0,
|
||
'product_uom': self.productB.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
with self.assertRaises(UserError):
|
||
delivery_order.button_validate()
|
||
|
||
def test_empty_moves_validation_2(self):
|
||
""" Use button validate on a picking that contains only moves
|
||
without initial demand but at least one with a quantity done
|
||
should process the move with quantity done and cancel the
|
||
other.
|
||
"""
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
move_a = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 0,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
move_b = self.MoveObj.create({
|
||
'name': self.productB.name,
|
||
'product_id': self.productB.id,
|
||
'product_uom_qty': 0,
|
||
'product_uom': self.productB.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
move_a.quantity_done = 1
|
||
delivery_order.button_validate()
|
||
|
||
self.assertEqual(move_a.state, 'done')
|
||
self.assertEqual(move_b.state, 'cancel')
|
||
back_order = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
|
||
self.assertFalse(back_order, 'There should be no back order')
|
||
|
||
def test_unlink_move_1(self):
|
||
picking = Form(self.env['stock.picking'])
|
||
ptout = self.env['stock.picking.type'].browse(self.picking_type_out)
|
||
picking.picking_type_id = ptout
|
||
with picking.move_ids_without_package.new() as move:
|
||
move.product_id = self.productA
|
||
move.product_uom_qty = 10
|
||
picking = picking.save()
|
||
self.assertEqual(picking.immediate_transfer, False)
|
||
self.assertEqual(picking.state, 'draft')
|
||
|
||
picking = Form(picking)
|
||
picking.move_ids_without_package.remove(0)
|
||
picking = picking.save()
|
||
self.assertEqual(len(picking.move_ids_without_package), 0)
|
||
|
||
def test_additional_move_1(self):
|
||
""" On a planned trasfer, add a stock move when the picking is already ready. Check that
|
||
the check availability button appears and work.
|
||
"""
|
||
# Make some stock for productA and productB.
|
||
receipt = self.env['stock.picking'].create({
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
'picking_type_id': self.picking_type_in,
|
||
})
|
||
move_1 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 10,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
})
|
||
move_2 = self.MoveObj.create({
|
||
'name': self.productB.name,
|
||
'product_id': self.productB.id,
|
||
'product_uom_qty': 10,
|
||
'product_uom': self.productB.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
})
|
||
receipt.action_confirm()
|
||
move_1.quantity_done = 10
|
||
move_2.quantity_done = 10
|
||
receipt.button_validate()
|
||
self.assertEqual(self.productA.qty_available, 10)
|
||
self.assertEqual(self.productB.qty_available, 10)
|
||
|
||
# Create a delivery for 1 productA, reserve, check the picking is ready
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
'move_type': 'one',
|
||
})
|
||
move_3 = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 10,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': delivery_order.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
})
|
||
delivery_order.action_confirm()
|
||
delivery_order.action_assign()
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
|
||
# Add a unit of productB, the check_availability button should appear.
|
||
delivery_order = Form(delivery_order)
|
||
with delivery_order.move_ids_without_package.new() as move:
|
||
move.product_id = self.productB
|
||
move.product_uom_qty = 10
|
||
delivery_order = delivery_order.save()
|
||
|
||
# The autocoform ran, the picking shoud be confirmed and reservable.
|
||
self.assertEqual(delivery_order.state, 'confirmed')
|
||
self.assertEqual(delivery_order.show_mark_as_todo, False)
|
||
self.assertEqual(delivery_order.show_check_availability, True)
|
||
|
||
delivery_order.action_assign()
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(delivery_order.show_check_availability, False)
|
||
self.assertEqual(delivery_order.show_mark_as_todo, False)
|
||
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.assertEqual(self.env['stock.quant']._gather(self.productA, stock_location).reserved_quantity, 10.0)
|
||
self.assertEqual(self.env['stock.quant']._gather(self.productB, stock_location).reserved_quantity, 10.0)
|
||
|
||
def test_additional_move_2(self):
|
||
""" On an immediate trasfer, add a stock move when the picking is already ready. Check that
|
||
the check availability button doest not appear.
|
||
"""
|
||
# Create a delivery for 1 productA, check the picking is ready
|
||
delivery_order = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
'immediate_transfer': True,
|
||
'move_ids_without_package': [(0, 0, {
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'quantity_done': 5,
|
||
})],
|
||
})
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
|
||
# Add a unit of productB, the check_availability button should not appear.
|
||
delivery_order = Form(delivery_order)
|
||
with delivery_order.move_ids_without_package.new() as move:
|
||
move.product_id = self.productB
|
||
delivery_order = delivery_order.save()
|
||
|
||
self.assertEqual(delivery_order.state, 'assigned')
|
||
self.assertEqual(delivery_order.show_check_availability, False)
|
||
self.assertEqual(delivery_order.show_mark_as_todo, False)
|
||
|
||
def test_owner_1(self):
|
||
# Required for `owner_id` to be visible in the view
|
||
self.env.user.groups_id += self.env.ref("stock.group_tracking_owner")
|
||
"""Make a receipt, set an owner and validate"""
|
||
owner1 = self.env['res.partner'].create({'name': 'owner'})
|
||
receipt = self.env['stock.picking'].create({
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
'picking_type_id': self.picking_type_in,
|
||
})
|
||
move1 = self.env['stock.move'].create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 1,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': receipt.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
})
|
||
receipt.action_confirm()
|
||
receipt = Form(receipt)
|
||
receipt.owner_id = owner1
|
||
receipt = receipt.save()
|
||
wiz = receipt.button_validate()
|
||
wiz = Form(self.env['stock.immediate.transfer'].with_context(wiz['context'])).save()
|
||
wiz.process()
|
||
|
||
supplier_location = self.env['stock.location'].browse(self.supplier_location)
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
supplier_quant = self.env['stock.quant']._gather(self.productA, supplier_location)
|
||
stock_quant = self.env['stock.quant']._gather(self.productA, stock_location)
|
||
|
||
self.assertEqual(supplier_quant.owner_id, owner1)
|
||
self.assertEqual(supplier_quant.quantity, -1)
|
||
self.assertEqual(stock_quant.owner_id, owner1)
|
||
self.assertEqual(stock_quant.quantity, 1)
|
||
|
||
def test_putaway_for_picking_sml(self):
|
||
""" Checks picking's move lines will take in account the putaway rules
|
||
to define the `location_dest_id`.
|
||
"""
|
||
partner = self.env['res.partner'].create({'name': 'Partner'})
|
||
supplier_location = self.env['stock.location'].browse(self.supplier_location)
|
||
stock_location = self.env['stock.location'].create({
|
||
'name': 'test-stock',
|
||
'usage': 'internal',
|
||
})
|
||
shelf_location = self.env['stock.location'].create({
|
||
'name': 'shelf1',
|
||
'usage': 'internal',
|
||
'location_id': stock_location.id,
|
||
})
|
||
|
||
# We need to activate multi-locations to use putaway rules.
|
||
grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
|
||
self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]})
|
||
putaway_product = self.env['stock.putaway.rule'].create({
|
||
'product_id': self.productA.id,
|
||
'location_in_id': stock_location.id,
|
||
'location_out_id': shelf_location.id,
|
||
})
|
||
# Changes config of receipt type to allow to edit move lines directly.
|
||
picking_type = self.env['stock.picking.type'].browse(self.picking_type_in)
|
||
picking_type.show_operations = True
|
||
|
||
receipt_form = Form(self.env['stock.picking'].with_context(
|
||
force_detailed_view=True
|
||
), view='stock.view_picking_form')
|
||
receipt_form.partner_id = partner
|
||
receipt_form.picking_type_id = picking_type
|
||
# <field name="location_id" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"
|
||
receipt_form.location_dest_id = stock_location
|
||
receipt = receipt_form.save()
|
||
with receipt_form.move_line_nosuggest_ids.new() as move_line:
|
||
move_line.product_id = self.productA
|
||
move_line.qty_done = 1.0
|
||
|
||
receipt = receipt_form.save()
|
||
# Checks receipt has still its destination location and checks its move
|
||
# line took the one from the putaway rule.
|
||
self.assertEqual(receipt.location_dest_id.id, stock_location.id)
|
||
self.assertEqual(receipt.move_line_ids.location_dest_id.id, shelf_location.id)
|
||
|
||
def test_cancel_plan_transfer(self):
|
||
""" Test canceling plan transfer """
|
||
# Create picking with stock move.
|
||
picking = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
'move_ids': [(0, 0, {
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 10,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})]
|
||
})
|
||
# Confirm the outgoing picking, state should be changed.
|
||
picking.action_confirm()
|
||
self.assertEqual(picking.state, 'confirmed', "Picking should be in a confirmed state.")
|
||
|
||
# Picking in a confirmed state and try to cancel it.
|
||
picking.action_cancel()
|
||
self.assertEqual(picking.state, 'cancel', "Picking should be in a cancel state.")
|
||
|
||
def test_immediate_transfer(self):
|
||
""" Test picking should be in ready state if immediate transfer and SML is created via view +
|
||
Test picking cancelation with immediate transfer and done quantity"""
|
||
# create picking with stock move line
|
||
picking = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
'immediate_transfer': True,
|
||
'move_line_ids': [(0, 0, {
|
||
'product_id': self.productA.id,
|
||
'qty_done': 10,
|
||
'product_uom_id': self.productA.uom_id.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
})]
|
||
})
|
||
|
||
self.assertEqual(picking.state, 'assigned', "Picking should not be in a draft state.")
|
||
self.assertEqual(len(picking.move_ids), 1, "Picking should have stock move.")
|
||
picking.action_cancel()
|
||
self.assertEqual(picking.move_ids.state, 'cancel', "Stock move should be in a cancel state.")
|
||
self.assertEqual(picking.state, 'cancel', "Picking should be in a cancel state.")
|
||
|
||
|
||
class TestStockUOM(TestStockCommon):
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
super().setUpClass()
|
||
dp = cls.env.ref('product.decimal_product_uom')
|
||
dp.digits = 7
|
||
|
||
def test_pickings_transfer_with_different_uom_and_back_orders(self):
|
||
""" Picking transfer with diffrent unit of meassure. """
|
||
# weight category
|
||
categ_test = self.env['uom.category'].create({'name': 'Bigger than tons'})
|
||
|
||
T_LBS = self.env['uom.uom'].create({
|
||
'name': 'T-LBS',
|
||
'category_id': categ_test.id,
|
||
'uom_type': 'reference',
|
||
'rounding': 0.01
|
||
})
|
||
T_GT = self.env['uom.uom'].create({
|
||
'name': 'T-GT',
|
||
'category_id': categ_test.id,
|
||
'uom_type': 'bigger',
|
||
'rounding': 0.0000001,
|
||
'factor_inv': 2240.00,
|
||
})
|
||
T_TEST = self.env['product.product'].create({
|
||
'name': 'T_TEST',
|
||
'type': 'product',
|
||
'uom_id': T_LBS.id,
|
||
'uom_po_id': T_LBS.id,
|
||
'tracking': 'lot',
|
||
})
|
||
|
||
picking_in = self.env['stock.picking'].create({
|
||
'picking_type_id': self.picking_type_in,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location
|
||
})
|
||
move = self.env['stock.move'].create({
|
||
'name': 'First move with 60 GT',
|
||
'product_id': T_TEST.id,
|
||
'product_uom_qty': 60,
|
||
'product_uom': T_GT.id,
|
||
'picking_id': picking_in.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location
|
||
})
|
||
picking_in.action_confirm()
|
||
|
||
self.assertEqual(move.product_uom_qty, 60.00, 'Wrong T_GT quantity')
|
||
self.assertEqual(move.product_qty, 134400.00, 'Wrong T_LBS quantity')
|
||
|
||
lot = self.env['stock.lot'].create({'name': 'Lot TEST', 'product_id': T_TEST.id, 'company_id': self.env.company.id, })
|
||
self.env['stock.move.line'].create({
|
||
'move_id': move.id,
|
||
'product_id': T_TEST.id,
|
||
'product_uom_id': T_LBS.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.stock_location,
|
||
'qty_done': 42760.00,
|
||
'lot_id': lot.id,
|
||
})
|
||
|
||
picking_in._action_done()
|
||
back_order_in = self.env['stock.picking'].search([('backorder_id', '=', picking_in.id)])
|
||
|
||
self.assertEqual(len(back_order_in), 1.00, 'There should be one back order created')
|
||
self.assertEqual(back_order_in.move_ids.product_qty, 91640.00, 'There should be one back order created')
|
||
|
||
def test_move_product_with_different_uom(self):
|
||
""" Product defined in g with 0.01 rounding
|
||
Decimal Accuracy (DA) 3 digits.
|
||
Quantity on hand: 149.88g
|
||
Picking of 1kg
|
||
kg has 0.0001 rounding
|
||
Due to conversions, we may end up reserving 150g
|
||
(more than the quantity in stock), we check that
|
||
we reserve less than the quantity in stock
|
||
"""
|
||
precision = self.env.ref('product.decimal_product_uom')
|
||
precision.digits = 3
|
||
precision_digits = precision.digits
|
||
|
||
self.uom_kg.rounding = 0.0001
|
||
self.uom_gm.rounding = 0.01
|
||
|
||
product_G = self.env['product.product'].create({
|
||
'name': 'Product G',
|
||
'type': 'product',
|
||
'categ_id': self.env.ref('product.product_category_all').id,
|
||
'uom_id': self.uom_gm.id,
|
||
'uom_po_id': self.uom_gm.id,
|
||
})
|
||
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.env['stock.quant']._update_available_quantity(product_G, stock_location, 149.88)
|
||
self.assertEqual(len(product_G.stock_quant_ids), 1, 'One quant should exist for the product.')
|
||
quant = product_G.stock_quant_ids
|
||
|
||
# transfer 1kg of product_G
|
||
picking = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||
})
|
||
|
||
move = self.env['stock.move'].create({
|
||
'name': 'test_reserve_product_G',
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_id': picking.id,
|
||
'product_id': product_G.id,
|
||
'product_uom': self.uom_kg.id,
|
||
'product_uom_qty': 1,
|
||
})
|
||
|
||
self.assertEqual(move.product_uom.id, self.uom_kg.id)
|
||
self.assertEqual(move.product_uom_qty, 1.0)
|
||
|
||
picking.action_confirm()
|
||
picking.action_assign()
|
||
|
||
self.assertEqual(product_G.uom_id.rounding, 0.01)
|
||
self.assertEqual(move.product_uom.rounding, 0.0001)
|
||
|
||
self.assertEqual(len(picking.move_line_ids), 1, 'One move line should exist for the picking.')
|
||
move_line = picking.move_line_ids
|
||
# check that we do not reserve more (in the same UOM) than the quantity in stock
|
||
self.assertEqual(float_compare(move_line.reserved_qty, quant.quantity, precision_digits=precision_digits), -1, "We do not reserve more (in the same UOM) than the quantity in stock")
|
||
# check that we reserve the same quantity in the ml and the quant
|
||
self.assertTrue(float_is_zero(move_line.reserved_qty - quant.reserved_quantity, precision_digits=precision_digits))
|
||
|
||
def test_update_product_move_line_with_different_uom(self):
|
||
""" Check that when the move line and corresponding
|
||
product have different UOM with possibly conflicting
|
||
precisions, we do not reserve more than the quantity
|
||
in stock. Similar initial configuration as
|
||
test_move_product_with_different_uom.
|
||
"""
|
||
precision = self.env.ref('product.decimal_product_uom')
|
||
precision.digits = 3
|
||
precision_digits = precision.digits
|
||
|
||
self.uom_kg.rounding = 0.0001
|
||
self.uom_gm.rounding = 0.01
|
||
|
||
product_LtDA = self.env['product.product'].create({
|
||
'name': 'Product Less than DA',
|
||
'type': 'product',
|
||
'categ_id': self.env.ref('product.product_category_all').id,
|
||
'uom_id': self.uom_gm.id,
|
||
'uom_po_id': self.uom_gm.id,
|
||
})
|
||
|
||
product_GtDA = self.env['product.product'].create({
|
||
'name': 'Product Greater than DA',
|
||
'type': 'product',
|
||
'categ_id': self.env.ref('product.product_category_all').id,
|
||
'uom_id': self.uom_gm.id,
|
||
'uom_po_id': self.uom_gm.id,
|
||
})
|
||
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
|
||
# quantity in hand converted to kg is not more precise than the DA
|
||
self.env['stock.quant']._update_available_quantity(product_LtDA, stock_location, 149)
|
||
# quantity in hand converted to kg is more precise than the DA
|
||
self.env['stock.quant']._update_available_quantity(product_GtDA, stock_location, 149.88)
|
||
|
||
self.assertEqual(len(product_LtDA.stock_quant_ids), 1, 'One quant should exist for the product.')
|
||
self.assertEqual(len(product_GtDA.stock_quant_ids), 1, 'One quant should exist for the product.')
|
||
quant_LtDA = product_LtDA.stock_quant_ids
|
||
quant_GtDA = product_GtDA.stock_quant_ids
|
||
|
||
# create 2 moves of 1kg
|
||
move_LtDA = self.env['stock.move'].create({
|
||
'name': 'test_reserve_product_LtDA',
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'product_id': product_LtDA.id,
|
||
'product_uom': self.uom_kg.id,
|
||
'product_uom_qty': 1,
|
||
})
|
||
|
||
move_GtDA = self.env['stock.move'].create({
|
||
'name': 'test_reserve_product_GtDA',
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'product_id': product_GtDA.id,
|
||
'product_uom': self.uom_kg.id,
|
||
'product_uom_qty': 1,
|
||
})
|
||
|
||
self.assertEqual(move_LtDA.state, 'draft')
|
||
self.assertEqual(move_GtDA.state, 'draft')
|
||
move_LtDA._action_confirm()
|
||
move_GtDA._action_confirm()
|
||
self.assertEqual(move_LtDA.state, 'confirmed')
|
||
self.assertEqual(move_GtDA.state, 'confirmed')
|
||
# check availability, less than initial demand
|
||
move_LtDA._action_assign()
|
||
move_GtDA._action_assign()
|
||
self.assertEqual(move_LtDA.state, 'partially_available')
|
||
self.assertEqual(move_GtDA.state, 'partially_available')
|
||
# the initial demand is 1kg
|
||
self.assertEqual(move_LtDA.product_uom.id, self.uom_kg.id)
|
||
self.assertEqual(move_GtDA.product_uom.id, self.uom_kg.id)
|
||
self.assertEqual(move_LtDA.product_uom_qty, 1.0)
|
||
self.assertEqual(move_GtDA.product_uom_qty, 1.0)
|
||
# one move line is created
|
||
self.assertEqual(len(move_LtDA.move_line_ids), 1)
|
||
self.assertEqual(len(move_GtDA.move_line_ids), 1)
|
||
|
||
# increase quantity by 0.14988 kg (more precise than DA)
|
||
self.env['stock.quant']._update_available_quantity(product_LtDA, stock_location, 149.88)
|
||
self.env['stock.quant']._update_available_quantity(product_GtDA, stock_location, 149.88)
|
||
|
||
# _update_reserved_quantity is called on a move only in _action_assign
|
||
move_LtDA._action_assign()
|
||
move_GtDA._action_assign()
|
||
|
||
# as the move line for LtDA and its corresponding quant can be
|
||
# in different UOMs, a new move line can be created
|
||
# from _update_reserved_quantity
|
||
move_lines_LtDA = self.env["stock.move.line"].search([
|
||
('product_id', '=', quant_LtDA.product_id.id),
|
||
('location_id', '=', quant_LtDA.location_id.id),
|
||
('lot_id', '=', quant_LtDA.lot_id.id),
|
||
('package_id', '=', quant_LtDA.package_id.id),
|
||
('owner_id', '=', quant_LtDA.owner_id.id),
|
||
('reserved_qty', '!=', 0)
|
||
])
|
||
reserved_on_move_lines_LtDA = sum(move_lines_LtDA.mapped('reserved_qty'))
|
||
|
||
move_lines_GtDA = self.env["stock.move.line"].search([
|
||
('product_id', '=', quant_GtDA.product_id.id),
|
||
('location_id', '=', quant_GtDA.location_id.id),
|
||
('lot_id', '=', quant_GtDA.lot_id.id),
|
||
('package_id', '=', quant_GtDA.package_id.id),
|
||
('owner_id', '=', quant_GtDA.owner_id.id),
|
||
('reserved_qty', '!=', 0)
|
||
])
|
||
reserved_on_move_lines_GtDA = sum(move_lines_GtDA.mapped('reserved_qty'))
|
||
|
||
# check that we do not reserve more (in the same UOM) than the quantity in stock
|
||
self.assertEqual(float_compare(reserved_on_move_lines_LtDA, quant_LtDA.quantity, precision_digits=precision_digits), -1, "We do not reserve more (in the same UOM) than the quantity in stock")
|
||
self.assertEqual(float_compare(reserved_on_move_lines_GtDA, quant_GtDA.quantity, precision_digits=precision_digits), -1, "We do not reserve more (in the same UOM) than the quantity in stock")
|
||
|
||
# check that we reserve the same quantity in the ml and the quant
|
||
self.assertTrue(float_is_zero(reserved_on_move_lines_LtDA - quant_LtDA.reserved_quantity, precision_digits=precision_digits))
|
||
self.assertTrue(float_is_zero(reserved_on_move_lines_GtDA - quant_GtDA.reserved_quantity, precision_digits=precision_digits))
|
||
|
||
|
||
class TestRoutes(TestStockCommon):
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
super().setUpClass()
|
||
cls.product1 = cls.env['product.product'].create({
|
||
'name': 'product a',
|
||
'type': 'product',
|
||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||
})
|
||
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
|
||
cls.partner = cls.env['res.partner'].create({'name': 'Partner'})
|
||
|
||
def _enable_pick_ship(self):
|
||
self.wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
|
||
# create and get back the pick ship route
|
||
self.wh.write({'delivery_steps': 'pick_ship'})
|
||
self.pick_ship_route = self.wh.route_ids.filtered(lambda r: '(pick + ship)' in r.name)
|
||
|
||
def test_pick_ship_1(self):
|
||
""" Enable the pick ship route, force a procurement group on the
|
||
pick. When a second move is added, make sure the `partner_id` and
|
||
`origin` fields are erased.
|
||
"""
|
||
self._enable_pick_ship()
|
||
|
||
# create a procurement group and set in on the pick stock rule
|
||
procurement_group0 = self.env['procurement.group'].create({})
|
||
pick_rule = self.pick_ship_route.rule_ids.filtered(lambda rule: 'Stock → Output' in rule.name)
|
||
push_rule = self.pick_ship_route.rule_ids - pick_rule
|
||
pick_rule.write({
|
||
'group_propagation_option': 'fixed',
|
||
'group_id': procurement_group0.id,
|
||
})
|
||
|
||
ship_location = pick_rule.location_dest_id
|
||
customer_location = push_rule.location_dest_id
|
||
partners = self.env['res.partner'].search([], limit=2)
|
||
partner0 = partners[0]
|
||
partner1 = partners[1]
|
||
procurement_group1 = self.env['procurement.group'].create({'partner_id': partner0.id})
|
||
procurement_group2 = self.env['procurement.group'].create({'partner_id': partner1.id})
|
||
|
||
move1 = self.env['stock.move'].create({
|
||
'name': 'first out move',
|
||
'procure_method': 'make_to_order',
|
||
'location_id': ship_location.id,
|
||
'location_dest_id': customer_location.id,
|
||
'product_id': self.product1.id,
|
||
'product_uom': self.uom_unit.id,
|
||
'product_uom_qty': 1.0,
|
||
'warehouse_id': self.wh.id,
|
||
'group_id': procurement_group1.id,
|
||
'origin': 'origin1',
|
||
})
|
||
|
||
move2 = self.env['stock.move'].create({
|
||
'name': 'second out move',
|
||
'procure_method': 'make_to_order',
|
||
'location_id': ship_location.id,
|
||
'location_dest_id': customer_location.id,
|
||
'product_id': self.product1.id,
|
||
'product_uom': self.uom_unit.id,
|
||
'product_uom_qty': 1.0,
|
||
'warehouse_id': self.wh.id,
|
||
'group_id': procurement_group2.id,
|
||
'origin': 'origin2',
|
||
})
|
||
|
||
# first out move, the "pick" picking should have a partner and an origin
|
||
move1._action_confirm()
|
||
picking_pick = move1.move_orig_ids.picking_id
|
||
self.assertEqual(picking_pick.partner_id.id, procurement_group1.partner_id.id)
|
||
self.assertEqual(picking_pick.origin, move1.group_id.name)
|
||
|
||
# second out move, the "pick" picking should have lost its partner and origin
|
||
move2._action_confirm()
|
||
self.assertEqual(picking_pick.partner_id.id, False)
|
||
self.assertEqual(picking_pick.origin, False)
|
||
|
||
def test_replenish_pick_ship_1(self):
|
||
""" Creates 2 warehouses and make a replenish using one warehouse
|
||
to ressuply the other one, Then check if the quantity and the product are matching
|
||
"""
|
||
self.product_uom_qty = 42
|
||
|
||
warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
warehouse_2 = self.env['stock.warehouse'].create({
|
||
'name': 'Small Warehouse',
|
||
'code': 'SWH'
|
||
})
|
||
warehouse_1.write({
|
||
'resupply_wh_ids': [(6, 0, [warehouse_2.id])]
|
||
})
|
||
resupply_route = self.env['stock.route'].search([('supplier_wh_id', '=', warehouse_2.id), ('supplied_wh_id', '=', warehouse_1.id)])
|
||
self.assertTrue(resupply_route, "Ressuply route not found")
|
||
self.product1.write({'route_ids': [(4, resupply_route.id), (4, self.env.ref('stock.route_warehouse0_mto').id)]})
|
||
self.wh = warehouse_1
|
||
|
||
replenish_wizard = self.env['product.replenish'].create({
|
||
'product_id': self.product1.id,
|
||
'product_tmpl_id': self.product1.product_tmpl_id.id,
|
||
'product_uom_id': self.uom_unit.id,
|
||
'quantity': self.product_uom_qty,
|
||
'warehouse_id': self.wh.id,
|
||
})
|
||
|
||
replenish_wizard.launch_replenishment()
|
||
last_picking_id = self.env['stock.picking'].search([('origin', '=', 'Manual Replenishment')])[-1]
|
||
self.assertTrue(last_picking_id, 'Picking not found')
|
||
move_line = last_picking_id.move_ids.search([('product_id', '=', self.product1.id)])
|
||
self.assertTrue(move_line,'The product is not in the picking')
|
||
self.assertEqual(move_line[0].product_uom_qty, self.product_uom_qty, 'Quantities does not match')
|
||
self.assertEqual(move_line[1].product_uom_qty, self.product_uom_qty, 'Quantities does not match')
|
||
|
||
def test_push_rule_on_move_1(self):
|
||
""" Create a route with a push rule, force it on a move, check that it is applied.
|
||
"""
|
||
self._enable_pick_ship()
|
||
stock_location = self.env.ref('stock.stock_location_stock')
|
||
|
||
push_location = self.env['stock.location'].create({
|
||
'location_id': stock_location.location_id.id,
|
||
'name': 'push location',
|
||
})
|
||
|
||
# TODO: maybe add a new type on the "applicable on" fields?
|
||
route = self.env['stock.route'].create({
|
||
'name': 'new route',
|
||
'rule_ids': [(0, False, {
|
||
'name': 'create a move to push location',
|
||
'location_src_id': stock_location.id,
|
||
'location_dest_id': push_location.id,
|
||
'company_id': self.env.company.id,
|
||
'action': 'push',
|
||
'auto': 'manual',
|
||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||
})],
|
||
})
|
||
move1 = self.env['stock.move'].create({
|
||
'name': 'move with a route',
|
||
'location_id': stock_location.id,
|
||
'location_dest_id': stock_location.id,
|
||
'product_id': self.product1.id,
|
||
'product_uom': self.uom_unit.id,
|
||
'product_uom_qty': 1.0,
|
||
'route_ids': [(4, route.id)]
|
||
})
|
||
move1._action_confirm()
|
||
|
||
pushed_move = move1.move_dest_ids
|
||
self.assertEqual(pushed_move.location_dest_id.id, push_location.id)
|
||
|
||
def test_location_dest_update(self):
|
||
""" Check the location dest of a stock move changed by a push rule
|
||
with auto field set to transparent is done correctly. The stock_move
|
||
is create with the move line directly to pass into action_confirm() via
|
||
action_done(). """
|
||
self.wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
new_loc = self.env['stock.location'].create({
|
||
'name': 'New_location',
|
||
'usage': 'internal',
|
||
'location_id': self.env.ref('stock.stock_location_locations').id,
|
||
})
|
||
picking_type = self.env['stock.picking.type'].create({
|
||
'name': 'new_picking_type',
|
||
'code': 'internal',
|
||
'sequence_code': 'NPT',
|
||
'default_location_src_id': self.env.ref('stock.stock_location_stock').id,
|
||
'default_location_dest_id': new_loc.id,
|
||
'warehouse_id': self.wh.id,
|
||
})
|
||
route = self.env['stock.route'].create({
|
||
'name': 'new route',
|
||
'rule_ids': [(0, False, {
|
||
'name': 'create a move to push location',
|
||
'location_src_id': self.env.ref('stock.stock_location_stock').id,
|
||
'location_dest_id': new_loc.id,
|
||
'company_id': self.env.company.id,
|
||
'action': 'push',
|
||
'auto': 'transparent',
|
||
'picking_type_id': picking_type.id,
|
||
})],
|
||
})
|
||
product = self.env['product.product'].create({
|
||
'name': 'new_product',
|
||
'type': 'product',
|
||
'route_ids': [(4, route.id)]
|
||
})
|
||
move1 = self.env['stock.move'].create({
|
||
'name': 'move with a route',
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.env.ref('stock.stock_location_stock').id,
|
||
'product_id': product.id,
|
||
'product_uom_qty': 1.0,
|
||
'product_uom': self.uom_unit.id,
|
||
'move_line_ids': [(0, 0, {
|
||
'product_id': product.id,
|
||
'product_uom_id': self.uom_unit.id,
|
||
'location_id': self.supplier_location,
|
||
'location_dest_id': self.env.ref('stock.stock_location_stock').id,
|
||
'qty_done': 1.00,
|
||
})],
|
||
})
|
||
move1._action_done()
|
||
self.assertEqual(move1.location_dest_id, new_loc)
|
||
positive_quant = product.stock_quant_ids.filtered(lambda q: q.quantity > 0)
|
||
self.assertEqual(positive_quant.location_id, new_loc)
|
||
|
||
def test_mtso_mto(self):
|
||
""" Run a procurement for 5 products when there are only 4 in stock then
|
||
check that MTO is applied on the moves when the rule is set to 'mts_else_mto'
|
||
"""
|
||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
warehouse.delivery_steps = 'pick_pack_ship'
|
||
partner_demo_customer = self.partner
|
||
final_location = partner_demo_customer.property_stock_customer
|
||
product_a = self.env['product.product'].create({
|
||
'name': 'ProductA',
|
||
'type': 'product',
|
||
})
|
||
|
||
self.env['stock.quant']._update_available_quantity(product_a, warehouse.wh_output_stock_loc_id, 4.0)
|
||
|
||
# We set quantities in the stock location to avoid warnings
|
||
# triggered by '_onchange_product_id_check_availability'
|
||
self.env['stock.quant']._update_available_quantity(product_a, warehouse.lot_stock_id, 4.0)
|
||
|
||
# We alter one rule and we set it to 'mts_else_mto'
|
||
values = {'warehouse_id': warehouse}
|
||
rule = self.env['procurement.group']._get_rule(product_a, final_location, values)
|
||
rule.procure_method = 'mts_else_mto'
|
||
|
||
pg = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mto'})
|
||
|
||
self.env['procurement.group'].run([
|
||
pg.Procurement(
|
||
product_a,
|
||
5.0,
|
||
product_a.uom_id,
|
||
final_location,
|
||
'test_mtso_mto',
|
||
'test_mtso_mto',
|
||
warehouse.company_id,
|
||
{
|
||
'warehouse_id': warehouse,
|
||
'group_id': pg
|
||
}
|
||
)
|
||
])
|
||
|
||
qty_available = self.env['stock.quant']._get_available_quantity(product_a, warehouse.wh_output_stock_loc_id)
|
||
|
||
# 3 pickings should be created.
|
||
picking_ids = self.env['stock.picking'].search([('group_id', '=', pg.id)])
|
||
self.assertEqual(len(picking_ids), 3)
|
||
for picking in picking_ids:
|
||
# Only the picking from Stock to Pack should be MTS
|
||
if picking.location_id == warehouse.lot_stock_id:
|
||
self.assertEqual(picking.move_ids.procure_method, 'make_to_stock')
|
||
else:
|
||
self.assertEqual(picking.move_ids.procure_method, 'make_to_order')
|
||
|
||
self.assertEqual(len(picking.move_ids), 1)
|
||
self.assertEqual(picking.move_ids.product_uom_qty, 5, 'The quantity of the move should be the same as on the SO')
|
||
self.assertEqual(qty_available, 4, 'The 4 products should still be available')
|
||
|
||
def test_mtso_mts(self):
|
||
""" Run a procurement for 4 products when there are 4 in stock then
|
||
check that MTS is applied on the moves when the rule is set to 'mts_else_mto'
|
||
"""
|
||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
warehouse.delivery_steps = 'pick_pack_ship'
|
||
partner_demo_customer = self.partner
|
||
final_location = partner_demo_customer.property_stock_customer
|
||
product_a = self.env['product.product'].create({
|
||
'name': 'ProductA',
|
||
'type': 'product',
|
||
})
|
||
|
||
self.env['stock.quant']._update_available_quantity(product_a, warehouse.wh_output_stock_loc_id, 4.0)
|
||
|
||
# We alter one rule and we set it to 'mts_else_mto'
|
||
values = {'warehouse_id': warehouse}
|
||
rule = self.env['procurement.group']._get_rule(product_a, final_location, values)
|
||
rule.procure_method = 'mts_else_mto'
|
||
|
||
pg = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mts'})
|
||
|
||
self.env['procurement.group'].run([
|
||
pg.Procurement(
|
||
product_a,
|
||
4.0,
|
||
product_a.uom_id,
|
||
final_location,
|
||
'test_mtso_mts',
|
||
'test_mtso_mts',
|
||
warehouse.company_id,
|
||
{
|
||
'warehouse_id': warehouse,
|
||
'group_id': pg
|
||
}
|
||
)
|
||
])
|
||
|
||
# A picking should be created with its move having MTS as procure method.
|
||
picking_ids = self.env['stock.picking'].search([('group_id', '=', pg.id)])
|
||
self.assertEqual(len(picking_ids), 1)
|
||
picking = picking_ids
|
||
self.assertEqual(picking.move_ids.procure_method, 'make_to_stock')
|
||
self.assertEqual(len(picking.move_ids), 1)
|
||
self.assertEqual(picking.move_ids.product_uom_qty, 4)
|
||
|
||
def test_mtso_multi_pg(self):
|
||
""" Run 3 procurements for 2 products at the same times when there are 4 in stock then
|
||
check that MTS is applied on the moves when the rule is set to 'mts_else_mto'
|
||
"""
|
||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
warehouse.delivery_steps = 'pick_pack_ship'
|
||
partner_demo_customer = self.partner
|
||
final_location = partner_demo_customer.property_stock_customer
|
||
product_a = self.env['product.product'].create({
|
||
'name': 'ProductA',
|
||
'type': 'product',
|
||
})
|
||
|
||
self.env['stock.quant']._update_available_quantity(product_a, warehouse.wh_output_stock_loc_id, 4.0)
|
||
|
||
# We alter one rule and we set it to 'mts_else_mto'
|
||
values = {'warehouse_id': warehouse}
|
||
rule = self.env['procurement.group']._get_rule(product_a, final_location, values)
|
||
rule.procure_method = 'mts_else_mto'
|
||
|
||
pg1 = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mts-1'})
|
||
pg2 = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mts-2'})
|
||
pg3 = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mts-3'})
|
||
|
||
self.env['procurement.group'].run([
|
||
pg1.Procurement(
|
||
product_a,
|
||
2.0,
|
||
product_a.uom_id,
|
||
final_location,
|
||
'test_mtso_mts_1',
|
||
'test_mtso_mts_1',
|
||
warehouse.company_id,
|
||
{
|
||
'warehouse_id': warehouse,
|
||
'group_id': pg1
|
||
}
|
||
),
|
||
pg2.Procurement(
|
||
product_a,
|
||
2.0,
|
||
product_a.uom_id,
|
||
final_location,
|
||
'test_mtso_mts_2',
|
||
'test_mtso_mts_2',
|
||
warehouse.company_id,
|
||
{
|
||
'warehouse_id': warehouse,
|
||
'group_id': pg2
|
||
}
|
||
),
|
||
pg3.Procurement(
|
||
product_a,
|
||
2.0,
|
||
product_a.uom_id,
|
||
final_location,
|
||
'test_mtso_mts_3',
|
||
'test_mtso_mts_3',
|
||
warehouse.company_id,
|
||
{
|
||
'warehouse_id': warehouse,
|
||
'group_id': pg3
|
||
}
|
||
)
|
||
])
|
||
|
||
pickings_pg1 = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
|
||
pickings_pg2 = self.env['stock.picking'].search([('group_id', '=', pg2.id)])
|
||
pickings_pg3 = self.env['stock.picking'].search([('group_id', '=', pg3.id)])
|
||
|
||
# The 2 first procurements should have create only 1 picking since enough quantities
|
||
# are left in the delivery location
|
||
self.assertEqual(len(pickings_pg1), 1)
|
||
self.assertEqual(len(pickings_pg2), 1)
|
||
self.assertEqual(pickings_pg1.move_ids.procure_method, 'make_to_stock')
|
||
self.assertEqual(pickings_pg2.move_ids.procure_method, 'make_to_stock')
|
||
|
||
# The last one should have 3 pickings as there's nothing left in the delivery location
|
||
self.assertEqual(len(pickings_pg3), 3)
|
||
for picking in pickings_pg3:
|
||
# Only the picking from Stock to Pack should be MTS
|
||
if picking.location_id == warehouse.lot_stock_id:
|
||
self.assertEqual(picking.move_ids.procure_method, 'make_to_stock')
|
||
else:
|
||
self.assertEqual(picking.move_ids.procure_method, 'make_to_order')
|
||
|
||
# All the moves should be should have the same quantity as it is on each procurements
|
||
self.assertEqual(len(picking.move_ids), 1)
|
||
self.assertEqual(picking.move_ids.product_uom_qty, 2)
|
||
|
||
def test_mtso_mto_adjust_01(self):
|
||
""" Run '_adjust_procure_method' for products A & B:
|
||
- Product A has 5.0 available
|
||
- Product B has 3.0 available
|
||
Stock moves (SM) are created for 4.0 units
|
||
After '_adjust_procure_method':
|
||
- SM for A is 'make_to_stock'
|
||
- SM for B is 'make_to_order'
|
||
"""
|
||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
final_location = self.partner.property_stock_customer
|
||
product_A = self.env['product.product'].create({
|
||
'name': 'Product A',
|
||
'type': 'product',
|
||
})
|
||
product_B = self.env['product.product'].create({
|
||
'name': 'Product B',
|
||
'type': 'product',
|
||
})
|
||
|
||
# We alter one rule and we set it to 'mts_else_mto'
|
||
rule = self.env['procurement.group']._get_rule(product_A, final_location, {'warehouse_id': warehouse})
|
||
rule.procure_method = 'mts_else_mto'
|
||
|
||
self.env['stock.quant']._update_available_quantity(product_A, warehouse.lot_stock_id, 5.0)
|
||
self.env['stock.quant']._update_available_quantity(product_B, warehouse.lot_stock_id, 3.0)
|
||
|
||
move_tmpl = {
|
||
'name': 'Product',
|
||
'product_uom': self.uom_unit.id,
|
||
'product_uom_qty': 4.0,
|
||
'location_id': warehouse.lot_stock_id.id,
|
||
'location_dest_id': self.partner.property_stock_customer.id,
|
||
'warehouse_id': warehouse.id,
|
||
}
|
||
move_A_vals = dict(move_tmpl)
|
||
move_A_vals.update({
|
||
'product_id': product_A.id,
|
||
})
|
||
move_A = self.env['stock.move'].create(move_A_vals)
|
||
move_B_vals = dict(move_tmpl)
|
||
move_B_vals.update({
|
||
'product_id': product_B.id,
|
||
})
|
||
move_B = self.env['stock.move'].create(move_B_vals)
|
||
moves = move_A + move_B
|
||
|
||
self.assertEqual(move_A.procure_method, 'make_to_stock', 'Move A should be "make_to_stock"')
|
||
self.assertEqual(move_B.procure_method, 'make_to_stock', 'Move A should be "make_to_order"')
|
||
moves._adjust_procure_method()
|
||
self.assertEqual(move_A.procure_method, 'make_to_stock', 'Move A should be "make_to_stock"')
|
||
self.assertEqual(move_B.procure_method, 'make_to_order', 'Move A should be "make_to_order"')
|
||
|
||
def test_mtso_mto_adjust_02(self):
|
||
""" Run '_adjust_procure_method' for products A & B:
|
||
- Product A has 5.0 available
|
||
- Product B has 3.0 available
|
||
Stock moves (SM) are created for 2.0 + 2.0 units
|
||
After '_adjust_procure_method':
|
||
- SM for A is 'make_to_stock'
|
||
- SM for B is 'make_to_stock' and 'make_to_order'
|
||
"""
|
||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
final_location = self.partner.property_stock_customer
|
||
product_A = self.env['product.product'].create({
|
||
'name': 'Product A',
|
||
'type': 'product',
|
||
})
|
||
product_B = self.env['product.product'].create({
|
||
'name': 'Product B',
|
||
'type': 'product',
|
||
})
|
||
|
||
# We alter one rule and we set it to 'mts_else_mto'
|
||
rule = self.env['procurement.group']._get_rule(product_A, final_location, {'warehouse_id': warehouse})
|
||
rule.procure_method = 'mts_else_mto'
|
||
|
||
self.env['stock.quant']._update_available_quantity(product_A, warehouse.lot_stock_id, 5.0)
|
||
self.env['stock.quant']._update_available_quantity(product_B, warehouse.lot_stock_id, 3.0)
|
||
|
||
move_tmpl = {
|
||
'name': 'Product',
|
||
'product_uom': self.uom_unit.id,
|
||
'product_uom_qty': 2.0,
|
||
'location_id': warehouse.lot_stock_id.id,
|
||
'location_dest_id': self.partner.property_stock_customer.id,
|
||
'warehouse_id': warehouse.id,
|
||
}
|
||
move_A1_vals = dict(move_tmpl)
|
||
move_A1_vals.update({
|
||
'product_id': product_A.id,
|
||
})
|
||
move_A1 = self.env['stock.move'].create(move_A1_vals)
|
||
move_A2_vals = dict(move_tmpl)
|
||
move_A2_vals.update({
|
||
'product_id': product_A.id,
|
||
})
|
||
move_A2 = self.env['stock.move'].create(move_A2_vals)
|
||
move_B1_vals = dict(move_tmpl)
|
||
move_B1_vals.update({
|
||
'product_id': product_B.id,
|
||
})
|
||
move_B1 = self.env['stock.move'].create(move_B1_vals)
|
||
move_B2_vals = dict(move_tmpl)
|
||
move_B2_vals.update({
|
||
'product_id': product_B.id,
|
||
})
|
||
move_B2 = self.env['stock.move'].create(move_B2_vals)
|
||
moves = move_A1 + move_A2 + move_B1 + move_B2
|
||
|
||
self.assertEqual(move_A1.procure_method, 'make_to_stock', 'Move A1 should be "make_to_stock"')
|
||
self.assertEqual(move_A2.procure_method, 'make_to_stock', 'Move A2 should be "make_to_stock"')
|
||
self.assertEqual(move_B1.procure_method, 'make_to_stock', 'Move B1 should be "make_to_stock"')
|
||
self.assertEqual(move_B2.procure_method, 'make_to_stock', 'Move B2 should be "make_to_stock"')
|
||
moves._adjust_procure_method()
|
||
self.assertEqual(move_A1.procure_method, 'make_to_stock', 'Move A1 should be "make_to_stock"')
|
||
self.assertEqual(move_A2.procure_method, 'make_to_stock', 'Move A2 should be "make_to_stock"')
|
||
self.assertEqual(move_B1.procure_method, 'make_to_stock', 'Move B1 should be "make_to_stock"')
|
||
self.assertEqual(move_B2.procure_method, 'make_to_order', 'Move B2 should be "make_to_order"')
|
||
|
||
def test_mtso_mto_adjust_03(self):
|
||
""" Run '_adjust_procure_method' for products A with 4.0 available
|
||
2 Stock moves (SM) are created:
|
||
- SM1 for 5.0 Units
|
||
- SM2 for 3.0 Units
|
||
SM1 is confirmed, so 'virtual_available' is -1.0.
|
||
SM1 should become 'make_to_order'
|
||
SM2 should remain 'make_to_stock'
|
||
"""
|
||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
final_location = self.partner.property_stock_customer
|
||
product_A = self.env['product.product'].create({
|
||
'name': 'Product A',
|
||
'type': 'product',
|
||
})
|
||
|
||
# We alter one rule and we set it to 'mts_else_mto'
|
||
rule = self.env['procurement.group']._get_rule(product_A, final_location, {'warehouse_id': warehouse})
|
||
rule.procure_method = 'mts_else_mto'
|
||
|
||
self.env['stock.quant']._update_available_quantity(product_A, warehouse.lot_stock_id, 4.0)
|
||
|
||
move_tmpl = {
|
||
'name': 'Product',
|
||
'product_id': product_A.id,
|
||
'product_uom': self.uom_unit.id,
|
||
'location_id': warehouse.lot_stock_id.id,
|
||
'location_dest_id': self.partner.property_stock_customer.id,
|
||
'warehouse_id': warehouse.id,
|
||
}
|
||
move_A1_vals = dict(move_tmpl)
|
||
move_A1_vals.update({
|
||
'product_uom_qty': 5.0,
|
||
})
|
||
move_A1 = self.env['stock.move'].create(move_A1_vals)
|
||
move_A2_vals = dict(move_tmpl)
|
||
move_A2_vals.update({
|
||
'product_uom_qty': 3.0,
|
||
})
|
||
move_A2 = self.env['stock.move'].create(move_A2_vals)
|
||
moves = move_A1 + move_A2
|
||
|
||
self.assertEqual(move_A1.procure_method, 'make_to_stock', 'Move A1 should be "make_to_stock"')
|
||
self.assertEqual(move_A2.procure_method, 'make_to_stock', 'Move A2 should be "make_to_stock"')
|
||
move_A1._action_confirm()
|
||
moves._adjust_procure_method()
|
||
self.assertEqual(move_A1.procure_method, 'make_to_order', 'Move A should be "make_to_stock"')
|
||
self.assertEqual(move_A2.procure_method, 'make_to_stock', 'Move A should be "make_to_order"')
|
||
|
||
def test_delay_alert_3(self):
|
||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||
warehouse.delivery_steps = 'pick_pack_ship'
|
||
partner_demo_customer = self.partner
|
||
final_location = partner_demo_customer.property_stock_customer
|
||
product_a = self.env['product.product'].create({
|
||
'name': 'ProductA',
|
||
'type': 'product',
|
||
})
|
||
pg = self.env['procurement.group'].create({'name': 'Test-delay_alert_3'})
|
||
self.env['procurement.group'].run([
|
||
pg.Procurement(
|
||
product_a,
|
||
4.0,
|
||
product_a.uom_id,
|
||
final_location,
|
||
'delay',
|
||
'delay',
|
||
warehouse.company_id,
|
||
{
|
||
'warehouse_id': warehouse,
|
||
'group_id': pg
|
||
}
|
||
),
|
||
])
|
||
ship, pack, pick = self.env['stock.move'].search([('product_id', '=', product_a.id)])
|
||
|
||
# by default they all the same `date`
|
||
self.assertEqual(set((ship + pack + pick).mapped('date')), {pick.date})
|
||
|
||
# pick - pack - ship
|
||
ship.date += timedelta(days=2)
|
||
pack.date += timedelta(days=1)
|
||
self.assertFalse(pick.delay_alert_date)
|
||
self.assertFalse(pack.delay_alert_date)
|
||
self.assertFalse(ship.delay_alert_date)
|
||
|
||
# move the pack after the ship
|
||
# pick - ship - pack
|
||
pack.date += timedelta(days=2)
|
||
self.assertFalse(pick.delay_alert_date)
|
||
self.assertFalse(pack.delay_alert_date)
|
||
self.assertTrue(ship.delay_alert_date)
|
||
self.assertAlmostEqual(ship.delay_alert_date, pack.date)
|
||
|
||
# restore the pack before the ship
|
||
# pick - pack - ship
|
||
pack.date -= timedelta(days=2)
|
||
self.assertFalse(pick.delay_alert_date)
|
||
self.assertFalse(pack.delay_alert_date)
|
||
self.assertFalse(ship.delay_alert_date)
|
||
|
||
# move the pick after the pack
|
||
# pack - ship - pick
|
||
pick.date += timedelta(days=3)
|
||
self.assertFalse(pick.delay_alert_date)
|
||
self.assertTrue(pack.delay_alert_date)
|
||
self.assertFalse(ship.delay_alert_date)
|
||
self.assertAlmostEqual(pack.delay_alert_date, pick.date)
|
||
|
||
# move the ship before the pack
|
||
# ship - pack - pick
|
||
ship.date -= timedelta(days=2)
|
||
self.assertFalse(pick.delay_alert_date)
|
||
self.assertTrue(pack.delay_alert_date)
|
||
self.assertTrue(ship.delay_alert_date)
|
||
self.assertAlmostEqual(pack.delay_alert_date, pick.date)
|
||
self.assertAlmostEqual(ship.delay_alert_date, pack.date)
|
||
|
||
# move the pack at the end
|
||
# ship - pick - pack
|
||
pack.date = pick.date + timedelta(days=2)
|
||
self.assertFalse(pick.delay_alert_date)
|
||
self.assertFalse(pack.delay_alert_date)
|
||
self.assertTrue(ship.delay_alert_date)
|
||
self.assertAlmostEqual(ship.delay_alert_date, pack.date)
|
||
|
||
# fix the ship
|
||
ship.date = pack.date + timedelta(days=2)
|
||
self.assertFalse(pick.delay_alert_date)
|
||
self.assertFalse(pack.delay_alert_date)
|
||
self.assertFalse(ship.delay_alert_date)
|
||
|
||
def test_packaging_route(self):
|
||
"""Create a route for product and another route for its packaging. Create
|
||
a move of this product with this packaging. Check packaging route has
|
||
priority over product route.
|
||
"""
|
||
stock_location = self.env.ref('stock.stock_location_stock')
|
||
|
||
push_location_1 = self.env['stock.location'].create({
|
||
'location_id': stock_location.location_id.id,
|
||
'name': 'push location 1',
|
||
})
|
||
|
||
push_location_2 = self.env['stock.location'].create({
|
||
'location_id': stock_location.location_id.id,
|
||
'name': 'push location 2',
|
||
})
|
||
|
||
route_on_product = self.env['stock.route'].create({
|
||
'name': 'route on product',
|
||
'rule_ids': [(0, False, {
|
||
'name': 'create a move to push location 1',
|
||
'location_src_id': stock_location.id,
|
||
'location_dest_id': push_location_1.id,
|
||
'company_id': self.env.company.id,
|
||
'action': 'push',
|
||
'auto': 'manual',
|
||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||
})],
|
||
})
|
||
|
||
route_on_packaging = self.env['stock.route'].create({
|
||
'name': 'route on packaging',
|
||
'packaging_selectable': True,
|
||
'rule_ids': [(0, False, {
|
||
'name': 'create a move to push location 2',
|
||
'location_src_id': stock_location.id,
|
||
'location_dest_id': push_location_2.id,
|
||
'company_id': self.env.company.id,
|
||
'action': 'push',
|
||
'auto': 'manual',
|
||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||
})],
|
||
})
|
||
|
||
product = self.env['product.product'].create({
|
||
'name': 'Product with packaging',
|
||
'type': 'product',
|
||
'route_ids': [(4, route_on_product.id, 0)]
|
||
})
|
||
|
||
packaging = self.env['product.packaging'].create({
|
||
'name': 'box',
|
||
'product_id': product.id,
|
||
'route_ids': [(4, route_on_packaging.id, 0)]
|
||
})
|
||
|
||
|
||
move1 = self.env['stock.move'].create({
|
||
'name': 'move with a route',
|
||
'location_id': stock_location.id,
|
||
'location_dest_id': stock_location.id,
|
||
'product_id': product.id,
|
||
'product_packaging_id': packaging.id,
|
||
'product_uom': self.uom_unit.id,
|
||
'product_uom_qty': 1.0,
|
||
})
|
||
move1._action_confirm()
|
||
|
||
pushed_move = move1.move_dest_ids
|
||
self.assertEqual(pushed_move.location_dest_id.id, push_location_2.id)
|
||
|
||
|
||
class TestAutoAssign(TestStockCommon):
|
||
def create_pick_ship(self):
|
||
picking_client = self.env['stock.picking'].create({
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
|
||
dest = self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 10,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': picking_client.id,
|
||
'location_id': self.pack_location,
|
||
'location_dest_id': self.customer_location,
|
||
'state': 'waiting',
|
||
'procure_method': 'make_to_order',
|
||
})
|
||
|
||
picking_pick = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.pack_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 10,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': picking_pick.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.pack_location,
|
||
'move_dest_ids': [(4, dest.id)],
|
||
'state': 'confirmed',
|
||
})
|
||
return picking_pick, picking_client
|
||
|
||
def test_auto_assign_0(self):
|
||
"""Create a outgoing MTS move without enough products in stock, then
|
||
validate a incoming move to check if the outgoing move is automatically
|
||
assigned.
|
||
"""
|
||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.env['stock.picking.type'].browse(self.picking_type_out).reservation_method = 'at_confirm'
|
||
|
||
# create customer picking and move
|
||
customer_picking = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
customer_move = self.env['stock.move'].create({
|
||
'name': 'customer move',
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'product_id': self.productA.id,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'product_uom_qty': 10.0,
|
||
'picking_id': customer_picking.id,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
customer_picking.action_confirm()
|
||
customer_picking.action_assign()
|
||
self.assertEqual(customer_move.state, 'confirmed')
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 0)
|
||
|
||
# create supplier picking and move
|
||
supplier_picking = self.env['stock.picking'].create({
|
||
'location_id': self.customer_location,
|
||
'location_dest_id': self.stock_location,
|
||
'picking_type_id': self.picking_type_in,
|
||
})
|
||
supplier_move = self.env['stock.move'].create({
|
||
'name': 'test_transit_1',
|
||
'location_id': self.customer_location,
|
||
'location_dest_id': self.stock_location,
|
||
'product_id': self.productA.id,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'product_uom_qty': 10.0,
|
||
'picking_id': supplier_picking.id,
|
||
})
|
||
customer_picking.action_confirm()
|
||
customer_picking.action_assign()
|
||
supplier_move.quantity_done = 10
|
||
supplier_picking._action_done()
|
||
|
||
# customer move should be automatically assigned and no more available product in stock
|
||
self.assertEqual(customer_move.state, 'assigned')
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 0)
|
||
|
||
def test_auto_assign_1(self):
|
||
"""Create a outgoing MTO move without enough products, then validate a
|
||
move to make it available to check if the outgoing move is not
|
||
automatically assigned.
|
||
"""
|
||
picking_pick, picking_client = self.create_pick_ship()
|
||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.env['stock.picking.type'].browse(self.picking_type_out).reservation_method = 'at_confirm'
|
||
|
||
# make some stock
|
||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
|
||
|
||
# create another move to make product available in pack_location
|
||
picking_pick_2 = self.env['stock.picking'].create({
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.pack_location,
|
||
'picking_type_id': self.picking_type_out,
|
||
})
|
||
self.MoveObj.create({
|
||
'name': self.productA.name,
|
||
'product_id': self.productA.id,
|
||
'product_uom_qty': 10,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'picking_id': picking_pick_2.id,
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.pack_location,
|
||
'state': 'confirmed',
|
||
})
|
||
picking_pick_2.action_assign()
|
||
picking_pick_2.move_ids[0].move_line_ids[0].qty_done = 10.0
|
||
picking_pick_2._action_done()
|
||
|
||
self.assertEqual(picking_client.state, 'waiting', "MTO moves can't be automatically assigned.")
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 10.0)
|
||
|
||
def test_auto_assign_reservation_method(self):
|
||
"""Test different stock.picking.type reservation methods by:
|
||
1. Create multiple delivery picking types with different reservation methods
|
||
2. Create/confirm outgoing pickings for each of these picking types for a product not in stock
|
||
3. Create/do an incoming picking that fulfills all of the outgoing pickings
|
||
4. Check that only the correct outgoing pickings are auto_assigned
|
||
5. Additionally check that auto-assignment at confirmation correctly works when products are in stock
|
||
Note, default reservation method is expected to be covered by other tests.
|
||
Also check reservation_dates are as expected
|
||
"""
|
||
|
||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 0)
|
||
picking_type_out1 = self.env['stock.picking.type'].browse(self.picking_type_out).copy()
|
||
picking_type_out2 = picking_type_out1.copy()
|
||
picking_type_out3 = picking_type_out1.copy()
|
||
picking_type_out4 = picking_type_out1.copy()
|
||
picking_type_out1.reservation_method = 'manual'
|
||
picking_type_out2.reservation_method = 'by_date'
|
||
picking_type_out2.reservation_days_before = '1'
|
||
picking_type_out3.reservation_method = 'by_date'
|
||
picking_type_out3.reservation_days_before = '10'
|
||
picking_type_out4.reservation_method = 'at_confirm'
|
||
|
||
# 'manual' assign picking => should never auto-assign
|
||
customer_picking1 = self.env['stock.picking'].create({
|
||
'name': "Delivery 1",
|
||
'location_id': self.stock_location,
|
||
'location_dest_id': self.customer_location,
|
||
'picking_type_id': picking_type_out1.id,
|
||
})
|
||
|
||
# 'by_date' picking w/ 1 day before scheduled date auto-assign setting, set to 5 days in advance => shouldn't auto-assign
|
||
customer_picking2 = customer_picking1.copy({'name': "Delivery 2",
|
||
'picking_type_id': picking_type_out2.id,
|
||
'scheduled_date': customer_picking1.scheduled_date + timedelta(days=5)})
|
||
# 'by_date' picking w/ 10 days before scheduled date auto-assign setting, set to 5 days in advance => should auto-assign
|
||
customer_picking3 = customer_picking2.copy({'name': "Delivery 3", 'picking_type_id': picking_type_out3.id})
|
||
customer_picking4 = customer_picking3.copy({'name': "Delivery 4", 'picking_type_id': picking_type_out3.id})
|
||
# 'at_confirm' picking
|
||
customer_picking5 = customer_picking1.copy({'name': "Delivery 5", 'picking_type_id': picking_type_out4.id})
|
||
|
||
# create their associated moves (needs to be in form view so compute functions properly trigger)
|
||
customer_picking1 = Form(customer_picking1)
|
||
with customer_picking1.move_ids_without_package.new() as move:
|
||
move.product_id = self.productA
|
||
move.product_uom_qty = 10
|
||
customer_picking1 = customer_picking1.save()
|
||
|
||
customer_picking2 = Form(customer_picking2)
|
||
with customer_picking2.move_ids_without_package.new() as move:
|
||
move.product_id = self.productA
|
||
move.product_uom_qty = 10
|
||
customer_picking2 = customer_picking2.save()
|
||
|
||
customer_picking3 = Form(customer_picking3)
|
||
with customer_picking3.move_ids_without_package.new() as move:
|
||
move.product_id = self.productA
|
||
move.product_uom_qty = 10
|
||
customer_picking3 = customer_picking3.save()
|
||
|
||
customer_picking4 = Form(customer_picking4)
|
||
with customer_picking4.move_ids_without_package.new() as move:
|
||
move.product_id = self.productA
|
||
move.product_uom_qty = 10
|
||
customer_picking4 = customer_picking4.save()
|
||
|
||
customer_picking5 = Form(customer_picking5)
|
||
with customer_picking5.move_ids_without_package.new() as move:
|
||
move.product_id = self.productA
|
||
move.product_uom_qty = 10
|
||
customer_picking5 = customer_picking5.save()
|
||
|
||
customer_picking1.action_assign()
|
||
customer_picking2.action_assign()
|
||
customer_picking3.action_assign()
|
||
self.assertEqual(customer_picking1.move_ids.reserved_availability, 0, "There should be no products available to reserve yet.")
|
||
self.assertEqual(customer_picking2.move_ids.reserved_availability, 0, "There should be no products available to reserve yet.")
|
||
self.assertEqual(customer_picking3.move_ids.reserved_availability, 0, "There should be no products available to reserve yet.")
|
||
|
||
self.assertFalse(customer_picking1.move_ids.reservation_date, "Reservation Method: 'manual' shouldn't have a reservation_date")
|
||
self.assertEqual(customer_picking2.move_ids.reservation_date, (customer_picking2.scheduled_date - timedelta(days=1)).date(),
|
||
"Reservation Method: 'by_date' should have a reservation_date = scheduled_date - reservation_days_before")
|
||
self.assertFalse(customer_picking5.move_ids.reservation_date, "Reservation Method: 'at_confirm' shouldn't have a reservation_date until confirmed")
|
||
|
||
# create supplier picking and move
|
||
supplier_picking = self.env['stock.picking'].create({
|
||
'location_id': self.customer_location,
|
||
'location_dest_id': self.stock_location,
|
||
'picking_type_id': self.picking_type_in,
|
||
})
|
||
supplier_move = self.env['stock.move'].create({
|
||
'name': 'test_transit_1',
|
||
'location_id': self.customer_location,
|
||
'location_dest_id': self.stock_location,
|
||
'product_id': self.productA.id,
|
||
'product_uom': self.productA.uom_id.id,
|
||
'product_uom_qty': 50.0,
|
||
'picking_id': supplier_picking.id,
|
||
})
|
||
supplier_move.quantity_done = 50
|
||
supplier_picking._action_done()
|
||
|
||
self.assertEqual(customer_picking1.move_ids.reserved_availability, 0, "Reservation Method: 'manual' shouldn't ever auto-assign")
|
||
self.assertEqual(customer_picking2.move_ids.reserved_availability, 0, "Reservation Method: 'by_date' shouldn't auto-assign when not within reservation date range")
|
||
self.assertEqual(customer_picking3.move_ids.reserved_availability, 10, "Reservation Method: 'by_date' should auto-assign when within reservation date range")
|
||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 40)
|
||
|
||
customer_picking4.action_confirm()
|
||
customer_picking5.action_confirm()
|
||
self.assertEqual(customer_picking4.move_ids.reserved_availability, 10, "Reservation Method: 'by_date' should auto-assign when within reservation date range at confirmation")
|
||
self.assertEqual(customer_picking5.move_ids.reserved_availability, 10, "Reservation Method: 'at_confirm' should auto-assign at confirmation")
|
||
|
||
def test_serial_lot_ids(self):
|
||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||
self.customer_location = self.env.ref('stock.stock_location_customers')
|
||
self.supplier_location = self.env.ref('stock.stock_location_suppliers')
|
||
self.uom_unit = self.env.ref('uom.product_uom_unit')
|
||
self.product_serial = self.env['product.product'].create({
|
||
'name': 'PSerial',
|
||
'type': 'product',
|
||
'tracking': 'serial',
|
||
'categ_id': self.env.ref('product.product_category_all').id,
|
||
})
|
||
|
||
move = self.env['stock.move'].create({
|
||
'name': 'TestReceive',
|
||
'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,
|
||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||
})
|
||
self.assertEqual(move.state, 'draft')
|
||
lot1 = self.env['stock.lot'].create({
|
||
'name': 'serial1',
|
||
'product_id': self.product_serial.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
lot2 = self.env['stock.lot'].create({
|
||
'name': 'serial2',
|
||
'product_id': self.product_serial.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
lot3 = self.env['stock.lot'].create({
|
||
'name': 'serial3',
|
||
'product_id': self.product_serial.id,
|
||
'company_id': self.env.company.id,
|
||
})
|
||
move.lot_ids = [(4, lot1.id)]
|
||
move.lot_ids = [(4, lot2.id)]
|
||
move.lot_ids = [(4, lot3.id)]
|
||
self.assertEqual(move.quantity_done, 3.0)
|
||
move.lot_ids = [(3, lot2.id)]
|
||
self.assertEqual(move.quantity_done, 2.0)
|
||
|
||
self.uom_dozen = self.env.ref('uom.product_uom_dozen')
|
||
move = self.env['stock.move'].create({
|
||
'name': 'TestReceiveDozen',
|
||
'location_id': self.supplier_location.id,
|
||
'location_dest_id': self.stock_location.id,
|
||
'product_id': self.product_serial.id,
|
||
'product_uom': self.uom_dozen.id,
|
||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||
})
|
||
move.lot_ids = [(4, lot1.id)]
|
||
move.lot_ids = [(4, lot2.id)]
|
||
move.lot_ids = [(4, lot3.id)]
|
||
self.assertEqual(move.quantity_done, 3.0/12.0)
|
||
|
||
def test_update_description(self):
|
||
""" Create an empty picking. Adds a move on product1, select the picking type, add
|
||
again a move on product1. Confirm the picking. The two stock moves should be merged. """
|
||
product1 = self.env['product.product'].create({
|
||
'name': 'product',
|
||
'type':'product',
|
||
})
|
||
picking_form = Form(self.env['stock.picking'])
|
||
with picking_form.move_ids_without_package.new() as move:
|
||
move.product_id = product1
|
||
move.product_uom_qty = 10
|
||
move.location_id = self.env.ref('stock.stock_location_suppliers')
|
||
move.location_dest_id = self.env.ref('stock.stock_location_stock')
|
||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||
with picking_form.move_ids_without_package.new() as move:
|
||
move.product_id = product1
|
||
move.product_uom_qty = 15
|
||
|
||
picking = picking_form.save()
|
||
picking.action_confirm()
|
||
|
||
self.assertEqual(len(picking.move_ids), 1)
|
||
self.assertEqual(picking.move_ids.product_uom_qty, 25)
|