Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[14][IMP] shopfloor: Ensure destination location on moves for allow move create #973

Open
wants to merge 2 commits into
base: 14.0
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions shopfloor/actions/stock.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# Copyright 2025 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import _, fields
from odoo.tools.float_utils import float_round
@@ -228,3 +229,26 @@ def no_putaway_available(self, picking_types, move_lines):
# when no putaway is found, the move line destination stays the
# default's of the picking type
return any(line.location_dest_id in base_locations for line in move_lines)

def _lock_lines(self, lines):
self._actions_for("lock").for_update(lines)

def _set_destination_on_lines(self, lines, location_dest):
# when writing the destination on the package level, it writes
# on the moves and move lines
lines_with_package_level = lines.package_level_id.move_line_ids
lines_without_package_level = lines - lines_with_package_level
if lines_with_package_level:
lines_with_package_level.package_level_id.location_dest_id = location_dest
if lines_without_package_level:
lines_without_package_level.location_dest_id = location_dest
lines_without_package_level.move_id.location_dest_id = location_dest

def _unload_package(self, lines):
lines.result_package_id = False

def set_destination_and_unload_lines(self, lines, location_dest, unload=False):
self._lock_lines(lines)
self._set_destination_on_lines(lines, location_dest)
if unload:
self._unload_package(lines)
15 changes: 6 additions & 9 deletions shopfloor/services/cluster_picking.py
Original file line number Diff line number Diff line change
@@ -1147,8 +1147,11 @@ def set_destination_all(self, picking_batch_id, barcode, confirmation=None):
return self._unload_end(batch, completion_info_popup=completion_info_popup)

def _unload_write_destination_on_lines(self, lines, location):
lines.write({"shopfloor_unloaded": True, "location_dest_id": location.id})
lines.package_level_id.location_dest_id = location
stock = self._actions_for("stock")
stock.set_destination_and_unload_lines(
lines, location, self.work.menu.unload_package_at_destination
)
lines.write({"shopfloor_unloaded": True})
for line in lines:
# We set the picking to done only when the last line is
# unloaded to avoid backorders.
@@ -1158,8 +1161,6 @@ def _unload_write_destination_on_lines(self, lines, location):
picking_lines = picking.mapped("move_line_ids")
if all(line.shopfloor_unloaded for line in picking_lines):
picking._action_done()
if self.work.menu.unload_package_at_destination:
lines.result_package_id = False

def _unload_end(self, batch, completion_info_popup=None):
"""Try to close the batch if all transfers are done.
@@ -1279,15 +1280,11 @@ def unload_scan_destination(
batch, package, lines, barcode, confirmation=confirmation
)

def _lock_lines(self, lines):
"""Lock move lines"""
self._actions_for("lock").for_update(lines)

def _unload_scan_destination_lines(
self, batch, package, lines, barcode, confirmation=None
):
# Lock move lines that will be updated
self._lock_lines(lines)
self._actions_for("lock").for_update(lines)
first_line = fields.first(lines)
scanned_location = self._actions_for("search").location_from_scan(barcode)
if not scanned_location:
5 changes: 2 additions & 3 deletions shopfloor/services/location_content_transfer.py
Original file line number Diff line number Diff line change
@@ -456,10 +456,9 @@ def _find_transfer_move_lines(self, location):
)
return lines

# hook used in module shopfloor_checkout_sync
def _write_destination_on_lines(self, lines, location, package=None):
lines.location_dest_id = location
lines.package_level_id.location_dest_id = location
stock = self._actions_for("stock")
stock.set_destination_and_unload_lines(lines, location)
if package:
lines.result_package_id = package

7 changes: 4 additions & 3 deletions shopfloor/services/single_pack_transfer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2020-2021 Camptocamp SA (http://www.camptocamp.com)
# Copyright 2020-2021 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
# Copyright 2020 Akretion (http://www.akretion.com)
# Copyright 2025 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import fields

@@ -266,10 +267,10 @@ def _router_validate_success(self, package_level):
return self._response_for_start(message=message, popup=completion_info_popup)

def _set_destination_and_done(self, package_level, scanned_location):
# when writing the destination on the package level, it writes
# on the move lines
package_level.location_dest_id = scanned_location
stock = self._actions_for("stock")
stock.set_destination_and_unload_lines(
package_level.move_line_ids, scanned_location
)
stock.put_package_level_in_move(package_level)
stock.validate_moves(package_level.move_line_ids.move_id)

15 changes: 5 additions & 10 deletions shopfloor/services/zone_picking.py
Original file line number Diff line number Diff line change
@@ -1036,7 +1036,7 @@ def _set_destination_package(self, move_line, quantity, package):
)
return (package_changed, response)
stock = self._actions_for("stock")
self._lock_lines(move_line)
stock._lock_lines(move_line)
try:
stock.mark_move_line_as_picked(
move_line, quantity, package, check_user=True
@@ -1591,11 +1591,10 @@ def set_destination_all(self, barcode, confirmation=None):
return self._set_destination_all_response(buffer_lines, message=message)

def _write_destination_on_lines(self, lines, location):
self._lock_lines(lines)
lines.location_dest_id = location
lines.package_level_id.location_dest_id = location
if self.work.menu.unload_package_at_destination:
lines.result_package_id = False
stock = self._actions_for("stock")
stock.set_destination_and_unload_lines(
lines, location, unload=self.work.menu.unload_package_at_destination
)

def unload_split(self):
"""Indicates that now the buffer must be treated line per line
@@ -1676,10 +1675,6 @@ def unload_scan_pack(self, package_id, barcode):
unload_single_message=self.msg_store.barcode_no_match(package.name),
)

def _lock_lines(self, lines):
"""Lock move lines"""
self._actions_for("lock").for_update(lines)

def unload_set_destination(self, package_id, barcode, confirmation=None):
"""Scan the final destination for move lines in the buffer with the
destination package
74 changes: 71 additions & 3 deletions shopfloor/tests/test_single_pack_transfer.py
Original file line number Diff line number Diff line change
@@ -186,7 +186,8 @@ def test_start_no_operation_create(self):
package_level = move_line.package_level_id

self.assertTrue(package_level.is_done)

self.assertEqual(move_line.location_id, self.pack_a.location_id)
self.assertEqual(move_line.move_id.location_id, self.pack_a.location_id)
expected_data = {
"id": package_level.id,
"name": package_level.package_id.name,
@@ -204,6 +205,60 @@ def test_start_no_operation_create(self):

self.assert_response(response, next_state="scan_location", data=expected_data)

def test_start_validate_no_operation_create(self):
self.menu.sudo().allow_move_create = True
self.picking.do_unreserve()
barcode = self.pack_a.name
params = {"barcode": barcode}

# Simulate the client scanning a package's barcode, which
# in turns should start the operation in odoo
response = self.service.dispatch("start", params=params)

move_line = self.env["stock.move.line"].search(
[("package_id", "=", self.pack_a.id)]
)
package_level = move_line.package_level_id

response = self.service.dispatch(
"validate",
params={
"package_level_id": package_level.id,
"location_barcode": self.shelf2.barcode,
},
)

self.assert_response(
response,
next_state="start",
message={
"message_type": "success",
"body": "The pack has been moved, you can scan a new pack.",
},
)

self.assertRecordValues(
package_level.move_line_ids,
[
{
"qty_done": 1.0,
"location_dest_id": self.shelf2.id,
"location_id": self.shelf1.id,
"state": "done",
}
],
)
self.assertRecordValues(
package_level.move_line_ids.move_id,
[
{
"location_dest_id": self.shelf2.id,
"location_id": self.shelf1.id,
"state": "done",
}
],
)

def test_start_barcode_not_known(self):
"""Test /start when the barcode is unknown

@@ -449,11 +504,24 @@ def test_validate(self):

self.assertRecordValues(
package_level.move_line_ids,
[{"qty_done": 1.0, "location_dest_id": self.shelf2.id, "state": "done"}],
[
{
"qty_done": 1.0,
"location_dest_id": self.shelf2.id,
"location_id": self.shelf1.id,
"state": "done",
}
],
)
self.assertRecordValues(
package_level.move_line_ids.move_id,
[{"location_dest_id": self.shelf2.id, "state": "done"}],
[
{
"location_dest_id": self.shelf2.id,
"location_id": self.shelf1.location_id.id,
"state": "done",
}
],
)

def test_validate_completion_info(self):
1 change: 0 additions & 1 deletion shopfloor_checkout_sync/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
from . import actions
from . import services
1 change: 1 addition & 0 deletions shopfloor_checkout_sync/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import checkout_sync
from . import stock
23 changes: 12 additions & 11 deletions shopfloor_checkout_sync/actions/checkout_sync.py
Original file line number Diff line number Diff line change
@@ -16,19 +16,20 @@ def _has_to_sync_destination(self, lines):
# the sync has already been done
return any(line.location_dest_id.child_ids for line in lines)

def _all_lines_to_lock(self, lines):
def _all_moves(self, lines):
if self._has_to_sync_destination(lines):
dest_pickings = lines.move_id._moves_to_sync_checkout()
all_moves = self.env["stock.move"].union(*dest_pickings.values())
# add lock on all the lines that will be synchronized on the
# destination so other transactions will wait before trying to
# change the destination
lines = lines | all_moves.move_line_ids
return lines
return all_moves
return lines.move_id

def _all_lines_to_lock(self, lines):
# add lock on all the lines that will be synchronized on the
# destination so other transactions will wait before trying to
# change the destination
all_moves = self._all_moves(lines)
return lines | all_moves.move_line_ids

def _sync_checkout(self, lines, location):
moves = lines.mapped("move_id")
if self._has_to_sync_destination(lines):
dest_pickings = moves._moves_to_sync_checkout()
all_moves = self.env["stock.move"].union(*dest_pickings.values())
all_moves.sync_checkout_destination(location)
all_moves = self._all_moves(lines)
all_moves.sync_checkout_destination(location)
18 changes: 18 additions & 0 deletions shopfloor_checkout_sync/actions/stock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2024 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo.addons.component.core import Component


class StockAction(Component):
_inherit = "shopfloor.stock.action"

def _set_destination_on_lines(self, lines, location_dest):
checkout_sync = self._actions_for("checkout.sync")
checkout_sync._sync_checkout(lines, location_dest)
super()._set_destination_on_lines(lines, location_dest)

def set_destination_and_unload_lines(self, lines, location_dest, unload=False):
checkout_sync = self._actions_for("checkout.sync")
all_lines = checkout_sync._all_lines_to_lock(lines)
super().set_destination_and_unload_lines(all_lines, location_dest, unload)
1 change: 1 addition & 0 deletions shopfloor_checkout_sync/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
* `Trobz <https://trobz.com>`_:
* Michael Tietz (MT Software) <mtietz@mt-software.de>
3 changes: 0 additions & 3 deletions shopfloor_checkout_sync/services/__init__.py

This file was deleted.

19 changes: 0 additions & 19 deletions shopfloor_checkout_sync/services/cluster_picking.py

This file was deleted.

19 changes: 0 additions & 19 deletions shopfloor_checkout_sync/services/location_content_transfer.py

This file was deleted.

19 changes: 0 additions & 19 deletions shopfloor_checkout_sync/services/zone_picking.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@

from odoo.addons.base_rest.components.service import to_int
from odoo.addons.component.core import Component
from odoo.addons.component.exception import NoComponentError
from odoo.addons.shopfloor.utils import to_float

_logger = logging.getLogger("shopfloor.services.single_product_transfer")
@@ -612,31 +611,9 @@ def _set_quantity__check_location(
move_line, message=message, asking_confirmation=confirmation or None
)

def _lock_lines(self, lines):
self._actions_for("lock").for_update(lines)

def _write_destination_on_lines(self, lines, location):
# TODO
# '_write_destination_on_lines' is implemented in:
#
# - 'location_content_transfer'
# - 'zone_picking'
# - 'cluster_picking' (but it is called '_unload_write_destination_on_lines')
#
# And all of them has a different implementation,
# To refactor later.
try:
# TODO lose dependency on 'shopfloor_checkout_sync' to avoid having
# yet another glue module. In the long term we should make
# 'shopfloor_checkout_sync' use events and trash the overrides made
# on all scenarios.
checkout_sync = self._actions_for("checkout.sync")
except NoComponentError:
self._lock_lines(lines)
else:
self._lock_lines(checkout_sync._all_lines_to_lock(lines))
checkout_sync._sync_checkout(lines, location)
lines.location_dest_id = location
stock = self._actions_for("stock")
stock.set_destination_and_unload_lines(lines, location)

def _set_quantity__post_move(self, move_line, location, confirmation=None):
# TODO qty_done = 0: transfer_no_qty_done
@@ -864,7 +841,7 @@ def set_quantity(self, selected_line_id, barcode, quantity, confirmation=None):
# TODO Should probably return to scan_product or scan_location?
return self._response_for_set_quantity(move_line)

self._lock_lines(move_line)
self._actions_for("stock")._lock_lines(move_line)
if move_line.state == "done":
message = self.msg_store.move_already_done()
return self._response_for_set_quantity(move_line, message=message)
24 changes: 23 additions & 1 deletion shopfloor_single_product_transfer/tests/test_set_quantity.py
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ class TestSetQuantity(CommonCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.location = cls.location_src
cls.location = cls.env.ref("stock.stock_location_components")
cls.product = cls.product_a
cls.packaging = cls.product_a_packaging
cls.packaging.qty = 5
@@ -429,6 +429,10 @@ def test_set_quantity_scan_packaging_with_allow_move_create(self):
self.assert_response(
response, next_state="select_product", message=expected_message, data=data
)
self.assertEqual(move_line.location_dest_id, self.dispatch_location)
self.assertEqual(move_line.location_id, location)
self.assertEqual(move_line.move_id.location_dest_id, self.dispatch_location)
self.assertEqual(move_line.move_id.location_id, location)

def test_set_quantity_scan_packaging_with_allow_move_create_and_no_prefill_qty(
self,
@@ -725,6 +729,15 @@ def test_set_quantity_scan_location(self):
self.assertFalse(picking.move_line_ids.result_package_id)
self.assertEqual(picking.user_id.id, False)
self.assertEqual(picking.move_line_ids.shopfloor_user_id.id, False)
self.assertEqual(picking.move_line_ids.location_dest_id, self.dispatch_location)
self.assertEqual(picking.move_line_ids.location_id, self.location)
self.assertEqual(
picking.move_line_ids.move_id.location_dest_id, self.dispatch_location
)
self.assertEqual(
picking.move_line_ids.move_id.location_id,
self.picking_type.default_location_src_id,
)

def test_set_quantity_scan_location_allow_move_create(self):
self.menu.sudo().allow_move_create = True
@@ -754,6 +767,15 @@ def test_set_quantity_scan_location_allow_move_create(self):
self.assertFalse(backorder)
self.assertEqual(picking.move_line_ids.qty_done, 6.0)
self.assertEqual(picking.move_line_ids.state, "done")
self.assertEqual(picking.move_line_ids.location_dest_id, self.dispatch_location)
self.assertEqual(picking.move_line_ids.location_id, self.location)
self.assertEqual(
picking.move_line_ids.move_id.location_dest_id, self.dispatch_location
)
self.assertEqual(
picking.move_line_ids.move_id.location_id,
self.picking_type.default_location_src_id,
)

def test_set_quantity_scan_package_not_empty(self):
# We scan a package that's not empty