Skip to content

Commit

Permalink
[FIX] shopfloor: change lot
Browse files Browse the repository at this point in the history
  • Loading branch information
jbaudoux committed Jan 3, 2024
1 parent a5062fc commit ceb2307
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 88 deletions.
137 changes: 73 additions & 64 deletions shopfloor/actions/change_package_lot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# Copyright 2024 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import _, exceptions
from odoo.tools.float_utils import float_compare, float_is_zero
Expand Down Expand Up @@ -63,107 +64,115 @@ def is_lesser(value, other, rounding):

inventory = self._actions_for("inventory")
product = move_line.product_id
rounding = product.uom_id.rounding
if lot.product_id != product:
return response_error_func(
move_line, message=self.msg_store.lot_on_wrong_product(lot.name)
)
previous_lot = move_line.lot_id
# Changing the lot on the move line updates the reservation on the quants

message_parts = []

# Changing the lot on the move line updates the reservation on the quants
values = {"lot_id": lot.id}

available_quantity = self.env["stock.quant"]._get_available_quantity(
product, move_line.location_id, lot_id=lot, strict=True
)

if move_line.package_id:
move_line.package_level_id.explode_package()
values["package_id"] = False

to_assign_moves = self.env["stock.move"]
if float_is_zero(
available_quantity, precision_rounding=product.uom_id.rounding
):
quants = self.env["stock.quant"]._gather(
product, move_line.location_id, lot_id=lot, strict=True
to_reassign_moves = self.env["stock.move"]
available_quantity = 0
quants = (
self.env["stock.quant"]
._gather(product, move_line.location_id, lot_id=lot, strict=True)
.filtered(
lambda q: float_compare(q.quantity, 0, precision_rounding=rounding) > 0
)
if quants:
# we have quants but they are all reserved by other lines:
# unreserve the other lines and reserve them again after
unreservable_lines = self.env["stock.move.line"].search(
)
if quants:
for quant in quants:
quant_available = quant.quantity - quant.reserved_quantity
if float_compare(quant_available, 0, precision_rounding=rounding) > 0:
available_quantity += quant_available

if is_lesser(available_quantity, move_line.reserved_qty, rounding):
# We switch reservations with other move lines that have not
# yet been processed
other_lines = self.env["stock.move.line"].search(
[
("lot_id", "=", lot.id),
("product_id", "=", product.id),
("location_id", "=", move_line.location_id.id),
("state", "in", ("partially_available", "assigned")),
("reserved_uom_qty", ">", 0),
("qty_done", "=", 0),
]
],
order="reserved_uom_qty desc",
)
if not unreservable_lines:
return response_error_func(
move_line,
message=self.msg_store.cannot_change_lot_already_picked(lot),
)
available_quantity = sum(unreservable_lines.mapped("reserved_qty"))
to_assign_moves = unreservable_lines.move_id
# if we leave the package level, it will try to reserve the same
# one again
unreservable_lines.package_level_id.explode_package()
# unreserve qties of other lines
unreservable_lines.unlink()
else:
# * we have *no* quant:
# The lot is not found at all, but the user scanned it, which means
# it's an error in the stock data! To allow the user to continue,
# we post an inventory to add the missing quantity, and a second
# draft inventory to check later
inventory.create_stock_correction(
move_line.move_id,
move_line.location_id,
self.env["stock.quant.package"].browse(),
lot,
move_line.reserved_qty,
)
inventory.create_control_stock(
move_line.location_id,
move_line.product_id,
move_line.package_id,
move_line.lot_id,
_("Pick: stock issue on lot: {} found in {}").format(
lot.name, move_line.location_id.name
),
)
message_parts.append(
_("A draft inventory has been created for control.")
# Favor lines from non-printed pickings.
other_lines.sorted(
lambda ml: (
ml.picking_id == move_line.picking_id
or not ml.picking_id.printed,
-ml.reserved_uom_qty)
)
# Stop when required quantity is reached
for line in other_lines:
available_quantity += line.reserved_qty
to_reassign_moves |= line.move_id
# if we leave the package level, it will try to reserve the same
# one again
line.package_level_id.explode_package()
# unreserve qties of other lines
line.unlink()
if (
float_compare(
available_quantity,
move_line.reserved_qty,
precision_rounding=rounding,
)
>= 0
):
# We reached the required quantity
break

if float_is_zero(available_quantity, precision_rounding=rounding):
# The lot is not found at all, but the user scanned it, which means
# it's an error in the stock data!
message = self.msg_store.cannot_change_lot_already_picked(lot)
# We post an draft inventory to control the stock
inventory.create_control_stock(
move_line.location_id,
move_line.product_id,
move_line.package_id,
move_line.lot_id,
_("Pick: stock issue on lot: {} found in {}").format(
lot.name, move_line.location_id.name
),
)
return response_error_func(move_line, message=message)

# re-evaluate float_is_zero because we may have changed available_quantity
if not float_is_zero(
available_quantity, precision_rounding=product.uom_id.rounding
) and is_lesser(
available_quantity, move_line.reserved_qty, product.uom_id.rounding
):
if is_lesser(available_quantity, move_line.reserved_qty, rounding):
new_uom_qty = product.uom_id._compute_quantity(
available_quantity, move_line.product_uom_id, rounding_method="HALF-UP"
)
values["reserved_uom_qty"] = new_uom_qty

move_line.write(values)

if "reserved_uom_qty" in values:
message_parts.append(_("The quantity to do has changed!"))
move_line.write(values)
# when we change the quantity of the move, the state
# will still be "assigned" and be skipped by "_action_assign",
# recompute the state to be "partially_available"
move_line.move_id._recompute_state()
else:
move_line.write(values)

# if the new package has less quantities, assign will create new move
# lines
move_line.move_id._action_assign()

# Find other available goods for the lines which were using the
# lot before...
to_assign_moves._action_assign()
if to_reassign_moves:
to_reassign_moves._action_assign()

message = self.msg_store.lot_replaced_by_lot(previous_lot, lot)
if message_parts:
Expand Down
47 changes: 23 additions & 24 deletions shopfloor/tests/test_actions_change_package_lot.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,13 @@ def test_change_lot_less_quantity_ok(self):
new_lot = self._create_lot(self.product_a)
# ensure we have our new package in the same location
self._update_qty_in_location(source_location, line.product_id, 8, lot=new_lot)
expected_message = self.msg_store.lot_replaced_by_lot(initial_lot, new_lot)
expected_message["body"] += " The quantity to do has changed!"
self.change_package_lot.change_lot(
line,
new_lot,
# success callback
lambda move_line, message=None: self.assertEqual(
message, self.msg_store.lot_replaced_by_lot(initial_lot, new_lot)
),
lambda move_line, message=None: self.assertEqual(message, expected_message),
# failure callback
self.unreachable_func,
)
Expand All @@ -114,34 +114,31 @@ def test_change_lot_less_quantity_ok(self):
self.assert_quant_reserved_qty(line, lambda: 2, lot=initial_lot)
self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=new_lot)

def test_change_lot_zero_quant_ok(self):
def test_change_lot_zero_quant_error(self):
"""No quant in the location for the scanned lot
As the user scanned it, it's an inventory error.
We expect a new posted inventory that updates the quantity.
And another control one.
"""
initial_lot = self._create_lot(self.product_a)
self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=initial_lot)
picking = self._create_picking(lines=[(self.product_a, 10)])
picking.action_assign()
line = picking.move_line_ids
new_lot = self._create_lot(self.product_a)
expected_message = self.msg_store.lot_replaced_by_lot(initial_lot, new_lot)
expected_message["body"] += " A draft inventory has been created for control."
expected_message = self.msg_store.cannot_change_lot_already_picked(new_lot)
self.change_package_lot.change_lot(
line,
new_lot,
# success callback
lambda move_line, message=None: self.assertEqual(message, expected_message),
# failure callback
self.unreachable_func,
# failure callback
lambda move_line, message=None: self.assertEqual(message, expected_message),
)

self.assertRecordValues(line, [{"lot_id": new_lot.id, "reserved_qty": 10}])
# check that reservations have been updated
self.assert_quant_reserved_qty(line, lambda: 0, lot=initial_lot)
self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=new_lot)
self.assertRecordValues(line, [{"lot_id": initial_lot.id, "reserved_qty": 10}])
# check that reservations have not been updated
self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=initial_lot)
self.assert_quant_reserved_qty(line, lambda: 0, lot=new_lot)

def test_change_lot_package_explode_ok(self):
"""Scan a lot on units replacing a package"""
Expand Down Expand Up @@ -247,6 +244,7 @@ def test_change_lot_reserved_partial_qty_ok(self):
self.assertEqual(line2.lot_id, new_lot)

expected_message = self.msg_store.lot_replaced_by_lot(initial_lot, new_lot)
expected_message["body"] += " The quantity to do has changed!"
self.change_package_lot.change_lot(
line,
new_lot,
Expand Down Expand Up @@ -312,31 +310,31 @@ def test_change_lot_reserved_qty_done_error(self):
self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=initial_lot)
self.assert_quant_reserved_qty(line2, lambda: line2.reserved_qty, lot=new_lot)

def test_change_lot_different_location_ok(self):
def test_change_lot_different_location_error(self):
"If the scanned lot is in a different location, we cannot process it"
self.product_a.tracking = "lot"
initial_lot = self._create_lot(self.product_a)
self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=initial_lot)
picking = self._create_picking(lines=[(self.product_a, 10)])
picking.action_assign()
line = picking.move_line_ids
new_lot = self._create_lot(self.product_a)
# ensure we have our new package in a different location
# ensure we have our new lot in a different location
self._update_qty_in_location(self.shelf2, line.product_id, 10, lot=new_lot)
expected_message = self.msg_store.lot_replaced_by_lot(initial_lot, new_lot)
expected_message["body"] += " A draft inventory has been created for control."
expected_message = self.msg_store.cannot_change_lot_already_picked(new_lot)
self.change_package_lot.change_lot(
line,
new_lot,
# success callback
lambda move_line, message=None: self.assertEqual(message, expected_message),
# failure callback
self.unreachable_func,
# failure callback
lambda move_line, message=None: self.assertEqual(message, expected_message),
)

self.assertRecordValues(line, [{"lot_id": new_lot.id}])
# check that reservations have been updated
self.assert_quant_reserved_qty(line, lambda: 0, lot=initial_lot)
self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=new_lot)
self.assertRecordValues(line, [{"lot_id": initial_lot.id}])
# check that reservations have not been updated
self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=initial_lot)
self.assert_quant_reserved_qty(line, lambda: 0, lot=new_lot)

def test_change_lot_in_several_packages_error(self):
self.product_a.tracking = "lot"
Expand Down Expand Up @@ -588,6 +586,7 @@ def test_change_pack_different_location(self):
picking = self._create_picking(lines=[(self.product_a, 10)])
picking.action_assign()
line = picking.move_line_ids
self.assertEqual(line.package_id, initial_package)
# when the operator wants to pick the initial package, in shelf1, the new
# package is in front of the other so they want to change the package
self.change_package_lot.change_package(
Expand Down

0 comments on commit ceb2307

Please sign in to comment.