From a0aff5bda250fe962e5424f2bb2625ebab66391b Mon Sep 17 00:00:00 2001 From: "David Monnom (moda)" Date: Tue, 30 May 2023 13:32:56 +0000 Subject: [PATCH] [IMP] pos_self_order: refacto, models and serverside price calculation General refactoring of the codebase. Inline of certain components that had no logic in the JS side, the XML of these components was directly added to the place where they were called. Simplification of the python controller, inline of many functions and separation of the logic. There is now one function that manages new orders and another that manages and updates existing orders if we are in "pay after meal" mode. In "pay after each" mode, orders cannot be edited. Addition of different models, product, order and orderlines in order to facilitate the use of these objects between the backend and the frontend. Added price calculation on the serverside because we can't trust the data coming from the customer in the self since no identification is required. Management of last change sent added. Thanks to this, users will no longer be able to click on the order button if no changes have been made to the order. closes odoo/odoo#122901 Signed-off-by: Heinz Robin (rhe) --- .../data/pos_restaurant_demo.xml | 1 - addons/pos_self_order/__manifest__.py | 9 +- addons/pos_self_order/controllers/__init__.py | 4 +- .../controllers/{qr_menu.py => entry.py} | 65 +-- addons/pos_self_order/controllers/ordering.py | 464 ------------------ addons/pos_self_order/controllers/orders.py | 211 ++++++++ addons/pos_self_order/controllers/utils.py | 6 +- .../pos_self_order/data/custom_link_data.xml | 9 - .../pos_self_order/demo/custom_link_data.xml | 10 + .../demo/pos_restaurant_demo.xml | 8 +- addons/pos_self_order/models/pos_config.py | 29 +- addons/pos_self_order/models/pos_order.py | 63 +-- .../pos_self_order/models/pos_restaurant.py | 1 + .../pos_self_order/models/product_product.py | 1 + .../models/res_config_settings.py | 6 +- .../IncrementCounter/IncrementCounter.js | 8 - .../IncrementCounter/IncrementCounter.scss | 4 - .../IncrementCounter/IncrementCounter.xml | 14 - .../static/src/Components/Loading/Loading.js | 8 - .../static/src/Components/Loading/Loading.xml | 10 - .../src/Components/MainButton/MainButton.js | 8 - .../src/Components/MainButton/MainButton.xml | 11 - .../static/src/Components/Order/Order.js | 15 - .../static/src/Components/Order/Order.xml | 23 - .../src/Components/OrderLines/OrderLines.js | 19 - .../src/Components/OrderLines/OrderLines.xml | 11 - .../src/Components/ProductCard/ProductCard.js | 55 --- .../Components/ProductCard/ProductCard.xml | 59 --- .../src/Components/TagSelector/TagSelector.js | 32 -- .../Components/TagSelector/TagSelector.xml | 16 - .../static/src/Hooks/useDetection.js | 93 ---- .../static/src/Pages/CartView/CartView.js | 26 - .../static/src/Pages/CartView/CartView.xml | 21 - .../src/Pages/LandingPage/LandingPage.xml | 48 -- .../static/src/Pages/OrdersView/OrdersView.js | 16 - .../src/Pages/OrdersView/OrdersView.xml | 16 - .../src/Pages/ProductList/ProductList.xml | 105 ---- .../Pages/ProductMainView/ProductMainView.js | 97 ---- .../Pages/ProductMainView/ProductMainView.xml | 120 ----- .../static/src/SelfOrderRoot.xml | 24 - .../static/src/SelfOrderService.js | 402 --------------- .../static/src/components/lines/lines.js | 40 ++ .../static/src/components/lines/lines.xml | 36 ++ .../NavBar.js => components/navbar/navbar.js} | 6 +- .../navbar/navbar.scss} | 0 .../navbar/navbar.xml} | 6 +- .../price_details/price_details.js} | 2 +- .../price_details/price_details.xml} | 9 +- .../components/product_card/product_card.js | 56 +++ .../product_card/product_card.scss} | 0 .../components/product_card/product_card.xml | 35 ++ .../static/src/hooks/use_detection.js | 60 +++ .../use_scroll_direction.js} | 0 .../pos_self_order/static/src/models/line.js | 33 ++ .../pos_self_order/static/src/models/order.js | 79 +++ .../static/src/models/product.js | 42 ++ .../landing_page/landing_page.js} | 4 +- .../src/pages/landing_page/landing_page.xml | 43 ++ .../static/src/pages/order_cart/order_cart.js | 34 ++ .../src/pages/order_cart/order_cart.xml | 23 + .../pages/orders_history/orders_history.js | 44 ++ .../pages/orders_history/orders_history.xml | 40 ++ .../product_list/product_list.js} | 45 +- .../src/pages/product_list/product_list.xml | 104 ++++ .../product_main_view/product_main_view.js | 143 ++++++ .../product_main_view/product_main_view.scss | 4 + .../product_main_view/product_main_view.xml | 114 +++++ .../static/src/pos_self_order_styles.scss | 6 +- addons/pos_self_order/static/src/router.js | 82 ++-- .../static/src/self_order_bus_service.js | 50 ++ .../{SelfOrderRoot.js => self_order_root.js} | 22 +- .../static/src/self_order_root.xml | 23 + .../static/src/self_order_router_service.js | 67 +++ .../static/src/self_order_service.js | 228 +++++++++ addons/pos_self_order/static/src/utils.js | 14 + .../static/tests/tours/pos_qr_menu_tour.js | 141 ------ .../static/tests/tours/pos_self_order_tour.js | 64 --- .../tours/self_order_after_each_cart_tour.js | 82 ++++ .../tours/self_order_after_meal_cart_tour.js | 88 ++++ .../self_order_after_meal_product_tour.js | 25 + .../tests/tours/self_order_menu_only_tour.js | 20 + .../static/tests/tours/tour_utils.js | 221 ++++++--- addons/pos_self_order/tests/test_frontend.py | 43 +- .../views/custom_link_views.xml | 4 +- addons/pos_self_order/views/qr_code.xml | 29 +- .../views/res_config_settings_views.xml | 2 +- 86 files changed, 2125 insertions(+), 2236 deletions(-) rename addons/pos_self_order/controllers/{qr_menu.py => entry.py} (67%) delete mode 100644 addons/pos_self_order/controllers/ordering.py create mode 100644 addons/pos_self_order/controllers/orders.py delete mode 100644 addons/pos_self_order/data/custom_link_data.xml create mode 100644 addons/pos_self_order/demo/custom_link_data.xml delete mode 100644 addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.js delete mode 100644 addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.scss delete mode 100644 addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.xml delete mode 100644 addons/pos_self_order/static/src/Components/Loading/Loading.js delete mode 100644 addons/pos_self_order/static/src/Components/Loading/Loading.xml delete mode 100644 addons/pos_self_order/static/src/Components/MainButton/MainButton.js delete mode 100644 addons/pos_self_order/static/src/Components/MainButton/MainButton.xml delete mode 100644 addons/pos_self_order/static/src/Components/Order/Order.js delete mode 100644 addons/pos_self_order/static/src/Components/Order/Order.xml delete mode 100644 addons/pos_self_order/static/src/Components/OrderLines/OrderLines.js delete mode 100644 addons/pos_self_order/static/src/Components/OrderLines/OrderLines.xml delete mode 100644 addons/pos_self_order/static/src/Components/ProductCard/ProductCard.js delete mode 100644 addons/pos_self_order/static/src/Components/ProductCard/ProductCard.xml delete mode 100644 addons/pos_self_order/static/src/Components/TagSelector/TagSelector.js delete mode 100644 addons/pos_self_order/static/src/Components/TagSelector/TagSelector.xml delete mode 100644 addons/pos_self_order/static/src/Hooks/useDetection.js delete mode 100644 addons/pos_self_order/static/src/Pages/CartView/CartView.js delete mode 100644 addons/pos_self_order/static/src/Pages/CartView/CartView.xml delete mode 100644 addons/pos_self_order/static/src/Pages/LandingPage/LandingPage.xml delete mode 100644 addons/pos_self_order/static/src/Pages/OrdersView/OrdersView.js delete mode 100644 addons/pos_self_order/static/src/Pages/OrdersView/OrdersView.xml delete mode 100644 addons/pos_self_order/static/src/Pages/ProductList/ProductList.xml delete mode 100644 addons/pos_self_order/static/src/Pages/ProductMainView/ProductMainView.js delete mode 100644 addons/pos_self_order/static/src/Pages/ProductMainView/ProductMainView.xml delete mode 100644 addons/pos_self_order/static/src/SelfOrderRoot.xml delete mode 100644 addons/pos_self_order/static/src/SelfOrderService.js create mode 100644 addons/pos_self_order/static/src/components/lines/lines.js create mode 100644 addons/pos_self_order/static/src/components/lines/lines.xml rename addons/pos_self_order/static/src/{Components/NavBar/NavBar.js => components/navbar/navbar.js} (70%) rename addons/pos_self_order/static/src/{Components/NavBar/NavBar.scss => components/navbar/navbar.scss} (100%) rename addons/pos_self_order/static/src/{Components/NavBar/NavBar.xml => components/navbar/navbar.xml} (77%) rename addons/pos_self_order/static/src/{Components/PriceDetails/PriceDetails.js => components/price_details/price_details.js} (80%) rename addons/pos_self_order/static/src/{Components/PriceDetails/PriceDetails.xml => components/price_details/price_details.xml} (53%) create mode 100644 addons/pos_self_order/static/src/components/product_card/product_card.js rename addons/pos_self_order/static/src/{Components/ProductCard/ProductCard.scss => components/product_card/product_card.scss} (100%) create mode 100644 addons/pos_self_order/static/src/components/product_card/product_card.xml create mode 100644 addons/pos_self_order/static/src/hooks/use_detection.js rename addons/pos_self_order/static/src/{Hooks/useScrollDirection.js => hooks/use_scroll_direction.js} (100%) create mode 100644 addons/pos_self_order/static/src/models/line.js create mode 100644 addons/pos_self_order/static/src/models/order.js create mode 100644 addons/pos_self_order/static/src/models/product.js rename addons/pos_self_order/static/src/{Pages/LandingPage/LandingPage.js => pages/landing_page/landing_page.js} (70%) create mode 100644 addons/pos_self_order/static/src/pages/landing_page/landing_page.xml create mode 100644 addons/pos_self_order/static/src/pages/order_cart/order_cart.js create mode 100644 addons/pos_self_order/static/src/pages/order_cart/order_cart.xml create mode 100644 addons/pos_self_order/static/src/pages/orders_history/orders_history.js create mode 100644 addons/pos_self_order/static/src/pages/orders_history/orders_history.xml rename addons/pos_self_order/static/src/{Pages/ProductList/ProductList.js => pages/product_list/product_list.js} (76%) create mode 100644 addons/pos_self_order/static/src/pages/product_list/product_list.xml create mode 100644 addons/pos_self_order/static/src/pages/product_main_view/product_main_view.js create mode 100644 addons/pos_self_order/static/src/pages/product_main_view/product_main_view.scss create mode 100644 addons/pos_self_order/static/src/pages/product_main_view/product_main_view.xml create mode 100644 addons/pos_self_order/static/src/self_order_bus_service.js rename addons/pos_self_order/static/src/{SelfOrderRoot.js => self_order_root.js} (70%) create mode 100644 addons/pos_self_order/static/src/self_order_root.xml create mode 100644 addons/pos_self_order/static/src/self_order_router_service.js create mode 100644 addons/pos_self_order/static/src/self_order_service.js create mode 100644 addons/pos_self_order/static/src/utils.js delete mode 100644 addons/pos_self_order/static/tests/tours/pos_qr_menu_tour.js delete mode 100644 addons/pos_self_order/static/tests/tours/pos_self_order_tour.js create mode 100644 addons/pos_self_order/static/tests/tours/self_order_after_each_cart_tour.js create mode 100644 addons/pos_self_order/static/tests/tours/self_order_after_meal_cart_tour.js create mode 100644 addons/pos_self_order/static/tests/tours/self_order_after_meal_product_tour.js create mode 100644 addons/pos_self_order/static/tests/tours/self_order_menu_only_tour.js diff --git a/addons/pos_restaurant/data/pos_restaurant_demo.xml b/addons/pos_restaurant/data/pos_restaurant_demo.xml index b073911b094..e25964359eb 100644 --- a/addons/pos_restaurant/data/pos_restaurant_demo.xml +++ b/addons/pos_restaurant/data/pos_restaurant_demo.xml @@ -196,7 +196,6 @@ - Main Floor rgb(136,137,242) diff --git a/addons/pos_self_order/__manifest__.py b/addons/pos_self_order/__manifest__.py index 713db7c505d..cee81b3a640 100644 --- a/addons/pos_self_order/__manifest__.py +++ b/addons/pos_self_order/__manifest__.py @@ -9,13 +9,13 @@ "auto_install": ["pos_restaurant"], "demo": [ "demo/pos_restaurant_demo.xml", + "demo/custom_link_data.xml", ], "data": [ "security/ir.model.access.csv", "views/index.xml", "views/qr_code.xml", "views/custom_link_views.xml", - "data/custom_link_data.xml", "data/restaurant_table_data.xml", "views/res_config_settings_views.xml", "views/point_of_sale_dashboard.xml", @@ -35,6 +35,8 @@ "web/static/src/session.js", "web/static/src/core/utils/transitions.scss", "web/static/src/core/**/*", + ("remove", "web/static/src/core/browser/router_service.js"), + ("remove", "web/static/src/core/debug/**/*"), "web/static/lib/owl/owl.js", "web/static/lib/owl/odoo_module.js", "web/static/lib/luxon/luxon.js", @@ -43,6 +45,11 @@ "web/static/lib/odoo_ui_icons/*", "pos_self_order/static/src/**/*", "point_of_sale/static/src/utils.js", + # bus service + 'bus/static/src/services/bus_service.js', + 'bus/static/src/bus_parameters_service.js', + 'bus/static/src/multi_tab_service.js', + 'bus/static/src/workers/*', ], "pos_self_order.assets_tests": [ "web/static/lib/jquery/jquery.js", diff --git a/addons/pos_self_order/controllers/__init__.py b/addons/pos_self_order/controllers/__init__.py index 7d91bf1afc7..0aa6faafeb3 100644 --- a/addons/pos_self_order/controllers/__init__.py +++ b/addons/pos_self_order/controllers/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- -from . import qr_menu -from . import ordering +from . import entry +from . import orders diff --git a/addons/pos_self_order/controllers/qr_menu.py b/addons/pos_self_order/controllers/entry.py similarity index 67% rename from addons/pos_self_order/controllers/qr_menu.py rename to addons/pos_self_order/controllers/entry.py index 7601e573442..a065183677d 100644 --- a/addons/pos_self_order/controllers/qr_menu.py +++ b/addons/pos_self_order/controllers/entry.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, Optional +from typing import Optional import werkzeug from odoo import http @@ -11,8 +11,6 @@ from odoo.addons.pos_self_order.controllers.utils import ( get_any_pos_config_sudo, get_table_sudo, ) -from odoo.addons.pos_self_order.models.pos_config import PosConfig -from odoo.addons.pos_self_order.models.pos_restaurant import RestaurantTable class PosQRMenuController(http.Controller): @@ -30,41 +28,46 @@ class PosQRMenuController(http.Controller): return request.redirect(f"/menu/{get_any_pos_config_sudo().id}") @http.route( - [ - "/menu/", - "/menu//products", - "/menu//products/", - "/menu//cart", - "/menu//orders", - ], + [ + "/menu/", + "/menu//" + ], auth="public", website=True, sitemap=True, ) - def pos_self_order_start( - self, pos_name: str, at: Optional[str] = None, product_id: Optional[int] = None - ): + def pos_self_order_start(self, config_id: str, at=None): """ The user gets this route from the QR code that they scan at the table - :param pos_name: the name of the pos config: can be the id or the slugified name of the pos config. (e.g. "3" or "bar-3") + :param config_id: the name of the pos config: can be the id or the slugified name of the pos config. (e.g. "3" or "bar-3") :param at: the access token of the table; we call this argument "at" because - it will be displayed in the url ( as a query param ), and "at" is more user friendly than "table_access_token" + it will be displayed in the url ( as a query param ), and "at" is more user friendly than "access_token" the user is allowed to order only if this "at" matches the access token of a table :param product_id: the id of the product that the user wants to see the details of; we never actually use this argument in this function ( it will be read by the client side router ), but we still have it here, because otherwise we get a Warning in the logs :return: the rendered template """ + config_sudo = get_pos_config_sudo(config_id) + table_sudo = get_table_sudo(access_token=at) + self_order_mode = 'qr_code' + table_infos = False + + if config_sudo.has_active_session and config_sudo.self_order_table_mode and table_sudo: + table_infos = table_sudo._get_self_order_data() + self_order_mode = config_sudo.self_order_pay_after + return request.render( - "pos_self_order.index", + 'pos_self_order.index', { - "session_info": { + 'session_info': { **request.env["ir.http"].get_frontend_session_info(), - "currencies": request.env["ir.http"].get_currencies(), - "pos_self_order_data": self._get_self_order_config( - get_pos_config_sudo(pos_name), - get_table_sudo(table_access_token=at), - ), + 'currencies': request.env["ir.http"].get_currencies(), + 'pos_self_order_data': { + 'self_order_mode': self_order_mode, + 'table': table_infos, + **config_sudo._get_self_order_data(), + }, } }, ) @@ -116,21 +119,3 @@ class PosQRMenuController(http.Controller): ._get_image_stream_from(pos_config_sudo, field_name="self_order_image") .get_response() ) - - def _get_self_order_config( - self, pos_config_sudo: PosConfig, table_sudo: RestaurantTable - ) -> Dict: - """ - Returns the necessary information for the POS Self Order App - """ - return { - **pos_config_sudo._get_self_order_data(), - **( - table_sudo - and pos_config_sudo.has_active_session - and pos_config_sudo.self_order_table_mode - # having the table_id in the frontend means that the client will be able to order - and {"table": table_sudo._get_self_order_data()} - or {} - ), - } diff --git a/addons/pos_self_order/controllers/ordering.py b/addons/pos_self_order/controllers/ordering.py deleted file mode 100644 index 903fd58f571..00000000000 --- a/addons/pos_self_order/controllers/ordering.py +++ /dev/null @@ -1,464 +0,0 @@ -# -*- coding: utf-8 -*- - -from typing import List, Dict, Optional, Union -import werkzeug -import uuid - -from odoo import http, fields -from odoo.http import request -from odoo.tools import partition - - -from odoo.addons.pos_self_order.controllers.utils import ( - get_pos_config_sudo, - get_table_sudo, -) -from odoo.addons.pos_self_order.models.product_product import ProductProduct -from odoo.addons.pos_self_order.models.pos_order import PosOrder, PosOrderLine -from odoo.addons.pos_self_order.models.pos_config import PosConfig - - -class PosSelfOrderController(http.Controller): - @http.route( - "/pos-self-order/send-order", - auth="public", - type="json", - website=True, - ) - def pos_self_order_send_order( - self, - cart: List[Dict], - pos_config_id: int, - table_access_token: Optional[str] = None, - order_pos_reference: Optional[str] = None, - order_access_token: Optional[str] = None, - ) -> Dict[str, str]: - """ - There are 2 types of self order configurations: - 1. Order at table and pay after each order - ==> pos doesn't allow ongoing orders - 2. Order at table and pay at the end of the meal - ==> pos allows ongoing orders - (order at table means order from personal device (smartphone)) - In case 1. we will create a new order each time the user sends an order - In case 2. we will create a new order only if the user does not have an ongoing order - (we will look whether there is an order with state = 'draft' - with the same pos_reference and order_access_token as the ones that the user has provided) - :param cart: order lines - :param pos_config_id: the id of the POS where the order is being sent - :param table_access_token: the access token of the table where the order is being sent (UUID v4) - :param pos_reference: the id of the order that is being edited; ex: "Order 00001-001-0001" - :param order_access_token: the access token of the order that is being edited (UUID v4) - :return: dictionary with keys: pos_reference, order_access_token - """ - # we will only respond with the order "access_token" and "id". After clicking on the `Order` button, - # the customer will be redirected to the landing page. - # From the `landing page`, the customer can go to the `My Orders` page. - # on this page, the app will automatically make a request to the `view-order` route, - # so the customer will get the order details that way - - pos_config_sudo = get_pos_config_sudo(pos_config_id) - - if not pos_config_sudo.self_order_table_mode or not pos_config_sudo.has_active_session: - raise werkzeug.exceptions.BadRequest() - - order = self._form_order( - self._create_order_data( - cart, - pos_config_sudo, - table_access_token, - order_pos_reference, - order_access_token, - ) - ) - posted_order_id = ( - request.env["pos.order"] - .sudo() - .create_from_ui( - [order], - draft=True, - )[0] - .get("id") - ) - - return ( - request.env["pos.order"] - .sudo() - .browse(posted_order_id) - .read(["pos_reference", "access_token"])[0] - ) - - # # is_trusted is set to True by default. - # # We need to set it to False, because we are creating an order from a public route - # # FIXME: make it so we only set it to false if it is a new order - # # if the server already acknowledged the order, we should not set it to false again - # # every time the user adds a new item to the order - # request.env["pos.order"].sudo().browse(order_resp.get("id")).is_trusted = False - - @http.route("/pos-self-order/view-order", auth="public", type="json", website=True) - def pos_self_order_view_order(self, pos_reference: str, access_token: str) -> Dict: - """ - Return some information about a given order. - This is used by the frontend to find the latest state of an order. - (e.g. if a customer orders something from the self order app and then the waiter adds an item to the order, - the customer will be able to see the new item in the order) - :param pos_reference: the id of the order that we want to view - :param access_token: the access token of the order that we want to view -- this is needed - for security reasons, so that only the customer who created the order - can view it - """ - order_sudo = self._find_order(pos_reference, access_token) - if not order_sudo: - raise werkzeug.exceptions.Unauthorized() - return order_sudo._export_for_self_order() - - def _create_order_data( - self, - cart: List[Dict], - pos_config_sudo: PosConfig, - table_access_token: Optional[str], - order_pos_reference: Optional[str], - order_access_token: Optional[str], - ) -> Dict[str, Union[int, str, List[Dict]]]: - """ - Given the data, we decide whether we need to create a new order or update an existing one - :return: dictionary with the data needed to create a new order - """ - - # We only care about the previous order if the customer is paying after the meal - # ( paying after the meal means that the customer can order multiple times on the same order ) - order_sudo = pos_config_sudo.self_order_pay_after == "meal" and self._find_order( - order_pos_reference, order_access_token, state="draft" - ) - - return { - **( - order_sudo._get_self_order_data() - if order_sudo - else self._create_new_order_data(pos_config_sudo, table_access_token) - ), - "lines": self._create_orderlines( - self._merge_orderlines( - cart, order_sudo.lines.export_for_ui(), order_sudo.config_id - ) - if order_sudo - else cart, - pos_config_sudo, - ), - } - - def _create_new_order_data( - self, pos_config_sudo: PosConfig, table_access_token: str - ) -> Dict[str, Union[str, int]]: - """ - :param pos_config_id: the id of the pos config for which we want to create the order - :param table_access_token: the access token of the table for which we want to create the order - :return: a dictionary containing the data that we need to create a new order - """ - table_sudo = get_table_sudo(table_access_token) - pos_session_sudo = pos_config_sudo.current_session_id - if not table_sudo or not pos_session_sudo: - raise werkzeug.exceptions.Unauthorized() - - sequence_number = self._get_sequence_number(table_sudo.id, pos_session_sudo.id) - - return { - "id": self._generate_unique_id(pos_session_sudo.id, table_sudo.id, sequence_number), - "sequence_number": sequence_number, - "access_token": uuid.uuid4().hex, - "session_id": pos_session_sudo.id, - "table_id": table_sudo.id, - } - - def _merge_orderlines( - self, - incoming_orderlines: List[Dict], - existing_orderlines: List[Dict], - pos_config_sudo: PosConfig, - ) -> List[Dict]: - """ - If the customer has an existing order, we will add the items from the existing order to the current cart. - (This is because the create_from_ui method will overwrite the old items from the order) - :param cart: The cart from the frontend. - :return: list of dictionaries with the items from the cart and the items from the existing order - """ - has_merge_candidate = lambda item: self._has_merge_candidate(item, existing_orderlines) - - orderlines_to_be_merged, orderlines_to_be_appended = partition( - has_merge_candidate, incoming_orderlines - ) - - return ( - self._get_updated_orderlines( - existing_orderlines, orderlines_to_be_merged, pos_config_sudo - ) - + orderlines_to_be_appended - ) - - def _has_merge_candidate(self, item: Dict, existing_orderlines: List[Dict]) -> bool: - return any(self._can_be_merged(item, line) for line in existing_orderlines) - - def _can_be_merged(self, orderline1, orderline2): - return self._is_pos_groupable(orderline1["product_id"]) and all( - orderline1.get(key, "") == orderline2.get(key, "") - for key in PosOrderLine._get_unique_keys() - ) - - def _get_updated_orderlines( - self, orderlines: List[Dict], cart: List[Dict], pos_config_sudo: PosConfig - ) -> List[Dict]: - """ - :returns updated version of orderlines that takes into account the new items from the cart - """ - return [ - self._get_updated_orderline(line, updated_item, pos_config_sudo) - if ( - updated_item := next( - (item for item in cart if self._can_be_merged(item, line)), None - ) - ) - else line - for line in orderlines - ] - - def _get_updated_orderline( - self, line: Dict, updated_item: Dict, pos_config_sudo: PosConfig - ) -> Dict: - """ - This function assumes that the price of the product was not changed between the time the - first items were added to the order and the time the new items were added to the order. - """ - new_qty = updated_item["qty"] + line["qty"] - new_price = ( - request.env["product.product"] - .sudo() - .browse(int(line["product_id"])) - ._get_price_info(pos_config_sudo, qty=new_qty) - ) - return { - **line, - **{ - "price_subtotal": new_price["price_without_tax"], - "price_subtotal_incl": new_price["price_with_tax"], - "qty": new_qty, - "customer_note": updated_item.get("customer_note") or line.get("customer_note"), - }, - } - - def _is_pos_groupable(self, product_id: int) -> bool: - return ( - request.env["product.product"].sudo().browse(int(product_id)).uom_id.is_pos_groupable - ) - - def _get_full_product_name(self, name: str, description: str) -> str: - """ - :param name: ex: "[E-COM12] Desk Organizer" - :param description: ex: "M, Leather" - :return: ex: "Desk Organizer (M, Leather)" - """ - return f"{name} ({description})" if description else name - - def _create_orderlines(self, cart: List[Dict], pos_config_sudo: PosConfig) -> List[Dict]: - """ - Function that constructs the order lines (as the create_from_ui method expects them) from the cart. - From the frontend we only get basic data of the cart, such as the id of the product and the quantity. - We need to get the other details of the product from the database. - This is done for security reasons. - """ - - return [ - [ - 0, - 0, - self._create_orderline( - item, - pos_config_sudo, - ), - ] - # having the "full product name" means that the orderline is already in the db - # so we don't need to create a new object - if not item.get("full_product_name") else [0, 0, item] - for item in cart - ] - - def _create_orderline(self, item: Dict, pos_config_sudo: PosConfig) -> Dict: - """ - Function that constructs an order line (as the create_from_ui method expects it) from an item from the cart. - """ - product_sudo = request.env["product.product"].sudo().browse(int(item.get("product_id"))) - return { - "product_id": item.get("product_id"), - "qty": item.get("qty"), - **self._get_orderline_price_info(product_sudo, item, pos_config_sudo), - "discount": 0, - "tax_ids": product_sudo.taxes_id, - "pack_lot_ids": [], - "description": item.get("description"), # ex "M, Leather" - "full_product_name": self._get_full_product_name( # ex: "Desk Organizer (M, Leather)" - product_sudo._get_name(), - item.get("description"), - ), - "customer_note": item.get("customer_note"), - "pricec_type": "original", - "note": "", - "uuid": str(uuid.uuid4()), - } - - def _get_orderline_price_info( - self, product_sudo: ProductProduct, item: Dict, pos_config_sudo: PosConfig - ) -> Dict: - price_extra = ( - self._compute_price_extra(product_sudo, item.get("description"), pos_config_sudo) - if item.get("description") - else 0 - ) - price_unit = product_sudo.lst_price + price_extra - price_subtotal_info = product_sudo._get_price_info( - pos_config_sudo, price_unit, item.get("qty") - ) - - return { - "price_unit": price_unit, - "price_subtotal": price_subtotal_info["price_without_tax"], - "price_subtotal_incl": price_subtotal_info["price_with_tax"], - "price_extra": price_extra, - } - - def _compute_price_extra( - self, product_sudo: ProductProduct, description: str, pos_config_sudo: PosConfig - ) -> float: - """ - Function that computes the price_extra of a product based on the description. - :param description: The description of the product. ex: "M, Leather" - represents the selected values of the product's attributes. - """ - - return sum( - v["price_extra"]["price_without_tax"] - for attr, selected_value_name in zip( - product_sudo._get_attributes(pos_config_sudo), description.split(", ") - ) - for v in attr["values"] - if v["name"] == selected_value_name - ) - - def _form_order(self, order: Dict) -> Dict: - """ - Function that constructs an order object (as the create_from_ui method expects it) - It takes as input the order object that was created by self._create_order_data() - :param order: Dictionary representing an order. - :return: Dictionary representing an order (as the create_from_ui method expects it). - """ - # The self order can only update an order that was created on the self order itself. - # This is because the self order does not have the ability to update orders that were created on the POS, - # because the app wouldn't know the access token of the order. - - return { - "id": order.get("id"), - "data": { - "name": order.get("id"), - "amount_paid": 0, - "amount_total": self._compute_amount_total(order.get("lines")), - "amount_tax": self._compute_amount_tax(order.get("lines")), - "amount_return": 0, - "lines": order.get("lines"), - "statement_ids": [], - "pos_session_id": order.get("session_id"), - "partner_id": False, - "user_id": request.session.uid, - # we only need the digits of the pos_reference, not the whole id, which starts with word "Order" - "uid": order.get("id")[6:], - "sequence_number": order.get("sequence_number"), - "creation_date": str(fields.Datetime.now()), - "fiscal_position_id": False, - "to_invoice": False, - "to_ship": False, - "is_tipped": False, - "tip_amount": 0, - "access_token": order.get("access_token"), - "server_id": False, - "table_id": order.get("table_id"), - }, - "to_invoice": False, - "session_id": order.get("session_id"), - } - - def _compute_amount_total(self, lines: List[Dict]) -> float: - return sum(orderline[2].get("price_subtotal_incl") for orderline in lines) - - def _compute_amount_tax(self, lines: List[Dict]) -> float: - return sum( - orderline[2].get("price_subtotal_incl") - orderline[2].get("price_subtotal") - for orderline in lines - ) - - # the 2nd argument of pos_reference is the login number; - # the regular pos receives the login number from the backend on the first load - # the problem is that if the pos has login number 1, for example - # and we create a new order in the database, with sequence number 1, for example, - # when the regular pos will try to create a new order, it will also use sequence number 1 - # and login number 1 and the new order will have the same id as the order created by the self order app - # so it will ultimately not be recorded in the database. - # ( the regular pos does not check what was the last order id from the database ) - # it keeps track itself of the last order id -- this is why this problem occurs - # I thought that it is best to make the pos_self_order app work around - # the problem, instead of modifying the regular pos. - # so I decided to use the numbers from 900 to 999 for the login number - # this way, the regular pos will have a login number of 1, 2, 3, etc - # and the pos_self_order app will have a login number of 901, 902, 903, etc, - # where 901 is the login number of the table with id = 1, - # 902 is the login number of the table with id = 2, etc - # TODO: make it so the regular pos can't have a login number of 900 or more - # FIXME: what happens if `table_id` is larger that 100? - # [possible fix: using a different pos session for the self order an synchronizing the orders] - def _generate_unique_id(self, pos_session_id: int, table_id: int, sequence_number: int) -> str: - """ - Generates a public identification number for the order. - :return: The order name. Example: Order 00001-001-0001 - """ - return f"Order {pos_session_id:0>5}-{900 + table_id :0>3}-{sequence_number:0>4}" - - def _find_order( - self, - pos_reference: Optional[str], - order_access_token: Optional[str], - state: Optional[str] = None, - ) -> PosOrder: - """ - :return: pos order with the given pos_reference, access token and state - """ - return ( - pos_reference - and order_access_token - and request.env["pos.order"] - .sudo() - .search( - [ - ("pos_reference", "=", pos_reference), - ("access_token", "=", order_access_token), - *(state and [("state", "=", state)] or []), - ], - limit=1, - ) - ) - - def _get_sequence_number(self, table_id: int, session_id: int) -> int: - """ - :return: the new sequence number for the order. - """ - return ( - request.env["pos.order"] - .sudo() - .search( - [ - ( - "pos_reference", - "=like", - f"Order {session_id:0>5}-{900 + table_id}-____", - ), - ], - limit=1, - ) - .sequence_number - + 1 - ) or 1 diff --git a/addons/pos_self_order/controllers/orders.py b/addons/pos_self_order/controllers/orders.py new file mode 100644 index 00000000000..e5144c947e3 --- /dev/null +++ b/addons/pos_self_order/controllers/orders.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- + +import uuid + +from odoo import http, fields, Command +from odoo.http import request +from odoo.addons.pos_self_order.controllers.utils import ( + get_pos_config_sudo, + get_table_sudo, +) + +from werkzeug.exceptions import NotFound, BadRequest, Unauthorized + +class PosSelfOrderController(http.Controller): + @http.route("/pos-self-order/process-new-order", auth="public", type="json", website=True) + def process_new_order(self, order, table_access_token): + pos_config_id = order.get('pos_config_id') + lines = order.get('lines') + + pos_config_sudo = get_pos_config_sudo(pos_config_id) + pos_session_sudo = pos_config_sudo.current_session_id + table_sudo = get_table_sudo(table_access_token) + + if not pos_config_sudo.self_order_table_mode or not pos_config_sudo.has_active_session: + raise Unauthorized + if not table_sudo or not pos_session_sudo: + raise Unauthorized + + sequence_number = self._get_sequence_number(table_sudo.id, pos_session_sudo.id) + unique_id = self._generate_unique_id(pos_session_sudo.id, table_sudo.id, sequence_number) + + # Create the order without lines and prices computed + # We need to remap the order because some required fields are not used in the frontend. + order = { + 'id': unique_id, + 'data': { + 'uuid': order.get('uuid'), + 'name': unique_id, + 'user_id': request.session.uid, + 'sequence_number': sequence_number, + 'access_token': uuid.uuid4().hex, + 'pos_session_id': pos_session_sudo.id, + 'table_id': table_sudo.id, + "partner_id": False, + "creation_date": str(fields.Datetime.now()), + "fiscal_position_id": pos_config_sudo.default_fiscal_position_id, + "statement_ids": [], + "lines": [], + 'amount_tax': 0, + 'amount_total': 0, + 'amount_paid': 0, + 'amount_return': 0, + }, + "to_invoice": False, + 'session_id': pos_session_sudo.id, + } + + # Save the order in the database to get the id + posted_order_id = request.env['pos.order'].sudo().create_from_ui([order], draft=True)[0].get('id') + + # Process the lines and get their prices computed + lines = self._process_lines(lines, pos_config_sudo, posted_order_id) + + # Compute the order prices + amount_total, amount_untaxed = self._get_order_prices(lines) + + # Update the order with the computed prices and lines + order_sudo = request.env["pos.order"].sudo().browse(posted_order_id) + order_sudo.write({ + 'lines': [Command.create(line) for line in lines], + 'amount_tax': amount_total - amount_untaxed, + 'amount_total': amount_total, + }) + + return order_sudo._export_for_self_order() + + @http.route('/pos-self-order/get-orders-taxes', auth='public', type='json', website=True) + def get_order_taxes(self, order, pos_config_id): + pos_config_sudo = get_pos_config_sudo(pos_config_id) + + if not pos_config_sudo or not pos_config_sudo.self_order_table_mode: + raise Unauthorized + + lines = self._process_lines(order.get('lines'), pos_config_sudo, 0) + amount_total, amount_untaxed = self._get_order_prices(lines) + + return { + 'lines': [{ + 'uuid': line.get('uuid'), + 'price_subtotal': line.get('price_subtotal'), + 'price_subtotal_incl': line.get('price_subtotal_incl'), + } for line in lines], + 'amount_total': amount_total, + 'amount_tax': amount_total - amount_untaxed, + } + + @http.route('/pos-self-order/update-existing-order', auth="public", type="json", website=True) + def update_existing_order(self, order): + order_pos_reference = order.get('pos_reference') + order_access_token = order.get('access_token') + pos_config_id = order.get('pos_config_id') + pos_config_sudo = get_pos_config_sudo(pos_config_id) + + order_sudo = request.env['pos.order'].sudo().search([ + ('pos_reference', '=', order_pos_reference), + ('access_token', '=', order_access_token), + ]) + + if not order_sudo: + raise Unauthorized("Order not found in the server !") + elif order_sudo.state != 'draft': + raise BadRequest("Order is not in draft state") + + lines = self._process_lines(order.get('lines'), pos_config_sudo, order_sudo.id) + for line in lines: + if line.get('id'): + line_sudo = order_sudo.lines.filtered(lambda l: l.id == line.get('id')) + + if line.get('qty') < line_sudo.qty: + line.set('qty', line_sudo.qty) + + line_sudo.write({ + **line, + }) + else: + order_sudo.lines.create(line) + + amount_total, amount_untaxed = self._get_order_prices(lines) + order_sudo.write({ + 'amount_tax': amount_total - amount_untaxed, + 'amount_total': amount_total, + }) + + return order_sudo._export_for_self_order() + + @http.route('/pos-self-order/get-orders', auth='public', type='json', website=True) + def get_orders_by_access_token(self, access_tokens): + orders_sudo = request.env["pos.order"].sudo().search([ + ("access_token", "in", access_tokens), + ]) + + if not orders_sudo: + raise NotFound() + + orders = [] + for order in orders_sudo: + orders.append(order._export_for_self_order()) + + return orders + + def _process_lines(self, lines, pos_config_sudo, pos_order_id): + newLines = [] + pricelist = request.env['product.pricelist'].sudo().browse(pos_config_sudo.pricelist_id.id) + + for line in lines: + product_sudo = request.env["product.product"].sudo().browse(int(line.get("product_id"))) + # todo take into account the price extra + price_unit = pricelist._get_product_price(product_sudo, quantity=1) if pricelist else product_sudo.lst_price + + config_fiscal_pos = pos_config_sudo.default_fiscal_position_id + selected_account_tax = config_fiscal_pos.map_tax(product_sudo.taxes_id) if config_fiscal_pos else product_sudo.taxes_id + + tax_results = selected_account_tax.compute_all( + price_unit, + pos_config_sudo.currency_id, + line.get('qty'), + product_sudo, + ) + + newLines.append({ + 'price_unit': price_unit, + 'price_subtotal': tax_results.get('total_excluded'), + 'price_subtotal_incl': tax_results.get('total_included'), + 'price_extra': 0, + 'id': line.get('id'), + 'order_id': pos_order_id, + 'tax_ids': product_sudo.taxes_id, + 'uuid': line.get('uuid'), + 'product_id': line.get('product_id'), + 'qty': line.get('qty'), + 'customer_note': line.get('customer_note'), + 'selected_attributes': line.get('selected_attributes'), + 'full_product_name': line.get('full_product_name'), + }) + + return newLines + + def _get_order_prices(self, lines): + amount_untaxed = sum([line.get('price_subtotal') for line in lines]) + amount_total = sum([line.get('price_subtotal_incl') for line in lines]) + return amount_total, amount_untaxed + + # The first part will be the session_id of the order. + # The second part will be the table_id of the order. + # Last part the sequence number of the order. + # INFO: This is allow a maximum of 999 tables and 9999 orders per table, so about ~1M orders per session. + # Example: 'Self-Order 00001-001-0001' + def _generate_unique_id(self, pos_session_id: int, table_id: int, sequence_number: int) -> str: + first_part = "{:05d}".format(int(pos_session_id)) + second_part = "{:03d}".format(int(table_id)) + third_part = "{:04d}".format(int(sequence_number)) + + return f"Self-Order {first_part}-{second_part}-{third_part}" + + def _get_sequence_number(self, table_id: int, session_id: int) -> int: + order_sudo = request.env["pos.order"].sudo().search([( + 'pos_reference', + 'like', + f"Self-Order {session_id:0>5}-{table_id:0>3}")], order='id desc', limit=1) + + return (order_sudo.sequence_number + 1) or 1 diff --git a/addons/pos_self_order/controllers/utils.py b/addons/pos_self_order/controllers/utils.py index b2f8463a3d1..07e6296b096 100644 --- a/addons/pos_self_order/controllers/utils.py +++ b/addons/pos_self_order/controllers/utils.py @@ -39,11 +39,11 @@ def get_any_pos_config_sudo() -> PosConfig: ) or _raise(werkzeug.exceptions.NotFound()) -def get_table_sudo(table_access_token: Optional[str]) -> Optional[RestaurantTable]: - return table_access_token and ( +def get_table_sudo(access_token: Optional[str]) -> Optional[RestaurantTable]: + return access_token and ( request.env["restaurant.table"] .sudo() - .search([("access_token", "=", table_access_token), ("active", "=", True)], limit=1) + .search([("access_token", "=", access_token), ("active", "=", True)], limit=1) ) diff --git a/addons/pos_self_order/data/custom_link_data.xml b/addons/pos_self_order/data/custom_link_data.xml deleted file mode 100644 index 1a440a52e0b..00000000000 --- a/addons/pos_self_order/data/custom_link_data.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - View Menu - /products - - - diff --git a/addons/pos_self_order/demo/custom_link_data.xml b/addons/pos_self_order/demo/custom_link_data.xml new file mode 100644 index 00000000000..e66ccc69535 --- /dev/null +++ b/addons/pos_self_order/demo/custom_link_data.xml @@ -0,0 +1,10 @@ + + + + + View Menu + + + + + diff --git a/addons/pos_self_order/demo/pos_restaurant_demo.xml b/addons/pos_self_order/demo/pos_restaurant_demo.xml index 30de26cfd9e..4ce760cfa1f 100644 --- a/addons/pos_self_order/demo/pos_restaurant_demo.xml +++ b/addons/pos_self_order/demo/pos_restaurant_demo.xml @@ -4,12 +4,12 @@ True True - - diff --git a/addons/pos_self_order/models/pos_config.py b/addons/pos_self_order/models/pos_config.py index 984c72d6ebd..12ab9faca1a 100644 --- a/addons/pos_self_order/models/pos_config.py +++ b/addons/pos_self_order/models/pos_config.py @@ -61,15 +61,21 @@ class PosConfig(models.Model): directly on the fields, but `module_pos_restaurant` would not be known at the time that the function for this default value would run) """ - for vals in vals_list: - if vals.get("module_pos_restaurant"): - vals.update( - { - "self_order_view_mode": True, - "self_order_table_mode": True, - } - ) - return super().create(vals_list) + pos_config_ids = super().create(vals_list) + + for pos_config_id in pos_config_ids: + if pos_config_id.module_pos_restaurant: + pos_config_id.self_order_view_mode = True + pos_config_id.self_order_table_mode = True + + self.env['pos_self_order.custom_link'].create({ + 'url': '/menu/%s/products' % pos_config_id.id, + 'name': 'View Menu', + 'pos_config_ids': pos_config_id, + 'style': 'primary', + }) + + return pos_config_ids @api.depends("module_pos_restaurant") def _compute_self_order(self): @@ -86,14 +92,14 @@ class PosConfig(models.Model): base_route = f"/menu/{self.id}" if not self.self_order_table_mode: return base_route - table_access_token = ( + access_token = ( self.env["restaurant.table"] .search( [("active", "=", True), *(table_id and [("id", "=", table_id)] or [])], limit=1 ) .access_token ) - return f"{base_route}?at={table_access_token}" + return f"{base_route}?at={access_token}" def _get_self_order_url(self, table_id: Optional[int] = None) -> str: self.ensure_one() @@ -165,7 +171,6 @@ class PosConfig(models.Model): "custom_links": self._get_self_order_custom_links(), "products": self._get_available_products()._get_self_order_data(self), "has_active_session": self.has_active_session, - "orderline_unique_keys": PosOrderLine._get_unique_keys(), } def _generate_data_for_qr_codes_page(self, cols: int = 4) -> Dict[str, List[Dict]]: diff --git a/addons/pos_self_order/models/pos_order.py b/addons/pos_self_order/models/pos_order.py index 552ce181baf..1ded0c55ea9 100644 --- a/addons/pos_self_order/models/pos_order.py +++ b/addons/pos_self_order/models/pos_order.py @@ -4,80 +4,51 @@ import re from typing import Dict, Union -from odoo import models +from odoo import models, fields class PosOrderLine(models.Model): _inherit = "pos.order.line" + # For the moment we need this to keep attributes consistency between the server and client_side. + selected_attributes = fields.Json(string="Selected Attributes") + + # FIXME: uuid already pass in pos and move note in pos_restaurant. def _export_for_ui(self, orderline): return { - "uuid": orderline.uuid, - "note": orderline.note, + 'uuid': orderline.uuid, + 'note': orderline.note, **super()._export_for_ui(orderline), } - def _get_description(self): - """ - The pos sends a "description" key to the backend, which is a string containing the selected value for - each attribute of the product, separated by a comma. In the db we end up storing the - "full_product_name", which is composed of the product name and the description. - :line.full_product_name: ex: "Desk Organizer (M, Leather)" - :return: ex: "M, Leather" - """ - self.ensure_one() - description = re.findall(r"\(([^)]+)\)", self.full_product_name) - if description: - # It might happen that the product has a name with parenthesis. ex: "Salad (Vegie)" - # In that case the full_product_name will be "Salad (Vegie) (Small)", but are interested in returning the variant "Small" - # That's why we return the last element of the list - return description[-1] - return "" - - @staticmethod - def _get_unique_keys(): - """ - These are the keys that define the uniqueness of an orderline. - We use them to check if 2 orderlines can be merged or not. - """ - return ["product_id", "description", "customer_note"] - class PosOrder(models.Model): _inherit = "pos.order" def _export_for_self_order(self) -> Dict: - """ - Given an order, it returns a dictionary with the keys that we need in the frontend - """ self.ensure_one() + return { + "id": self.id, + "pos_config_id": self.config_id.id, "pos_reference": self.pos_reference, "access_token": self.access_token, "state": self.state, "date": str(self.date_order), "amount_total": self.amount_total, "amount_tax": self.amount_tax, - "items": [ + "lines": [ { + "id": line.id, + "price_subtotal": line.price_subtotal, + "price_subtotal_incl": line.price_subtotal_incl, "product_id": line.product_id.id, + "selected_attributes": line.selected_attributes, + "uuid": line.uuid, "qty": line.qty, "customer_note": line.customer_note, - "price_extra": line.product_id._get_price_info( - self.config_id, - line.price_extra, - ), - "description": line._get_description(), + "full_product_name": line.full_product_name, } for line in self.lines ], } - - def _get_self_order_data(self) -> Dict[str, Union[str, int]]: - return { - "id": self.pos_reference, - "sequence_number": self.sequence_number, - "access_token": self.access_token, - "session_id": self.session_id.id, - "table_id": self.table_id.id, - } diff --git a/addons/pos_self_order/models/pos_restaurant.py b/addons/pos_self_order/models/pos_restaurant.py index 8f21f0fd1a1..a3adcbe03b7 100644 --- a/addons/pos_self_order/models/pos_restaurant.py +++ b/addons/pos_self_order/models/pos_restaurant.py @@ -42,6 +42,7 @@ class RestaurantTable(models.Model): def _get_data_for_qr_codes_page(self, url: Callable[[Optional[int]], str]) -> List[Dict]: return [ { + "access_token": table.access_token, "id": table.id, "name": table.name, "url": url(table.id), diff --git a/addons/pos_self_order/models/product_product.py b/addons/pos_self_order/models/product_product.py index e1cf42aaefb..781374cca29 100644 --- a/addons/pos_self_order/models/product_product.py +++ b/addons/pos_self_order/models/product_product.py @@ -62,6 +62,7 @@ class ProductProduct(models.Model): ) return attributes + # FIXME: this method should be verified about price computation (pricelist taxes....) def _get_price_info( self, pos_config: PosConfig, price: Optional[float] = None, qty: int = 1 ) -> Dict[str, float]: diff --git a/addons/pos_self_order/models/res_config_settings.py b/addons/pos_self_order/models/res_config_settings.py index 713bde19d8b..806b683e8c4 100644 --- a/addons/pos_self_order/models/res_config_settings.py +++ b/addons/pos_self_order/models/res_config_settings.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -from odoo import models, fields, api +from odoo import models, fields, api, _ +from odoo.exceptions import UserError class ResConfigSettings(models.TransientModel): @@ -70,6 +71,9 @@ class ResConfigSettings(models.TransientModel): """ Generate the data needed to print the QR codes page """ + if len(self.pos_config_id.floor_ids.table_ids) == 0: + raise UserError(_("Before you can create QR codes, you need to create tables for the restaurant.")) + return self.env.ref("pos_self_order.report_self_order_qr_codes_page").report_action( [], data=self.pos_config_id._generate_data_for_qr_codes_page(cols=3) ) diff --git a/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.js b/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.js deleted file mode 100644 index a1a7af3d3ec..00000000000 --- a/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @odoo-module */ - -import { Component } from "@odoo/owl"; - -export class IncrementCounter extends Component { - static template = "pos_self_order.IncrementCounter"; - static props = ["value", "onClick"]; -} diff --git a/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.scss b/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.scss deleted file mode 100644 index 1593cdecea1..00000000000 --- a/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.scss +++ /dev/null @@ -1,4 +0,0 @@ -.o_self_order_incr_button button { - width: 40px; - height: 40px; -} \ No newline at end of file diff --git a/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.xml b/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.xml deleted file mode 100644 index 61f24267e8c..00000000000 --- a/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - -
- -
- -
- - diff --git a/addons/pos_self_order/static/src/Components/Loading/Loading.js b/addons/pos_self_order/static/src/Components/Loading/Loading.js deleted file mode 100644 index cd9a9ef9422..00000000000 --- a/addons/pos_self_order/static/src/Components/Loading/Loading.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @odoo-module */ - -import { Component } from "@odoo/owl"; - -export class Loading extends Component { - static template = "pos_self_order.Loading"; - static props = []; -} diff --git a/addons/pos_self_order/static/src/Components/Loading/Loading.xml b/addons/pos_self_order/static/src/Components/Loading/Loading.xml deleted file mode 100644 index ee214fef972..00000000000 --- a/addons/pos_self_order/static/src/Components/Loading/Loading.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - -
-
- Loading... -
-
-
-
diff --git a/addons/pos_self_order/static/src/Components/MainButton/MainButton.js b/addons/pos_self_order/static/src/Components/MainButton/MainButton.js deleted file mode 100644 index cfd5f986076..00000000000 --- a/addons/pos_self_order/static/src/Components/MainButton/MainButton.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @odoo-module */ - -import { Component } from "@odoo/owl"; - -export class MainButton extends Component { - static template = "pos_self_order.MainButton"; - static props = { slots: Object, onClick: Function }; -} diff --git a/addons/pos_self_order/static/src/Components/MainButton/MainButton.xml b/addons/pos_self_order/static/src/Components/MainButton/MainButton.xml deleted file mode 100644 index 0cafd90996a..00000000000 --- a/addons/pos_self_order/static/src/Components/MainButton/MainButton.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - -
- - -
-
-
diff --git a/addons/pos_self_order/static/src/Components/Order/Order.js b/addons/pos_self_order/static/src/Components/Order/Order.js deleted file mode 100644 index ae724cac57b..00000000000 --- a/addons/pos_self_order/static/src/Components/Order/Order.js +++ /dev/null @@ -1,15 +0,0 @@ -/** @odoo-module */ - -import { useSelfOrder } from "@pos_self_order/SelfOrderService"; -import { Component } from "@odoo/owl"; -import { ProductCard } from "@pos_self_order/Components/ProductCard/ProductCard"; -import { OrderLines } from "@pos_self_order/Components/OrderLines/OrderLines"; -import { PriceDetails } from "@pos_self_order/Components/PriceDetails/PriceDetails"; -export class Order extends Component { - static template = "pos_self_order.Order"; - static components = { ProductCard, OrderLines, PriceDetails }; - static props = ["order"]; - setup() { - this.selfOrder = useSelfOrder(); - } -} diff --git a/addons/pos_self_order/static/src/Components/Order/Order.xml b/addons/pos_self_order/static/src/Components/Order/Order.xml deleted file mode 100644 index 8f29cf23f72..00000000000 --- a/addons/pos_self_order/static/src/Components/Order/Order.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - -
-
-
-
- -
-

-

-
- -
- -
-
-
-
-
diff --git a/addons/pos_self_order/static/src/Components/OrderLines/OrderLines.js b/addons/pos_self_order/static/src/Components/OrderLines/OrderLines.js deleted file mode 100644 index fec8bb38c3c..00000000000 --- a/addons/pos_self_order/static/src/Components/OrderLines/OrderLines.js +++ /dev/null @@ -1,19 +0,0 @@ -/** @odoo-module */ - -import { Component } from "@odoo/owl"; -import { useSelfOrder } from "@pos_self_order/SelfOrderService"; -import { ProductCard } from "../ProductCard/ProductCard"; - -export class OrderLines extends Component { - static template = "pos_self_order.OrderLines"; - static components = { ProductCard }; - static props = ["lines"]; - setup() { - this.selfOrder = useSelfOrder(); - } - formatProduct(product) { - return (({ description_sale, has_image, ...rest }) => ({ has_image: false, ...rest }))( - product - ); - } -} diff --git a/addons/pos_self_order/static/src/Components/OrderLines/OrderLines.xml b/addons/pos_self_order/static/src/Components/OrderLines/OrderLines.xml deleted file mode 100644 index b8972280e11..00000000000 --- a/addons/pos_self_order/static/src/Components/OrderLines/OrderLines.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/addons/pos_self_order/static/src/Components/ProductCard/ProductCard.js b/addons/pos_self_order/static/src/Components/ProductCard/ProductCard.js deleted file mode 100644 index d5cefd93af5..00000000000 --- a/addons/pos_self_order/static/src/Components/ProductCard/ProductCard.js +++ /dev/null @@ -1,55 +0,0 @@ -/** @odoo-module */ - -import { Component } from "@odoo/owl"; -import { useSelfOrder } from "@pos_self_order/SelfOrderService"; -import { useForwardRefToParent } from "@web/core/utils/hooks"; - -export class ProductCard extends Component { - static template = "pos_self_order.ProductCard"; - static props = ["product", "orderLine?", "currentProductCard?", "class?"]; - static defaultProps = { class: "" }; - setup() { - this.selfOrder = useSelfOrder(); - useForwardRefToParent("currentProductCard"); - this.qtyInCart = !this.props.orderLine && this.getQtyInCart(); - } - clickOnProduct() { - const product = this.props.product; - if (!this.canOpenProductMainView(product)) { - return; - } - if (this.env.getCurrentRoute()[0] === "cart") { - this.selfOrder.setCurrentlyEditedOrderLine(this.props?.orderLine); - } - this.env.navigate("/products/" + product.product_id); - } - - canOpenProductMainView(product) { - return ( - this.selfOrder.table || - product.has_image || - product.description_sale || - product.attributes.length - ); - } - getQtyInCart() { - const cart = this.selfOrder.cart; - const product = this.props.product; - return cart - .filter((x) => x.product_id === product.product_id) - .reduce((sum, x) => sum + x.qty, 0); - } - getQtyInCartString() { - return `${this.props.orderLine.qty} x ${this.selfOrder.formatMonetary( - this.props.product.price_info.list_price + - (this.props.orderLine?.price_extra.list_price || 0) - )}`; - } - getTotalPriceString() { - return `${this.selfOrder.formatMonetary( - (this.props.orderLine?.qty || 1) * - (this.props.product.price_info.list_price + - (this.props.orderLine?.price_extra.list_price || 0)) - )}`; - } -} diff --git a/addons/pos_self_order/static/src/Components/ProductCard/ProductCard.xml b/addons/pos_self_order/static/src/Components/ProductCard/ProductCard.xml deleted file mode 100644 index b5a67016d5f..00000000000 --- a/addons/pos_self_order/static/src/Components/ProductCard/ProductCard.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - -
-
-
- -

- - - -
-