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

[FIX] shopfloor_reception: Unmark as picked on partial picking #118

3 changes: 2 additions & 1 deletion shopfloor_reception/data/shopfloor_scenario_data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
<field name="options_edit">
{
"auto_post_line": true,
"allow_return": true
"allow_return": true,
"allow_select_document_by_product": true
}
</field>
</record>
Expand Down
1 change: 1 addition & 0 deletions shopfloor_reception/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import stock_picking
from . import shopfloor_menu
28 changes: 28 additions & 0 deletions shopfloor_reception/models/shopfloor_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright 2023 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

from odoo import api, fields, models

ALLOW_SELECT_DOCUMENT_BY_PRODUCT_HELP = """
If enabled, users will be able to select transfers by product.
"""


class ShopfloorMenu(models.Model):
_inherit = "shopfloor.menu"

allow_select_document_by_product = fields.Boolean(
string="Allow select document by product",
default=False,
help=ALLOW_SELECT_DOCUMENT_BY_PRODUCT_HELP,
)
allow_select_document_by_product_is_possible = fields.Boolean(
compute="_compute_pick_pack_same_time_is_possible"
)

@api.depends("scenario_id")
def _compute_pick_pack_same_time_is_possible(self):
for menu in self:
menu.allow_select_document_by_product_is_possible = (
menu.scenario_id.has_option("allow_select_document_by_product")
)
101 changes: 81 additions & 20 deletions shopfloor_reception/services/reception.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,13 +256,8 @@ def _scan_line__find_or_create_line(self, picking, move, qty_done=1):
)
)
)
if line:
# The line quantity to do needs to correspond to
# the remaining quantity to do of its move.
line.product_uom_qty = move.product_uom_qty - move.quantity_done
else:
qty_todo_remaining = max(0, move.product_uom_qty - move.quantity_done)
values = move._prepare_move_line_vals(quantity=qty_todo_remaining)
if not line:
values = move._prepare_move_line_vals()
line = self.env["stock.move.line"].create(values)
return self._scan_line__assign_user(picking, line, qty_done)

Expand Down Expand Up @@ -749,7 +744,7 @@ def _response_for_set_lot(self, picking, line, message=None):
message=message,
)

def _align_product_uom_qties(self, move):
def _align_display_product_uom_qty(self, line, response):
# This method aligns product uom qties on move lines.
# In the shopfloor context, we might have multiple users working at
# the same time on the same move. This is done by creating one move line
Expand All @@ -768,25 +763,32 @@ def _align_product_uom_qties(self, move):
# If move is already done, do not update lines qties
# if move.state in ("done", "cancel"):
# return

move = line.move_id
qty_todo = move.product_uom_qty
qty_done = sum(move.move_line_ids.mapped("qty_done"))
qty_done = 0.0
move_uom = move.product_uom
for move_line in move.move_line_ids:
# Use move's uom
qty_done += move_uom._compute_quantity(
move_line.qty_done, move_line.product_uom_id, round=False
)
rounding = move.product_id.uom_id.rounding
compare = float_compare(qty_done, qty_todo, precision_rounding=rounding)
if compare < 1: # If qty done <= qty todo, align qty todo on move lines
if compare < 1: # If qty done < qty todo, align qty todo in the response
remaining_todo = qty_todo - qty_done
# if we didn't bypass reservation update, the quant reservation
# would be reduced as much as the deduced quantity, which is wrong
# as we only moved the quantity to a new move line
lines = move.move_line_ids.with_context(bypass_reservation_update=True)
for line in lines:
line.product_uom_qty = line.qty_done + remaining_todo
# Change back to line uom
line_todo = line.product_uom_id._compute_quantity(
line.qty_done + remaining_todo, move_uom, round=False
)
response["data"]["set_quantity"]["selected_move_line"][0][
"quantity"
] = line_todo
return response

def _response_for_set_quantity(
self, picking, line, message=None, asking_confirmation=False
):
self._align_product_uom_qties(line.move_id)
return self._response(
response = self._response(
next_state="set_quantity",
data={
"selected_move_line": self._data_for_move_lines(line),
Expand All @@ -795,6 +797,7 @@ def _response_for_set_quantity(
},
message=message,
)
return self._align_display_product_uom_qty(line, response)

def _response_for_set_destination(self, picking, line, message=None):
return self._response(
Expand Down Expand Up @@ -839,7 +842,12 @@ def start(self):
def _scan_document__get_handlers_by_type(self):
return {
"picking": self._scan_document__by_picking,
"product": self._scan_document__by_product,
# only add the handler if allow_select_document_by_product is enabled
"product": (
self._scan_document__by_product
if self.work.menu.allow_select_document_by_product
else None
),
"packaging": self._scan_document__by_packaging,
"lot": self._scan_document__by_lot,
"origin_move": self._scan_document__by_origin_move,
Expand Down Expand Up @@ -1147,6 +1155,22 @@ def set_quantity(
)
return self._response_for_set_quantity(picking, selected_line)

def set_quantity__cancel_action(self, picking_id, selected_line_id):
picking = self.env["stock.picking"].browse(picking_id)
selected_line = self.env["stock.move.line"].browse(selected_line_id)
message = self._check_picking_status(picking)
if message:
return self._response_for_set_quantity(
picking, selected_line, message=message
)
if selected_line.exists():
if selected_line.product_uom_qty:
stock = self._actions_for("stock")
stock.unmark_move_line_as_picked(selected_line)
else:
selected_line.unlink()
return self._response_for_select_move(picking)

def _set_quantity__process__set_qty_and_split(self, picking, line, quantity):
move = line.move_id
sum(move.move_line_ids.mapped("qty_done"))
Expand Down Expand Up @@ -1216,6 +1240,7 @@ def process_without_pack(self, picking_id, selected_line_id, quantity):
return self._response_for_set_destination(picking, selected_line)

def _post_line(self, selected_line):
selected_line.product_uom_qty = selected_line.qty_done
if (
selected_line.picking_id.is_shopfloor_created
and self.work.menu.allow_return
Expand All @@ -1237,12 +1262,20 @@ def _post_shopfloor_created_line(self, selected_line):
)

def _auto_post_line(self, selected_line):
# If user only processed 1/5 and is the only one working on the move,
# then selected_line is the only one related to this move.
# In such case, we must ensure there's another move line with the remaining
# quantity to do, so selected_line is extracted in a new move as expected.
new_move_line = selected_line._split_partial_quantity()
new_move = selected_line.move_id.split_other_move_lines(
selected_line, intersection=True
)
if new_move:
# A new move is created in case of partial quantity
new_move.extract_and_action_done()
stock = self._actions_for("stock")
stock.unmark_move_line_as_picked(new_move_line)
new_move_line.location_dest_id = new_move_line.move_id.location_dest_id
return
# In case of full quantity, post the initial move
selected_line.move_id.extract_and_action_done()
Expand Down Expand Up @@ -1411,6 +1444,16 @@ def set_quantity(self):
"confirmation": {"type": "boolean"},
}

def set_quantity__cancel_action(self):
return {
"picking_id": {"coerce": to_int, "required": True, "type": "integer"},
"selected_line_id": {
"coerce": to_int,
"type": "integer",
"required": True,
},
}

def process_with_existing_pack(self):
return {
"picking_id": {"coerce": to_int, "required": True, "type": "integer"},
Expand Down Expand Up @@ -1542,6 +1585,9 @@ def _set_lot_next_states(self):
def _set_quantity_next_states(self):
return {"set_quantity", "select_move", "set_destination"}

def _set_quantity__cancel_action_next_states(self):
return {"set_quantity", "select_move"}

def _set_destination_next_states(self):
return {"set_destination", "select_move"}

Expand Down Expand Up @@ -1622,6 +1668,16 @@ def _schema_set_quantity(self):
},
}

@property
def _schema_set_quantity__cancel_action(self):
return {
"selected_move_line": {
"type": "list",
"schema": {"type": "dict", "schema": self.schemas.move_line()},
},
"picking": {"type": "dict", "schema": self.schemas.picking()},
}

@property
def _schema_set_destination(self):
return {
Expand Down Expand Up @@ -1699,6 +1755,11 @@ def set_lot_confirm_action(self):
def set_quantity(self):
return self._response_schema(next_states=self._set_quantity_next_states())

def set_quantity__cancel_action(self):
return self._response_schema(
next_states=self._set_quantity__cancel_action_next_states()
)

def process_with_existing_pack(self):
return self._response_schema(
next_states=self._process_with_existing_pack_next_states()
Expand Down
32 changes: 32 additions & 0 deletions shopfloor_reception/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def setUpClassVars(cls, *args, **kwargs):
cls.profile = cls.env.ref("shopfloor.profile_demo_1")
cls.picking_type = cls.menu.picking_type_ids
cls.wh = cls.picking_type.warehouse_id
cls._enable_allow_select_document_by_product()

def _data_for_move_lines(self, lines, **kw):
return self.data.move_lines(lines, **kw)
Expand Down Expand Up @@ -138,3 +139,34 @@ def _get_today_pickings(self):
],
order="scheduled_date ASC",
)

@classmethod
def _enable_allow_select_document_by_product(cls):
cls.menu.sudo().allow_select_document_by_product = True

def assertMessage(self, response, expected_message):
message = response.get("message")
for key, value in expected_message.items():
self.assertEqual(message.get(key), value)

@classmethod
def _get_move_ids_from_response(cls, response):
state = response.get("next_state")
data = response["data"][state]
picking_data = data.get("pickings") or [data.get("picking")]
moves_data = []
for picking in picking_data:
moves_data.extend(picking["moves"])
return [move["id"] for move in moves_data]

def _get_service_for_user(self, user):
user_env = self.env(user=user)
return self.get_service(
"reception", menu=self.menu, profile=self.profile, env=user_env
)

@classmethod
def _shopfloor_manager_values(cls):
vals = super()._shopfloor_manager_values()
vals["groups_id"] = [(6, 0, [cls.env.ref("stock.group_stock_user").id])]
return vals
7 changes: 5 additions & 2 deletions shopfloor_reception/tests/test_return_scan_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,18 @@ def test_scan_packaging_in_delivery(self):
"barcode": self.product_a_packaging.barcode,
},
)
data = self.data.picking(return_picking)
selected_move_line = self.get_new_move_lines()
move_line_data = self.data.move_lines(selected_move_line)
move_line_data[0]["quantity"] = 20.0
# Displayed qtu todo is modified by _align_display_product_uom_qty
data = self.data.picking(return_picking)
self.assert_response(
response,
next_state="set_quantity",
data={
"confirmation_required": False,
"picking": data,
"selected_move_line": self.data.move_lines(selected_move_line),
"selected_move_line": move_line_data,
},
)
self.assertEqual(selected_move_line.qty_done, self.product_a_packaging.qty)
21 changes: 17 additions & 4 deletions shopfloor_reception/tests/test_select_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,20 @@ def test_assign_shopfloor_user_to_line(self):
self.assertEqual(other_move_line.shopfloor_user_id.id, False)

def test_create_new_line_none_available(self):
# If all lines for a product are already assigned to a different user
# and there's still qty todo remaining
# a new line will be created for that qty todo.
# If there's already a move line for a given incoming move,
# we assigned the whole move's product_uom_qty to it.
# The reason for that is that when recomputing states for a given move
# if sum(move.move_line_ids.product_uom_qty) != move.product_uom_qty,
# then it's state won't be assigned.
# For instance:
# - user 1 selects line1
# - user 2 selected line1 too
# - user 1 posts 20/40 goods
# - user 2 tries to process any qty, and it fails, because posting
# a move triggers the recompute of move's state
# To avoid that, the first created line gets
# product_uom_qty = move.product_uom_qty
# The next ones are getting 0.
picking = self._create_picking()
self.assertEqual(len(picking.move_line_ids), 2)
selected_move_line = picking.move_line_ids.filtered(
Expand All @@ -233,9 +244,11 @@ def test_create_new_line_none_available(self):
"barcode": self.product_a.barcode,
},
)
# A new line has been created
self.assertEqual(len(picking.move_line_ids), 3)
created_line = picking.move_line_ids[2]
self.assertEqual(created_line.product_uom_qty, 7)
# And its product_uom_qty is 0
self.assertEqual(created_line.product_uom_qty, 0.0)
self.assertEqual(created_line.shopfloor_user_id.id, self.env.uid)

def test_done_action(self):
Expand Down
Loading