diff --git a/stock_restrict_lot/models/stock_move.py b/stock_restrict_lot/models/stock_move.py index 1e1bd9ac6f10..ba052986d7c3 100644 --- a/stock_restrict_lot/models/stock_move.py +++ b/stock_restrict_lot/models/stock_move.py @@ -1,10 +1,11 @@ from odoo import _, api, exceptions, fields, models +from odoo.exceptions import ValidationError class StockMove(models.Model): _inherit = "stock.move" - # seems better to not copy this field except when a move is splitted, because a move + # seems better to not copy this field except when a move is split, because a move # can be copied in multiple different occasions and could even be copied with a # different product... restrict_lot_id = fields.Many2one("stock.lot", string="Restrict Lot", copy=False) @@ -88,3 +89,74 @@ def _split(self, qty, restrict_partner_id=False): if vals_list and self.restrict_lot_id: vals_list[0]["restrict_lot_id"] = self.restrict_lot_id.id return vals_list + + def get_all_dest_moves(self): + res = self.move_dest_ids + moves_to_search = self.move_dest_ids + while moves_to_search: + new_dest_ids = moves_to_search.move_dest_ids + moves_to_search = new_dest_ids - res + res |= new_dest_ids + return res + + def get_all_orig_moves(self): + res = self.move_orig_ids + moves_to_search = self.move_orig_ids + while moves_to_search: + new_orig_ids = moves_to_search.move_orig_ids + moves_to_search = new_orig_ids - res + res |= new_orig_ids + return res + + def write(self, vals): + if "restrict_lot_id" not in vals: + res = super().write(vals) + if "move_dest_ids" in vals or "move_orig_ids" in vals: + for move in self: + chained_moves = ( + move | move.get_all_dest_moves() | move.get_all_orig_moves() + ) + if not move.restrict_lot_id and move.state not in ["done", "cancel"]: + # update restrict_lot_id on current move from chained_moves + if chained_moves.restrict_lot_id: + move.restrict_lot_id = chained_moves.restrict_lot_id[0] + else: + # update chained_moves + to_update_move = chained_moves.filtered( + lambda sm: sm.state not in ['done', 'cancel'] and + sm.restrict_lot_id != move.restrict_lot_id + ) + to_update_move.restrict_lot_id = move.restrict_lot_id + else: + return res + else: + restrict_lot_id = vals.pop("restrict_lot_id") + restrict_lot = self.env["stock.lot"].browse(restrict_lot_id) + chained_moves = self | self.get_all_dest_moves() | self.get_all_orig_moves() + if any( + [ + sm.state == "done" and sm.lot_ids != restrict_lot + for sm in chained_moves + ] + ): + raise ValidationError( + _( + "You can't modify the Lot/Serial number " + "because at least one move in the chain has " + "already been done with another Lot/Serial number." + ) + ) + super(StockMove, chained_moves).write({"restrict_lot_id": restrict_lot_id}) + return super().write(vals) + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + for move in res: + if not move.restrict_lot_id: + chained_moves = ( + move | move.get_all_dest_moves() | move.get_all_orig_moves() + ) + if chained_moves.restrict_lot_id: + move.restrict_lot_id = chained_moves.restrict_lot_id[0] + return res diff --git a/stock_restrict_lot/tests/test_restrict_lot.py b/stock_restrict_lot/tests/test_restrict_lot.py index e8c3bac3f6a6..cb8e468ca717 100644 --- a/stock_restrict_lot/tests/test_restrict_lot.py +++ b/stock_restrict_lot/tests/test_restrict_lot.py @@ -1,3 +1,4 @@ +from odoo.exceptions import ValidationError from odoo.tests.common import TransactionCase @@ -19,6 +20,50 @@ def setUpClass(cls): } ) + def _create_move_with_lot(self): + move = self.env["stock.move"].create( + { + "product_id": self.product.id, + "location_id": self.output_loc.id, + "location_dest_id": self.customer_loc.id, + "product_uom_qty": 1, + "product_uom": self.product.uom_id.id, + "name": "test", + "procure_method": "make_to_order", + "warehouse_id": self.warehouse.id, + "route_ids": [(6, 0, self.warehouse.delivery_route_id.ids)], + "restrict_lot_id": self.lot.id, + } + ) + move._action_confirm() + move._action_assign() + new_lot = self.env["stock.lot"].create( + { + "name": "lot2", + "product_id": self.product.id, + "company_id": self.warehouse.company_id.id, + } + ) + return move, new_lot + + def _create_move_dest(self, with_lot = True): + return self.env["stock.move"].create( + { + "product_id": self.product.id, + "location_id": self.customer_loc.id, + "product_uom_qty": 1, + "product_uom": self.product.uom_id.id, + "picking_type_id": self.warehouse.out_type_id.id, + "location_dest_id": self.output_loc.id, + "name": "test", + "procure_method": "make_to_order", + "warehouse_id": self.warehouse.id, + "route_ids": [(6, 0, self.warehouse.delivery_route_id.ids)], + "state": "waiting", + "restrict_lot_id": self.lot.id if with_lot else False, + } + ) + def test_00_move_restrict_lot_propagation(self): move = self.env["stock.move"].create( { @@ -224,3 +269,58 @@ def assert_move_line_per_lot_and_location( assert_move_line_per_lot_and_location( pick.move_line_ids_without_package, lot2, location_2, 25 ) + + def test_restrict_lot_propagation_dest_moves(self): + move, new_lot = self._create_move_with_lot() + move_dest = self._create_move_dest() + move.move_dest_ids = [(4, move_dest.id)] + self.assertEqual(move_dest.restrict_lot_id, self.lot) + move.restrict_lot_id = new_lot.id + self.assertEqual(move_dest.restrict_lot_id, new_lot) + + def test_restrict_lot_propagation_dest_moves_without_lot(self): + move, new_lot = self._create_move_with_lot() + move_dest = self._create_move_dest(False) + move.move_dest_ids = [(4, move_dest.id)] + chained_moves = ( + move | move.get_all_dest_moves() | move.get_all_orig_moves() + ) + self.assertEqual(chained_moves.restrict_lot_id, self.lot) + self.assertEqual(move_dest.restrict_lot_id, self.lot) + move.restrict_lot_id = new_lot.id + self.assertEqual(move_dest.restrict_lot_id, new_lot) + + def test_restrict_lot_propagation_origin_moves(self): + move, new_lot = self._create_move_with_lot() + orig_move = move.move_orig_ids + self.assertEqual(orig_move.restrict_lot_id, self.lot) + move.restrict_lot_id = new_lot.id + self.assertEqual(orig_move.restrict_lot_id, new_lot) + + def test_restrict_lot_propagation_orig_moves_without_lot(self): + move, new_lot = self._create_move_with_lot() + move_dest = self._create_move_dest(False) + move_dest.move_orig_ids = [(4, move.id)] + self.assertEqual(move_dest.restrict_lot_id, self.lot) + move.restrict_lot_id = new_lot.id + self.assertEqual(move_dest.restrict_lot_id, new_lot) + + def test_restrict_lot_propagation_origin_and_dest_moves(self): + move, new_lot = self._create_move_with_lot() + move_dest = self._create_move_dest() + move.move_dest_ids = [(4, move_dest.id)] + orig_move = move.move_orig_ids + self.assertEqual(move_dest.restrict_lot_id, self.lot) + self.assertEqual(orig_move.restrict_lot_id, self.lot) + move.restrict_lot_id = new_lot.id + self.assertEqual(orig_move.restrict_lot_id, new_lot) + self.assertEqual(move_dest.restrict_lot_id, new_lot) + + def test_restrict_lot_no_propagation_error(self): + move, new_lot = self._create_move_with_lot() + orig_move = move.move_orig_ids + orig_move.state = "done" + self.assertEqual(orig_move.state, "done") + with self.assertRaises(ValidationError) as m: + move.restrict_lot_id = new_lot.id + self.assertIn("You can't modify the Lot/Serial number", m.exception.args[0])