Skip to content

Commit

Permalink
Merge pull request OCA#22 from jbaudoux/16.0-shopfloor-sbj-changelot
Browse files Browse the repository at this point in the history
[16.0][FIX] shopfloor: change lot action & fix inventory
  • Loading branch information
sbejaoui authored Jan 4, 2024
2 parents a5062fc + d665d05 commit fd5a751
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 138 deletions.
1 change: 1 addition & 0 deletions shopfloor/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"stock_helper",
"stock_picking_completion_info",
# OCA / stock-logistics-workflow
"stock_move_line_change_lot",
"stock_quant_package_dimension",
"stock_quant_package_product_packaging",
"stock_picking_progress",
Expand Down
151 changes: 48 additions & 103 deletions shopfloor/actions/change_package_lot.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# Copyright 2024 Jacques-Etienne Baudoux (BCIM) <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import _, exceptions
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare, float_is_zero

from odoo.addons.component.core import Component


class InventoryError(UserError):
pass


class ChangePackageLot(Component):
"""Provide methods for changing a package or a lot on a move line"""

Expand Down Expand Up @@ -58,116 +64,55 @@ def change_lot(self, move_line, lot, response_ok_func, response_error_func):
def _change_pack_lot_change_lot(
self, move_line, lot, response_ok_func, response_error_func
):
def is_lesser(value, other, rounding):
return float_compare(value, other, precision_rounding=rounding) == -1

inventory = self._actions_for("inventory")
product = move_line.product_id
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 = []
previous_reserved_uom_qty = move_line.reserved_uom_qty

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
inventory = self._actions_for("inventory")

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
)
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(
[
("lot_id", "=", lot.id),
("product_id", "=", product.id),
("location_id", "=", move_line.location_id.id),
("qty_done", "=", 0),
]
)
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.")
try:
with self.env.cr.savepoint():
move_line.write(
{
"lot_id": lot.id,
"package_id": False,
"result_package_id": False,
}
)

# 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
):
new_uom_qty = product.uom_id._compute_quantity(
available_quantity, move_line.product_uom_id, rounding_method="HALF-UP"
rounding = move_line.product_id.uom_id.rounding
if float_is_zero(
move_line.reserved_uom_qty, 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!
raise InventoryError("Lot not available")
except InventoryError:
inventory.create_control_stock(
move_line.location_id,
move_line.product_id,
lot=move_line.lot_id,
name=_("Pick: stock issue on lot: {} found in {}").format(
lot.name, move_line.location_id.name
),
)
values["reserved_uom_qty"] = new_uom_qty

move_line.write(values)

if "reserved_uom_qty" in 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()

# 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()
message = self.msg_store.cannot_change_lot_already_picked(lot)
return response_error_func(move_line, message=message)
except UserError as e:
message = {
"message_type": "error",
"body": str(e),
}
return response_error_func(move_line, message=message)

message = self.msg_store.lot_replaced_by_lot(previous_lot, lot)
if message_parts:
message["body"] = "{} {}".format(message["body"], " ".join(message_parts))
if (
float_compare(
move_line.reserved_uom_qty,
previous_reserved_uom_qty,
precision_rounding=rounding,
)
< 0
):
message["body"] += " " + _("The quantity to do has changed!")
return response_ok_func(move_line, message=message)

def _package_content_replacement_allowed(self, package, move_line):
Expand Down
44 changes: 33 additions & 11 deletions shopfloor/actions/inventory.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import _
from odoo import _, fields

from odoo.addons.component.core import Component

Expand Down Expand Up @@ -39,7 +39,7 @@ def _inventory_exists(self, location, product, package=None, lot=None):
domain.append(("lot_id", "=", lot.id))
return self.inventory_model.search_count(domain)

def _get_existing_quant(self, location, product, package=None, lot=None):
def _get_existing_quant(self, location, product, package=None, lot=None, limit=1):
domain = [("location_id", "=", location.id), ("product_id", "=", product.id)]
if package is not None:
domain.append(("package_id", "=", package.id))
Expand All @@ -49,21 +49,43 @@ def _get_existing_quant(self, location, product, package=None, lot=None):
domain.append(("lot_id", "=", lot.id))
else:
domain.append(("lot_id", "=", False))
return self.inventory_model.search(domain, limit=1)

def _create_draft_inventory(self, location, product):
return self.inventory_model.sudo().create(
{"location_id": location.id, "product_id": product.id}
)
return self.inventory_model.search(domain, limit=limit)

def _create_draft_inventory(self, location, product, lot=None):
quants = self._get_existing_quant(location, product, lot=lot, limit=None)
if quants:
for quant in quants:
if quant.inventory_quantity_set:
continue
quants.write(
{
# Set an inventory quantity to prevent the zero quant cleanup
"inventory_quantity": quant.inventory_quantity + 1,
"inventory_date": fields.Date.today(),
}
)
return quants
else:
return self.inventory_model.sudo().create(
{
"location_id": location.id,
"product_id": product.id,
"lot_id": lot.id,
"inventory_quantity": 1,
"inventory_date": fields.Date.today(),
}
)

def create_control_stock(self, location, product, package, lot, name=None):
def create_control_stock(
self, location, product, package=None, lot=None, name=None
):
"""Create a draft inventory so a user has to check a location
If a draft or in progress inventory already exists for the same
combination of product/package/lot, no inventory is created.
"""
if not self._inventory_exists(location, product):
self._create_draft_inventory(location, product)
if not self._inventory_exists(location, product, lot=lot):
self._create_draft_inventory(location, product, lot=lot)

def create_stock_issue(self, move, location, package, lot):
"""Create an inventory for a stock issue
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
1 change: 1 addition & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ odoo-addon-shopfloor-base @ git+https://github.com/OCA/wms.git@refs/pull/642/hea

# OCA/stock-logistics-workflow
odoo-addon-stock-picking-progress @ git+https://github.com/OCA/stock-logistics-workflow.git@refs/pull/1330/head#subdirectory=setup/stock_picking_progress
odoo-addon-stock-move-line-change-lot @ git+https://github.com/OCA/stock-logistics-workflow.git@refs/pull/1469/head#subdirectory=setup/stock_move_line_change_lot

# OCA/product-attribute
odoo-addon-product-packaging-level @ git+https://github.com/OCA/product-attribute.git@refs/pull/1215/head#subdirectory=setup/product_packaging_level
Expand Down

0 comments on commit fd5a751

Please sign in to comment.