diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index e9a32f38f2e..1d3cb98a94f 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -252,6 +252,10 @@ class StockMove(models.Model): res = super(StockMove, self)._should_be_assigned() return bool(res and not (self.production_id or self.raw_material_production_id)) + def _should_bypass_reservation(self): + res = super(StockMove, self)._should_bypass_reservation() + return bool(res and not self.production_id) + def _key_assign_picking(self): keys = super(StockMove, self)._key_assign_picking() return keys + (self.created_production_id,) diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py index d6acdf1db76..c1125633bf1 100644 --- a/addons/mrp/tests/test_workorder_operation.py +++ b/addons/mrp/tests/test_workorder_operation.py @@ -1766,6 +1766,7 @@ class TestRoutingAndKits(SavepointCase): self.bom_finished1.consumption = 'flexible' add_product = self.env['product.product'].create({ 'name': 'Additional', + 'type': 'product', }) mo_form = Form(self.env['mrp.production']) mo_form.product_id = self.finished1 @@ -1799,6 +1800,7 @@ class TestRoutingAndKits(SavepointCase): self.bom_finished1.consumption = 'flexible' add_product = self.env['product.product'].create({ 'name': 'Additional', + 'type': 'product', }) mo_form = Form(self.env['mrp.production']) mo_form.product_id = self.finished1 diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py index 822773beb01..f0189addc91 100644 --- a/addons/stock/models/stock_location.py +++ b/addons/stock/models/stock_location.py @@ -157,7 +157,7 @@ class Location(models.Model): def should_bypass_reservation(self): self.ensure_one() - return self.usage in ('supplier', 'customer', 'inventory', 'production') or self.scrap_location + return self.usage in ('supplier', 'customer', 'inventory', 'production') or self.scrap_location or (self.usage == 'transit' and not self.company_id) class Route(models.Model): diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index dd93a25a484..8aa76e8d2d3 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -1016,9 +1016,12 @@ class StockMove(models.Model): moves._assign_picking() self._push_apply() self._check_company() + moves = self if merge: - return self._merge_moves(merge_into=merge_into) - return self + moves = self._merge_moves(merge_into=merge_into) + # call `_action_assign` on every confirmed move which location_id bypasses the reservation + moves.filtered(lambda move: move._should_bypass_reservation() and move.state == 'confirmed')._action_assign() + return moves def _prepare_procurement_values(self): """ Prepare specific key for moves or other componenets that will be created from a stock rule diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index de8e9fa144a..b18920d4435 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -657,9 +657,6 @@ class Picking(models.Model): self.mapped('move_lines')\ .filtered(lambda move: move.state == 'draft')\ ._action_confirm() - # call `_action_assign` on every confirmed move which location_id bypasses the reservation - self.filtered(lambda picking: not picking.immediate_transfer and picking.location_id.usage in ('supplier', 'inventory', 'production') and picking.state == 'confirmed')\ - .mapped('move_lines')._action_assign() return True def action_assign(self): diff --git a/addons/stock/models/stock_production_lot.py b/addons/stock/models/stock_production_lot.py index 2ac426d33f5..40df922447c 100644 --- a/addons/stock/models/stock_production_lot.py +++ b/addons/stock/models/stock_production_lot.py @@ -7,7 +7,7 @@ from odoo.exceptions import UserError class ProductionLot(models.Model): _name = 'stock.production.lot' - _inherit = ['mail.thread','mail.activity.mixin'] + _inherit = ['mail.thread', 'mail.activity.mixin'] _description = 'Lot/Serial' _check_company_auto = True @@ -85,7 +85,7 @@ class ProductionLot(models.Model): def _product_qty(self): for lot in self: # We only care for the quants in internal or transit locations. - quants = lot.quant_ids.filtered(lambda q: q.location_id.usage in ['internal', 'transit']) + quants = lot.quant_ids.filtered(lambda q: q.location_id.usage == 'internal' or (q.location_id.usage == 'transit' and q.location_id.company_id)) lot.product_qty = sum(quants.mapped('quantity')) def action_lot_open_quants(self): diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py index 899343db755..b4bbbb07ef1 100644 --- a/addons/stock/models/stock_rule.py +++ b/addons/stock/models/stock_rule.py @@ -189,7 +189,10 @@ class StockRule(models.Model): else: new_move_vals = self._push_prepare_move_copy_values(move, new_date) new_move = move.sudo().copy(new_move_vals) - move.write({'move_dest_ids': [(4, new_move.id)]}) + if new_move._should_bypass_reservation(): + new_move.write({'procure_method': 'make_to_stock'}) + if not new_move.location_id.should_bypass_reservation(): + move.write({'move_dest_ids': [(4, new_move.id)]}) new_move._action_confirm() def _push_prepare_move_copy_values(self, move_to_copy, new_date): @@ -283,6 +286,11 @@ class StockRule(models.Model): # it is possible that we've already got some move done, so check for the done qty and create # a new move with the correct qty qty_left = product_qty + + move_dest_ids = [] + if not self.location_id.should_bypass_reservation(): + move_dest_ids = values.get('move_dest_ids', False) and [(4, x.id) for x in values['move_dest_ids']] or [] + move_values = { 'name': name[:2000], 'company_id': self.company_id.id or self.location_src_id.company_id.id or self.location_id.company_id.id or company_id.id, @@ -292,7 +300,7 @@ class StockRule(models.Model): 'partner_id': self.partner_address_id.id or (values.get('group_id', False) and values['group_id'].partner_id.id) or False, 'location_id': self.location_src_id.id, 'location_dest_id': location_id.id, - 'move_dest_ids': values.get('move_dest_ids', False) and [(4, x.id) for x in values['move_dest_ids']] or [], + 'move_dest_ids': move_dest_ids, 'rule_id': self.id, 'procure_method': self.procure_method, 'origin': origin, diff --git a/addons/stock/security/stock_security.xml b/addons/stock/security/stock_security.xml index bbf0a2714f5..e6364e02c36 100644 --- a/addons/stock/security/stock_security.xml +++ b/addons/stock/security/stock_security.xml @@ -116,14 +116,14 @@ stock_move multi-company - ['|', ('company_id', 'in', company_ids), '&', ('location_dest_id.company_id', '=', False), ('location_dest_id.usage', '=', 'transit')] + [('company_id', 'in', company_ids)] stock_move_line multi-company - ['|','|',('company_id','=',False),('company_id', 'in', company_ids),'&',('location_dest_id.company_id', '=', False), ('location_dest_id.usage', '=', 'transit')] + ['|',('company_id','=',False),('company_id', 'in', company_ids)] diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index 2ea2047baf0..cb0583a4101 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -67,10 +67,6 @@ class StockMove(SavepointCase): # confirmation move1._action_confirm() - self.assertEqual(move1.state, 'confirmed') - - # assignment - move1._action_assign() self.assertEqual(move1.state, 'assigned') self.assertEqual(len(move1.move_line_ids), 1) @@ -109,10 +105,6 @@ class StockMove(SavepointCase): # confirmation move1._action_confirm() - self.assertEqual(move1.state, 'confirmed') - - # assignment - move1._action_assign() self.assertEqual(move1.state, 'assigned') self.assertEqual(len(move1.move_line_ids), 1) move_line = move1.move_line_ids[0] @@ -154,10 +146,6 @@ class StockMove(SavepointCase): # confirmation move1._action_confirm() - self.assertEqual(move1.state, 'confirmed') - - # assignment - move1._action_assign() self.assertEqual(move1.state, 'assigned') self.assertEqual(len(move1.move_line_ids), 5) move_line = move1.move_line_ids[0] @@ -260,10 +248,6 @@ class StockMove(SavepointCase): # confirmation move1._action_confirm() - self.assertEqual(move1.state, 'confirmed') - - # assignment - move1._action_assign() self.assertEqual(move1.state, 'assigned') self.assertEqual(len(move1.move_line_ids), 1) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) @@ -714,10 +698,6 @@ class StockMove(SavepointCase): 'product_uom_qty': 100.0, }) move1._action_confirm() - self.assertEqual(move1.state, 'confirmed') - - # assignment - move1._action_assign() self.assertEqual(move1.state, 'assigned') self.assertEqual(len(move1.move_line_ids), 1) @@ -755,10 +735,6 @@ class StockMove(SavepointCase): 'product_uom_qty': 100.0, }) move1._action_confirm() - self.assertEqual(move1.state, 'confirmed') - - # assignment - move1._action_assign() self.assertEqual(move1.state, 'assigned') self.assertEqual(len(move1.move_line_ids), 1) @@ -809,10 +785,6 @@ class StockMove(SavepointCase): 'product_uom_qty': 100.0, }) move1._action_confirm() - self.assertEqual(move1.state, 'confirmed') - - # assignment - move1._action_assign() self.assertEqual(move1.state, 'assigned') self.assertEqual(len(move1.move_line_ids), 1) @@ -865,10 +837,6 @@ class StockMove(SavepointCase): 'product_uom_qty': 100.0, }) move1._action_confirm() - self.assertEqual(move1.state, 'confirmed') - - # assignment - move1._action_assign() self.assertEqual(move1.state, 'assigned') self.assertEqual(len(move1.move_line_ids), 1) @@ -908,10 +876,6 @@ class StockMove(SavepointCase): 'product_uom_qty': 100.0, }) move1._action_confirm() - self.assertEqual(move1.state, 'confirmed') - - # assignment - move1._action_assign() self.assertEqual(move1.state, 'assigned') self.assertEqual(len(move1.move_line_ids), 1) @@ -967,10 +931,6 @@ class StockMove(SavepointCase): 'product_uom_qty': 100.0, }) move1._action_confirm() - self.assertEqual(move1.state, 'confirmed') - - # assignment - move1._action_assign() self.assertEqual(move1.state, 'assigned') self.assertEqual(len(move1.move_line_ids), 1) @@ -1020,10 +980,6 @@ class StockMove(SavepointCase): 'product_uom_qty': 100.0, }) move1._action_confirm() - self.assertEqual(move1.state, 'confirmed') - - # assignment - move1._action_assign() self.assertEqual(move1.state, 'assigned') self.assertEqual(len(move1.move_line_ids), 1) @@ -1921,7 +1877,7 @@ class StockMove(SavepointCase): (move_supp_stock_1 + move_supp_stock_2 + move_stock_stock_1 + move_stock_stock_2)._action_confirm() move_supp_stock_1._action_assign() self.assertEqual(move_supp_stock_1.state, 'assigned') - self.assertEqual(move_supp_stock_2.state, 'confirmed') + self.assertEqual(move_supp_stock_2.state, 'assigned') self.assertEqual(move_stock_stock_1.state, 'waiting') self.assertEqual(move_stock_stock_2.state, 'waiting') self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0) @@ -1931,7 +1887,7 @@ class StockMove(SavepointCase): move_supp_stock_1.move_line_ids.qty_done = 3.0 move_supp_stock_1._action_done() self.assertEqual(move_supp_stock_1.state, 'done') - self.assertEqual(move_supp_stock_2.state, 'confirmed') + self.assertEqual(move_supp_stock_2.state, 'assigned') self.assertEqual(move_stock_stock_1.state, 'assigned') self.assertEqual(move_stock_stock_2.state, 'waiting') diff --git a/addons/stock/tests/test_multicompany.py b/addons/stock/tests/test_multicompany.py index 3ebc0f0c6f9..44f981f26eb 100644 --- a/addons/stock/tests/test_multicompany.py +++ b/addons/stock/tests/test_multicompany.py @@ -386,3 +386,213 @@ class TestMultiCompany(SavepointCase): }) with self.assertRaises(UserError): move._action_confirm() + + def test_intercom_lot_push(self): + """ Create a push rule to transfer products received in inter company + transit location to company b. Move a lot product from company a to the + transit location. Check the move created by the push rule is not chained + with previous move, and no product are reserved from inter-company + transit. """ + supplier_location = self.env.ref('stock.stock_location_suppliers') + intercom_location = self.env.ref('stock.stock_location_inter_wh') + intercom_location.write({'active': True}) + + product_lot = self.env['product.product'].create({ + 'type': 'product', + 'tracking': 'lot', + 'name': 'product lot', + }) + + picking_type_to_transit = self.env['stock.picking.type'].create({ + 'name': 'To Transit', + 'sequence_code': 'TRANSIT', + 'code': 'outgoing', + 'company_id': self.company_a.id, + 'warehouse_id': False, + 'default_location_src_id': self.stock_location_a.id, + 'default_location_dest_id': intercom_location.id, + 'sequence_id': self.env['ir.sequence'].create({ + 'code': 'transit', + 'name': 'transit sequence', + 'company_id': self.company_a.id, + }).id, + }) + + route = self.env['stock.location.route'].create({ + 'name': 'Push', + 'company_id': False, + 'rule_ids': [(0, False, { + 'name': 'create a move to company b', + 'company_id': self.company_b.id, + 'location_src_id': intercom_location.id, + 'location_id': self.stock_location_b.id, + 'action': 'push', + 'auto': 'manual', + 'picking_type_id': self.warehouse_b.in_type_id.id, + })], + }) + + move_from_supplier = self.env['stock.move'].create({ + 'company_id': self.company_a.id, + 'name': 'test_from_supplier', + 'location_id': supplier_location.id, + 'location_dest_id': self.stock_location_a.id, + 'product_id': product_lot.id, + 'product_uom': product_lot.uom_id.id, + 'product_uom_qty': 1.0, + 'picking_type_id': self.warehouse_a.in_type_id.id, + }) + move_from_supplier._action_confirm() + move_line_1 = move_from_supplier.move_line_ids[0] + move_line_1.lot_name = 'lot 1' + move_line_1.qty_done = 1.0 + move_from_supplier._action_done() + lot_1 = move_line_1.lot_id + + move_to_transit = self.env['stock.move'].create({ + 'company_id': self.company_a.id, + 'name': 'test_to_transit', + 'location_id': self.stock_location_a.id, + 'location_dest_id': intercom_location.id, + 'product_id': product_lot.id, + 'product_uom': product_lot.uom_id.id, + 'product_uom_qty': 1.0, + 'picking_type_id': picking_type_to_transit.id, + 'route_ids': [(4, route.id)], + }) + move_to_transit._action_confirm() + move_to_transit._action_assign() + move_line_2 = move_to_transit.move_line_ids[0] + self.assertTrue(move_line_2.lot_id, move_line_1.lot_id) + move_line_2.qty_done = 1.0 + move_to_transit._action_done() + + move_push = self.env['stock.move'].search([('location_id', '=', intercom_location.id), + ('product_id', '=', product_lot.id)]) + self.assertTrue(move_push, 'No move created from push rules') + self.assertEqual(move_push.state, "assigned") + self.assertTrue(move_push.move_line_ids, "No move line created for the move") + self.assertFalse(move_push in move_to_transit.move_dest_ids, + "Chained move created in transit location") + self.assertNotEqual(move_push.move_line_ids.lot_id, move_line_2.lot_id, + "Reserved from transit location") + picking_receipt = move_push.picking_id + with self.assertRaises(UserError): + picking_receipt.button_validate() + + move_line_3 = move_push.move_line_ids[0] + move_line_3.lot_name = 'lot 2' + move_line_3.qty_done = 1.0 + picking_receipt.button_validate() + lot_2 = move_line_3.lot_id + self.assertEqual(lot_1.company_id, self.company_a) + self.assertEqual(lot_1.name, 'lot 1') + self.assertEqual(self.env['stock.quant']._get_available_quantity(product_lot, intercom_location, lot_1), 1.0) + self.assertEqual(lot_2.company_id, self.company_b) + self.assertEqual(lot_2.name, 'lot 2') + self.assertEqual(self.env['stock.quant']._get_available_quantity(product_lot, self.stock_location_b, lot_2), 1.0) + + def test_intercom_lot_pull(self): + """Use warehouse of comany a to resupply warehouse of company b. Check + pull rule works correctly in two companies and moves are unchained from + inter-company transit location.""" + customer_location = self.env.ref('stock.stock_location_customers') + supplier_location = self.env.ref('stock.stock_location_suppliers') + intercom_location = self.env.ref('stock.stock_location_inter_wh') + intercom_location.write({'active': True}) + partner = self.env['res.partner'].create({'name': 'Deco Addict'}) + self.warehouse_a.resupply_wh_ids = [(6, 0, [self.warehouse_b.id])] + resupply_route = self.env['stock.location.route'].search([('supplier_wh_id', '=', self.warehouse_b.id), + ('supplied_wh_id', '=', self.warehouse_a.id)]) + self.assertTrue(resupply_route, "Resupply route not found") + + product_lot = self.env['product.product'].create({ + 'type': 'product', + 'tracking': 'lot', + 'name': 'product lot', + 'route_ids': [(4, resupply_route.id), (4, self.env.ref('stock.route_warehouse0_mto').id)], + }) + + move_sup_to_whb = self.env['stock.move'].create({ + 'company_id': self.company_b.id, + 'name': 'from_supplier_to_whb', + 'location_id': supplier_location.id, + 'location_dest_id': self.warehouse_b.lot_stock_id.id, + 'product_id': product_lot.id, + 'product_uom': product_lot.uom_id.id, + 'product_uom_qty': 1.0, + 'picking_type_id': self.warehouse_b.in_type_id.id, + }) + move_sup_to_whb._action_confirm() + move_line_1 = move_sup_to_whb.move_line_ids[0] + move_line_1.lot_name = 'lot b' + move_line_1.qty_done = 1.0 + move_sup_to_whb._action_done() + lot_b = move_line_1.lot_id + + picking_out = self.env['stock.picking'].create({ + 'company_id': self.company_a.id, + 'partner_id': partner.id, + 'picking_type_id': self.warehouse_a.out_type_id.id, + 'location_id': self.stock_location_a.id, + 'location_dest_id': customer_location.id, + }) + move_wha_to_cus = self.env['stock.move'].create({ + 'name': "WH_A to Customer", + 'product_id': product_lot.id, + 'product_uom_qty': 1, + 'product_uom': product_lot.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location_a.id, + 'location_dest_id': customer_location.id, + 'warehouse_id': self.warehouse_a.id, + 'procure_method': 'make_to_order', + 'company_id': self.company_a.id, + }) + picking_out.action_confirm() + + move_whb_to_transit = self.env['stock.move'].search([('location_id', '=', self.stock_location_b.id), + ('product_id', '=', product_lot.id)]) + move_transit_to_wha = self.env['stock.move'].search([('location_id', '=', intercom_location.id), + ('product_id', '=', product_lot.id)]) + self.assertTrue(move_whb_to_transit, "No move created by pull rule") + self.assertTrue(move_transit_to_wha, "No move created by pull rule") + self.assertTrue(move_wha_to_cus in move_transit_to_wha.move_dest_ids, + "Moves are not chained") + self.assertFalse(move_transit_to_wha in move_whb_to_transit.move_dest_ids, + "Chained move created in transit location") + self.assertEqual(move_wha_to_cus.state, "waiting") + self.assertEqual(move_transit_to_wha.state, "waiting") + self.assertEqual(move_whb_to_transit.state, "confirmed") + + (move_wha_to_cus + move_whb_to_transit + move_transit_to_wha).picking_id.action_assign() + self.assertEqual(move_wha_to_cus.state, "waiting") + self.assertEqual(move_transit_to_wha.state, "assigned") + self.assertEqual(move_whb_to_transit.state, "assigned") + + res_dict = move_whb_to_transit.picking_id.button_validate() + self.assertEqual(res_dict.get('res_model'), 'stock.immediate.transfer') + wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save() + wizard.process() + self.assertEqual(self.env['stock.quant']._get_available_quantity(product_lot, intercom_location, lot_b), 1.0) + with self.assertRaises(UserError): + move_transit_to_wha.picking_id.button_validate() + + move_line_2 = move_transit_to_wha.move_line_ids[0] + move_line_2.lot_name = 'lot a' + move_line_2.qty_done = 1.0 + move_transit_to_wha._action_done() + lot_a = move_line_2.lot_id + + move_wha_to_cus._action_assign() + self.assertEqual(move_wha_to_cus.state, "assigned") + res_dict = move_wha_to_cus.picking_id.button_validate() + self.assertEqual(res_dict.get('res_model'), 'stock.immediate.transfer') + wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save() + wizard.process() + self.assertEqual(self.env['stock.quant']._get_available_quantity(product_lot, customer_location, lot_a), 1.0) + + self.assertEqual(lot_a.company_id, self.company_a) + self.assertEqual(lot_a.name, 'lot a') + self.assertEqual(lot_b.company_id, self.company_b) + self.assertEqual(lot_b.name, 'lot b')