diff --git a/shopfloor/actions/message.py b/shopfloor/actions/message.py index 5835e401d86..7d4bfc09f3e 100644 --- a/shopfloor/actions/message.py +++ b/shopfloor/actions/message.py @@ -651,12 +651,17 @@ def location_content_transfer_complete(self, location_src, location_dest): ), } - def location_content_unable_to_transfer(self, location_dest): + def location_content_unable_to_transfer(self, move, location, location_dest): + message = _( + "The content of %(location)s cannot be transferred to " + "%(location_dest)s with this scenario for product %(product_name)s.", + location=location.name, + location_dest=location_dest.name, + product_name=move.product_id.display_name, + ) return { "message_type": "error", - "body": _( - "The content of {} cannot be transferred with this scenario." - ).format(location_dest.name), + "body": message, } def product_in_multiple_sublocation(self, product): diff --git a/shopfloor/services/location_content_transfer.py b/shopfloor/services/location_content_transfer.py index 7abffd7cded..182645059e5 100644 --- a/shopfloor/services/location_content_transfer.py +++ b/shopfloor/services/location_content_transfer.py @@ -386,12 +386,15 @@ def scan_location(self, barcode): # noqa: C901 move_lines = new_moves.move_line_ids for line in move_lines: if not self.is_dest_location_valid(line.move_id, line.location_dest_id): - savepoint.rollback() - return self._response_for_start( - message=self.msg_store.location_content_unable_to_transfer( - location + move = line.move_id + location_dest = line.location_id + self.msg_store.add_message( + **self.msg_store.location_content_unable_to_transfer( + move, location, location_dest ) ) + savepoint.rollback() + return self._response_for_start() stock = self._actions_for("stock") if self.work.menu.ignore_no_putaway_available and stock.no_putaway_available( diff --git a/shopfloor/tests/test_location_content_transfer_putaway.py b/shopfloor/tests/test_location_content_transfer_putaway.py index fb073f8bbc8..90326b89649 100644 --- a/shopfloor/tests/test_location_content_transfer_putaway.py +++ b/shopfloor/tests/test_location_content_transfer_putaway.py @@ -129,15 +129,19 @@ def test_putaway_move_dest_not_child_of_picking_type_dest(self): response = self.service.dispatch( "scan_location", params={"barcode": self.test_loc.barcode} ) + current_moves = self.env["stock.move"].search( + [("location_id", "=", self.test_loc.id), ("state", "=", "assigned")] + ) + self.assertEqual(existing_moves, current_moves) + + message = { + "message_type": "error", + "body": "The content of test cannot be transferred to test with " + "this scenario for product [A] Product A.", + } self.assert_response( response, next_state="scan_location", data=self.ANY, - message=self.service.msg_store.location_content_unable_to_transfer( - self.test_loc - ), + message=message, ) - current_moves = self.env["stock.move"].search( - [("location_id", "=", self.test_loc.id), ("state", "=", "assigned")] - ) - self.assertEqual(existing_moves, current_moves) diff --git a/shopfloor_base/actions/message.py b/shopfloor_base/actions/message.py index 4c8e111772f..706945699c3 100644 --- a/shopfloor_base/actions/message.py +++ b/shopfloor_base/actions/message.py @@ -1,9 +1,17 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + from odoo import _ from odoo.addons.component.core import Component +MESSAGE_TYPES = { + "info": 0, + "success": 1, + "warning": 2, + "error": 3, +} + class MessageAction(Component): """Provide message templates @@ -19,6 +27,24 @@ class MessageAction(Component): _inherit = "shopfloor.process.action" _usage = "message" + @property + def message_queue(self): + if not hasattr(self, "_message_queue"): + self._message_queue = [] + return self._message_queue + + def add_message(self, body, message_type="info"): + if not isinstance(body, str): + raise TypeError("You should set a string to message queue!") + if message_type not in MESSAGE_TYPES.keys(): + raise TypeError("You should use a correct Shopfloor message type!") + if not hasattr(self, "_message_queue"): + self._message_queue = [] + self._message_queue.append(ShopfloorMessage(body, message_type)) + + def clear_queue(self): + self._message_queue = list() + def generic_record_not_found(self): return { "message_type": "error", @@ -46,3 +72,13 @@ def generic_record_not_found(self): # then all depending modules can simply create records they need # instea of overriding and polluting the component. # Additional goodie: users can edit messages via UI. + + +class ShopfloorMessage: + + body = str() + message_type = str() + + def __init__(self, body, message_type, **kwargs): + self.body = body + self.message_type = message_type diff --git a/shopfloor_base/services/service.py b/shopfloor_base/services/service.py index e287764e6ed..059bfed1eb1 100644 --- a/shopfloor_base/services/service.py +++ b/shopfloor_base/services/service.py @@ -12,6 +12,7 @@ from odoo.addons.component.core import AbstractComponent from ..actions.base_action import get_actions_for +from ..actions.message import MESSAGE_TYPES from ..apispec.service_apispec import ShopfloorRestServiceAPISpec @@ -33,6 +34,10 @@ def _profile(self): # User private attributes to not mess up w/ public endpoints return getattr(self.work, "profile", self.env["shopfloor.profile"]) + def __init__(self, work_context): + super().__init__(work_context=work_context) + self._msg_store = False + def _get_api_spec(self, **params): return ShopfloorRestServiceAPISpec(self, **params) @@ -75,6 +80,45 @@ def _to_json(self, records): res.append(self._convert_one_record(record)) return res + def _get_message_type(self, message): + """ + TODO: To be removed if we change the API to support multiple messages + (and types) per result. + """ + message_type = "info" + if message and "message_type" in message: + message_type = message["message_type"] + for message_element in self.msg_store.message_queue: + message_type = ( + message_element.message_type + if MESSAGE_TYPES[message_element.message_type] + > MESSAGE_TYPES[message_type] + else message_type + ) + return message_type + + def _get_message_body(self, message) -> str: + body = "" + if "body" in message: + body = message["body"] + if self.msg_store and self.msg_store.message_queue: + body += "\n".join(element.body for element in self.msg_store.message_queue) + return body + + def _get_message(self, message=None) -> dict: + """ + This will combine the message contained in response and the + messages contained in the message queue + """ + if message is None: + message = {} + message_type = self._get_message_type(message) + body = self._get_message_body(message) + if message_type and body: + message["message_type"] = message_type + message["body"] = body + return message + def _response( self, base_response=None, data=None, next_state=None, message=None, popup=None ): @@ -117,8 +161,9 @@ def _response( elif data: response["data"] = data - if message: + if message := self._get_message(message): response["message"] = message + self.msg_store.clear_queue() if popup: response["popup"] = popup @@ -201,7 +246,14 @@ def schema_detail(self): @property def msg_store(self): - return self._actions_for("message") + if self._msg_store: + return self._msg_store + self._msg_store = self._actions_for("message") + return self._msg_store + + @msg_store.setter + def _set_msg_store(self, store): + self._msg_store = store # TODO: maybe to be proposed to base_rest # TODO: add tests diff --git a/shopfloor_base/tests/common.py b/shopfloor_base/tests/common.py index 8a888412886..c24f507268b 100644 --- a/shopfloor_base/tests/common.py +++ b/shopfloor_base/tests/common.py @@ -119,10 +119,15 @@ def setUpClass(cls): with cls.work_on_actions(cls) as work: cls.data = work.component(usage="data") cls.data_detail = work.component(usage="data_detail") - cls.msg_store = work.component(usage="message") + # cls.msg_store = work.component(usage="message") cls.schema = work.component(usage="schema") cls.schema_detail = work.component(usage="schema_detail") + @property + def msg_store(self): + with self.work_on_actions() as work: + return work.component(usage="message") + @classmethod def setUpClassUsers(cls): Users = cls.env["res.users"].with_context(