From 605943b5eaafe7e9224f0e7fb4fbdd020102c7a5 Mon Sep 17 00:00:00 2001 From: "David Monnom (moda)" Date: Mon, 24 Jul 2023 08:19:46 +0200 Subject: [PATCH] [ADD] point_of_sale,pos_*: add kiosk mode *:point_of_sale,pos_adyen,pos_online_payment_self_order,pos_restaurant, pos_sale,pos_self_order,pos_self_order_adyen,pos_self_order_restaurant, pos_self_order_sale This PR adds the kiosk to the existing `pos_self_order` module, which until now has enabled end-users to order their restaurant meals directly from their smartphones. We've added the possibility of creating kiosks / totems (devices with large touch screens) that allow users to order their menu directly without having to go through a waiter. Once the order has been placed, the user will be invited to pay either by adyen, already integrated into this PR, or at the counter. At the end of the order, the user will receive an order number and his table number if `service at table` is activated. Main differences with the Self-order mobile: - Kiosk shouldn't be accessible publicly - Kiosk should support payments via card readers closes odoo/odoo#129562 Design: XLU Dev: ADGU & MODA Taskid: 3323163 Related: odoo/upgrade#5024 Signed-off-by: Adrien Guilliams (adgu) --- addons/point_of_sale/models/pos_combo_line.py | 2 +- addons/point_of_sale/models/pos_order.py | 6 +- addons/point_of_sale/models/pos_session.py | 11 + .../models/res_config_settings.py | 1 + .../tests/test_point_of_sale_flow.py | 5 +- .../point_of_sale/views/pos_session_view.xml | 2 +- .../views/res_config_settings_views.xml | 135 ++++---- addons/pos_adyen/controllers/main.py | 2 +- .../views/res_config_settings_view.xml | 4 +- .../__manifest__.py | 4 +- .../static/src/pages/order_cart/order_cart.js | 2 +- .../static/src/self_order_service.js | 2 +- .../views/res_config_settings_views.xml | 2 +- addons/pos_restaurant/models/pos_config.py | 2 +- .../views/res_config_settings_views.xml | 56 ++-- .../views/res_config_settings_views.xml | 2 +- addons/pos_self_order/__manifest__.py | 32 +- addons/pos_self_order/controllers/__init__.py | 3 +- .../pos_self_order/controllers/kiosk_entry.py | 62 ++++ .../controllers/{entry.py => mobile_entry.py} | 42 +-- addons/pos_self_order/controllers/orders.py | 180 ++++++++--- addons/pos_self_order/controllers/utils.py | 2 +- addons/pos_self_order/models/__init__.py | 1 + addons/pos_self_order/models/pos_config.py | 289 +++++++++++++++--- addons/pos_self_order/models/pos_order.py | 36 +++ .../models/pos_payment_method.py | 9 + .../pos_self_order/models/product_product.py | 45 +-- .../models/res_config_settings.py | 28 ++ addons/pos_self_order/static/img/eatin.svg | 1 + addons/pos_self_order/static/img/takeAway.svg | 1 + .../hooks/use_scroll_direction.js | 0 .../static/src/common/models/combo.js | 22 ++ .../static/src/common/models/combo_line.js | 18 ++ .../static/src/{ => common}/models/line.js | 25 ++ .../static/src/{ => common}/models/order.js | 34 ++- .../static/src/{ => common}/models/product.js | 12 +- .../static/src/{ => common}/router.js | 4 +- .../{ => common}/self_order_router_service.js | 4 +- .../static/src/common/self_order_service.js | 110 +++++++ .../pos_self_order/static/src/common/utils.js | 14 + .../src/kiosk/bootstrap_overridden.scss | 2 + .../attribute_selection.js | 67 ++++ .../attribute_selection.scss | 17 ++ .../attribute_selection.xml | 57 ++++ .../components/cancel_popup/cancel_popup.js | 12 + .../components/cancel_popup/cancel_popup.scss | 6 + .../components/cancel_popup/cancel_popup.xml | 23 ++ .../combo_selection/combo_selection.js | 24 ++ .../combo_selection/combo_selection.xml | 43 +++ .../language_popup/language_popup.js | 27 ++ .../language_popup/language_popup.scss | 10 + .../language_popup/language_popup.xml | 28 ++ .../components/product_card/product_card.js | 104 +++++++ .../components/product_card/product_card.scss | 12 + .../components/product_card/product_card.xml | 19 ++ .../static/src/kiosk/pages/closed/closed.js | 9 + .../static/src/kiosk/pages/closed/closed.xml | 12 + .../static/src/kiosk/pages/combo/combo.js | 142 +++++++++ .../static/src/kiosk/pages/combo/combo.scss | 56 ++++ .../static/src/kiosk/pages/combo/combo.xml | 95 ++++++ .../pages/eating_location/eating_location.js | 27 ++ .../pages/eating_location/eating_location.xml | 36 +++ .../kiosk/pages/landing_page/landing_page.js | 44 +++ .../pages/landing_page/landing_page.scss | 11 + .../kiosk/pages/landing_page/landing_page.xml | 26 ++ .../src/kiosk/pages/order_cart/order_cart.js | 72 +++++ .../kiosk/pages/order_cart/order_cart.scss | 38 +++ .../src/kiosk/pages/order_cart/order_cart.xml | 77 +++++ .../static/src/kiosk/pages/payment/payment.js | 65 ++++ .../src/kiosk/pages/payment/payment.xml | 42 +++ .../pages/payment_success/payment_success.js | 47 +++ .../payment_success/payment_success.scss | 6 + .../pages/payment_success/payment_success.xml | 38 +++ .../static/src/kiosk/pages/product/product.js | 90 ++++++ .../src/kiosk/pages/product/product.xml | 56 ++++ .../kiosk/pages/product_list/product_list.js | 58 ++++ .../pages/product_list/product_list.scss | 28 ++ .../kiosk/pages/product_list/product_list.xml | 54 ++++ .../kiosk/pages/stand_number/stand_number.js | 43 +++ .../kiosk/pages/stand_number/stand_number.xml | 43 +++ .../src/kiosk/self_order_kiosk_index.js | 58 ++++ .../src/kiosk/self_order_kiosk_index.scss | 26 ++ .../src/kiosk/self_order_kiosk_index.xml | 38 +++ .../src/kiosk/self_order_kiosk_service.js | 108 +++++++ .../src/kiosk/template/kiosk_template.js | 54 ++++ .../src/kiosk/template/kiosk_template.scss | 13 + .../src/kiosk/template/kiosk_template.xml | 16 + .../{ => mobile}/components/lines/lines.js | 2 +- .../{ => mobile}/components/lines/lines.xml | 0 .../{ => mobile}/components/navbar/navbar.js | 2 +- .../components/navbar/navbar.scss | 0 .../{ => mobile}/components/navbar/navbar.xml | 0 .../components/popup_table/popup_table.js | 2 +- .../components/popup_table/popup_table.scss | 0 .../components/popup_table/popup_table.xml | 0 .../components/price_details/price_details.js | 2 +- .../price_details/price_details.xml | 0 .../components/product_card/product_card.js | 2 +- .../components/product_card/product_card.scss | 5 +- .../components/product_card/product_card.xml | 2 +- .../src/{ => mobile}/hooks/use_detection.js | 8 +- .../pages/landing_page/landing_page.js | 2 +- .../pages/landing_page/landing_page.xml | 2 +- .../pages/order_cart/order_cart.js | 12 +- .../pages/order_cart/order_cart.scss | 0 .../pages/order_cart/order_cart.xml | 0 .../pages/orders_history/orders_history.js | 16 +- .../pages/orders_history/orders_history.xml | 0 .../pages/product_list/product_list.js | 22 +- .../pages/product_list/product_list.xml | 18 +- .../product_main_view/product_main_view.js | 8 +- .../product_main_view/product_main_view.scss | 0 .../product_main_view/product_main_view.xml | 4 +- .../{ => mobile}/pos_self_order_styles.scss | 0 .../{ => mobile}/self_order_bus_service.js | 0 .../self_order_mobile_service.js} | 124 +------- .../src/{ => mobile}/self_order_root.js | 16 +- .../src/{ => mobile}/self_order_root.xml | 2 +- .../static/src/{ => mobile}/utils.js | 0 .../views/point_of_sale_dashboard.xml | 59 +++- .../pos_self_order/views/pos_config_view.xml | 13 + .../views/pos_self_order_kiosk.index.xml | 25 ++ ...ex.xml => pos_self_order_mobile.index.xml} | 6 +- .../views/res_config_settings_views.xml | 94 +++++- addons/pos_self_order_adyen/__init__.py | 4 + addons/pos_self_order_adyen/__manifest__.py | 11 + .../controllers/__init__.py | 2 + .../pos_self_order_adyen/controllers/main.py | 41 +++ .../pos_self_order_adyen/models/__init__.py | 3 + .../models/pos_payment_method.py | 50 +++ addons/pos_self_order_sale/__init__.py | 3 + addons/pos_self_order_sale/__manifest__.py | 21 ++ .../data/kiosk_sale_team.xml | 8 + addons/pos_self_order_sale/models/__init__.py | 4 + .../models/product_product.py | 13 + .../models/res_config_settings.py | 14 + .../static/src/common/models/product.js | 10 + .../src/kiosk/pages/order_cart/order_cart.js | 17 ++ .../src/kiosk/pages/order_cart/order_cart.xml | 24 ++ .../views/res_config_settings_views.xml | 13 + 140 files changed, 3429 insertions(+), 448 deletions(-) create mode 100644 addons/pos_self_order/controllers/kiosk_entry.py rename addons/pos_self_order/controllers/{entry.py => mobile_entry.py} (74%) create mode 100644 addons/pos_self_order/models/pos_payment_method.py create mode 100644 addons/pos_self_order/static/img/eatin.svg create mode 100644 addons/pos_self_order/static/img/takeAway.svg rename addons/pos_self_order/static/src/{ => common}/hooks/use_scroll_direction.js (100%) create mode 100644 addons/pos_self_order/static/src/common/models/combo.js create mode 100644 addons/pos_self_order/static/src/common/models/combo_line.js rename addons/pos_self_order/static/src/{ => common}/models/line.js (61%) rename addons/pos_self_order/static/src/{ => common}/models/order.js (73%) rename addons/pos_self_order/static/src/{ => common}/models/product.js (79%) rename addons/pos_self_order/static/src/{ => common}/router.js (96%) rename addons/pos_self_order/static/src/{ => common}/self_order_router_service.js (96%) create mode 100644 addons/pos_self_order/static/src/common/self_order_service.js create mode 100644 addons/pos_self_order/static/src/common/utils.js create mode 100644 addons/pos_self_order/static/src/kiosk/bootstrap_overridden.scss create mode 100644 addons/pos_self_order/static/src/kiosk/components/attribute_selection/attribute_selection.js create mode 100644 addons/pos_self_order/static/src/kiosk/components/attribute_selection/attribute_selection.scss create mode 100644 addons/pos_self_order/static/src/kiosk/components/attribute_selection/attribute_selection.xml create mode 100644 addons/pos_self_order/static/src/kiosk/components/cancel_popup/cancel_popup.js create mode 100644 addons/pos_self_order/static/src/kiosk/components/cancel_popup/cancel_popup.scss create mode 100644 addons/pos_self_order/static/src/kiosk/components/cancel_popup/cancel_popup.xml create mode 100644 addons/pos_self_order/static/src/kiosk/components/combo_selection/combo_selection.js create mode 100644 addons/pos_self_order/static/src/kiosk/components/combo_selection/combo_selection.xml create mode 100644 addons/pos_self_order/static/src/kiosk/components/language_popup/language_popup.js create mode 100644 addons/pos_self_order/static/src/kiosk/components/language_popup/language_popup.scss create mode 100644 addons/pos_self_order/static/src/kiosk/components/language_popup/language_popup.xml create mode 100644 addons/pos_self_order/static/src/kiosk/components/product_card/product_card.js create mode 100644 addons/pos_self_order/static/src/kiosk/components/product_card/product_card.scss create mode 100644 addons/pos_self_order/static/src/kiosk/components/product_card/product_card.xml create mode 100644 addons/pos_self_order/static/src/kiosk/pages/closed/closed.js create mode 100644 addons/pos_self_order/static/src/kiosk/pages/closed/closed.xml create mode 100644 addons/pos_self_order/static/src/kiosk/pages/combo/combo.js create mode 100644 addons/pos_self_order/static/src/kiosk/pages/combo/combo.scss create mode 100644 addons/pos_self_order/static/src/kiosk/pages/combo/combo.xml create mode 100644 addons/pos_self_order/static/src/kiosk/pages/eating_location/eating_location.js create mode 100644 addons/pos_self_order/static/src/kiosk/pages/eating_location/eating_location.xml create mode 100644 addons/pos_self_order/static/src/kiosk/pages/landing_page/landing_page.js create mode 100644 addons/pos_self_order/static/src/kiosk/pages/landing_page/landing_page.scss create mode 100644 addons/pos_self_order/static/src/kiosk/pages/landing_page/landing_page.xml create mode 100644 addons/pos_self_order/static/src/kiosk/pages/order_cart/order_cart.js create mode 100644 addons/pos_self_order/static/src/kiosk/pages/order_cart/order_cart.scss create mode 100644 addons/pos_self_order/static/src/kiosk/pages/order_cart/order_cart.xml create mode 100644 addons/pos_self_order/static/src/kiosk/pages/payment/payment.js create mode 100644 addons/pos_self_order/static/src/kiosk/pages/payment/payment.xml create mode 100644 addons/pos_self_order/static/src/kiosk/pages/payment_success/payment_success.js create mode 100644 addons/pos_self_order/static/src/kiosk/pages/payment_success/payment_success.scss create mode 100644 addons/pos_self_order/static/src/kiosk/pages/payment_success/payment_success.xml create mode 100644 addons/pos_self_order/static/src/kiosk/pages/product/product.js create mode 100644 addons/pos_self_order/static/src/kiosk/pages/product/product.xml create mode 100644 addons/pos_self_order/static/src/kiosk/pages/product_list/product_list.js create mode 100644 addons/pos_self_order/static/src/kiosk/pages/product_list/product_list.scss create mode 100644 addons/pos_self_order/static/src/kiosk/pages/product_list/product_list.xml create mode 100644 addons/pos_self_order/static/src/kiosk/pages/stand_number/stand_number.js create mode 100644 addons/pos_self_order/static/src/kiosk/pages/stand_number/stand_number.xml create mode 100644 addons/pos_self_order/static/src/kiosk/self_order_kiosk_index.js create mode 100644 addons/pos_self_order/static/src/kiosk/self_order_kiosk_index.scss create mode 100644 addons/pos_self_order/static/src/kiosk/self_order_kiosk_index.xml create mode 100644 addons/pos_self_order/static/src/kiosk/self_order_kiosk_service.js create mode 100644 addons/pos_self_order/static/src/kiosk/template/kiosk_template.js create mode 100644 addons/pos_self_order/static/src/kiosk/template/kiosk_template.scss create mode 100644 addons/pos_self_order/static/src/kiosk/template/kiosk_template.xml rename addons/pos_self_order/static/src/{ => mobile}/components/lines/lines.js (92%) rename addons/pos_self_order/static/src/{ => mobile}/components/lines/lines.xml (100%) rename addons/pos_self_order/static/src/{ => mobile}/components/navbar/navbar.js (89%) rename addons/pos_self_order/static/src/{ => mobile}/components/navbar/navbar.scss (100%) rename addons/pos_self_order/static/src/{ => mobile}/components/navbar/navbar.xml (100%) rename addons/pos_self_order/static/src/{ => mobile}/components/popup_table/popup_table.js (94%) rename addons/pos_self_order/static/src/{ => mobile}/components/popup_table/popup_table.scss (100%) rename addons/pos_self_order/static/src/{ => mobile}/components/popup_table/popup_table.xml (100%) rename addons/pos_self_order/static/src/{ => mobile}/components/price_details/price_details.js (77%) rename addons/pos_self_order/static/src/{ => mobile}/components/price_details/price_details.xml (100%) rename addons/pos_self_order/static/src/{ => mobile}/components/product_card/product_card.js (93%) rename addons/pos_self_order/static/src/{ => mobile}/components/product_card/product_card.scss (95%) rename addons/pos_self_order/static/src/{ => mobile}/components/product_card/product_card.xml (96%) rename addons/pos_self_order/static/src/{ => mobile}/hooks/use_detection.js (87%) rename addons/pos_self_order/static/src/{ => mobile}/pages/landing_page/landing_page.js (89%) rename addons/pos_self_order/static/src/{ => mobile}/pages/landing_page/landing_page.xml (97%) rename addons/pos_self_order/static/src/{ => mobile}/pages/order_cart/order_cart.js (80%) rename addons/pos_self_order/static/src/{ => mobile}/pages/order_cart/order_cart.scss (100%) rename addons/pos_self_order/static/src/{ => mobile}/pages/order_cart/order_cart.xml (100%) rename addons/pos_self_order/static/src/{ => mobile}/pages/orders_history/orders_history.js (64%) rename addons/pos_self_order/static/src/{ => mobile}/pages/orders_history/orders_history.xml (100%) rename addons/pos_self_order/static/src/{ => mobile}/pages/product_list/product_list.js (84%) rename addons/pos_self_order/static/src/{ => mobile}/pages/product_list/product_list.xml (93%) rename addons/pos_self_order/static/src/{ => mobile}/pages/product_main_view/product_main_view.js (94%) rename addons/pos_self_order/static/src/{ => mobile}/pages/product_main_view/product_main_view.scss (100%) rename addons/pos_self_order/static/src/{ => mobile}/pages/product_main_view/product_main_view.xml (98%) rename addons/pos_self_order/static/src/{ => mobile}/pos_self_order_styles.scss (100%) rename addons/pos_self_order/static/src/{ => mobile}/self_order_bus_service.js (100%) rename addons/pos_self_order/static/src/{self_order_service.js => mobile/self_order_mobile_service.js} (62%) rename addons/pos_self_order/static/src/{ => mobile}/self_order_root.js (72%) rename addons/pos_self_order/static/src/{ => mobile}/self_order_root.xml (89%) rename addons/pos_self_order/static/src/{ => mobile}/utils.js (100%) create mode 100644 addons/pos_self_order/views/pos_config_view.xml create mode 100644 addons/pos_self_order/views/pos_self_order_kiosk.index.xml rename addons/pos_self_order/views/{index.xml => pos_self_order_mobile.index.xml} (83%) create mode 100644 addons/pos_self_order_adyen/__init__.py create mode 100644 addons/pos_self_order_adyen/__manifest__.py create mode 100644 addons/pos_self_order_adyen/controllers/__init__.py create mode 100644 addons/pos_self_order_adyen/controllers/main.py create mode 100644 addons/pos_self_order_adyen/models/__init__.py create mode 100644 addons/pos_self_order_adyen/models/pos_payment_method.py create mode 100644 addons/pos_self_order_sale/__init__.py create mode 100644 addons/pos_self_order_sale/__manifest__.py create mode 100644 addons/pos_self_order_sale/data/kiosk_sale_team.xml create mode 100644 addons/pos_self_order_sale/models/__init__.py create mode 100644 addons/pos_self_order_sale/models/product_product.py create mode 100644 addons/pos_self_order_sale/models/res_config_settings.py create mode 100644 addons/pos_self_order_sale/static/src/common/models/product.js create mode 100644 addons/pos_self_order_sale/static/src/kiosk/pages/order_cart/order_cart.js create mode 100644 addons/pos_self_order_sale/static/src/kiosk/pages/order_cart/order_cart.xml create mode 100644 addons/pos_self_order_sale/views/res_config_settings_views.xml diff --git a/addons/point_of_sale/models/pos_combo_line.py b/addons/point_of_sale/models/pos_combo_line.py index 6293b2a427a..2ed444fb66c 100644 --- a/addons/point_of_sale/models/pos_combo_line.py +++ b/addons/point_of_sale/models/pos_combo_line.py @@ -5,7 +5,7 @@ class PosComboLine(models.Model): _name = "pos.combo.line" _description = "Product Combo Items" - product_id = fields.Many2one("product.product", string="Product") + product_id = fields.Many2one("product.product", string="Product", required=True) price = fields.Float("Price Extra", default=0.0) lst_price = fields.Float("Original Price", related="product_id.lst_price") combo_id = fields.Many2one("pos.combo") diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 69e1f454c82..09c973f45b3 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -776,6 +776,7 @@ class PosOrder(models.Model): 'balance': -balance, }) + # sort self.payment_ids by is_split_transaction: for payment_id in self.payment_ids: is_split_transaction = payment_id.payment_method_id.split_transactions if is_split_transaction: @@ -783,7 +784,10 @@ class PosOrder(models.Model): else: reversed_move_receivable_account_id = payment_id.payment_method_id.receivable_account_id or self.company_id.account_default_pos_receivable_account_id - aml_vals_entry_found = [aml_entry for aml_entry in aml_vals_list_per_nature['payment_terms'] if aml_entry['account_id'] == reversed_move_receivable_account_id.id] + aml_vals_entry_found = [aml_entry for aml_entry in aml_vals_list_per_nature['payment_terms'] + if aml_entry['account_id'] == reversed_move_receivable_account_id.id + and not aml_entry['partner_id']] + if aml_vals_entry_found and not is_split_transaction: aml_vals_entry_found[0]['amount_currency'] += self.session_id._amount_converter(payment_id.amount, self.date_order, False) aml_vals_entry_found[0]['balance'] += payment_id.amount diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 8eac73aa7ac..3aa1788b991 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -222,6 +222,17 @@ class PosSession(models.Model): else: sessions = super().create(vals_list) sessions.action_pos_session_open() + + for session in sessions: + session.env['ir.sequence'].sudo().create({ + 'name': _("PoS Order by Session"), + 'padding': 4, + 'code': f'pos.order_{session.id}', + 'number_next': 1, + 'number_increment': 1, + 'company_id': False, + }) + return sessions def unlink(self): diff --git a/addons/point_of_sale/models/res_config_settings.py b/addons/point_of_sale/models/res_config_settings.py index 3fd30d9c66a..26581e20f84 100644 --- a/addons/point_of_sale/models/res_config_settings.py +++ b/addons/point_of_sale/models/res_config_settings.py @@ -40,6 +40,7 @@ class ResConfigSettings(models.TransientModel): account_default_pos_receivable_account_id = fields.Many2one(string='Default Account Receivable (PoS)', related='company_id.account_default_pos_receivable_account_id', readonly=False) is_default_pricelist_displayed = fields.Boolean(compute="_compute_pos_pricelist_id", compute_sudo=True) barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', related='company_id.nomenclature_id', readonly=False) + is_kiosk_mode = fields.Boolean(string="Is Kiosk Mode", default=False) # pos.config fields pos_module_pos_discount = fields.Boolean(related='pos_config_id.module_pos_discount', readonly=False) diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py index 59da5aba01a..8fc9777ca6e 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -1598,10 +1598,11 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): original_payment = self.env['account.move'].search([('ref', '=', current_session.display_name)]) original_customer_payment_entry = original_payment.line_ids.filtered(lambda l: l.account_id.account_type == 'asset_receivable') reverser_customer_payment_entry = reverse_payment.line_ids.filtered(lambda l: l.account_id.account_type == 'asset_receivable') + #check that both use the same account self.assertEqual(len(reverser_customer_payment_entry), 2) - self.assertEqual(reverser_customer_payment_entry[0].balance, -2.0) - self.assertEqual(reverser_customer_payment_entry[1].balance, -4.0) + self.assertEqual(reverser_customer_payment_entry.filtered(lambda l: l.partner_id).balance, -4.0) + self.assertEqual(reverser_customer_payment_entry.filtered(lambda l: not l.partner_id).balance, -2.0) self.assertEqual(original_customer_payment_entry.account_id.id, reverser_customer_payment_entry.account_id.id) self.assertEqual(reverser_customer_payment_entry.partner_id, original_customer_payment_entry.partner_id) diff --git a/addons/point_of_sale/views/pos_session_view.xml b/addons/point_of_sale/views/pos_session_view.xml index 970a4789885..010f3111dbf 100644 --- a/addons/point_of_sale/views/pos_session_view.xml +++ b/addons/point_of_sale/views/pos_session_view.xml @@ -7,7 +7,7 @@
+ +
- + - -
@@ -49,6 +49,61 @@
+ + + + +
+
+
+ + + + + +
+
+
+
+
+ +
+
+
+
+
+
+ + +
+
+
+ + + + + +
+
+
+
+
+
+ +
@@ -56,7 +111,7 @@
- +
@@ -76,7 +131,7 @@ - + @@ -91,7 +146,6 @@
-
- +
@@ -168,7 +222,7 @@
- + @@ -179,7 +233,7 @@ target="_blank" class="oe-link">How to manage tax-included prices - + @@ -228,57 +282,6 @@
- - - -
-
-
- - - - - -
-
-
-
-
- -
-
-
-
-
-
- - -
-
-
- - - - - -
-
-
-
-
-
- diff --git a/addons/pos_adyen/controllers/main.py b/addons/pos_adyen/controllers/main.py index ec988da38d6..0cbf7983eda 100644 --- a/addons/pos_adyen/controllers/main.py +++ b/addons/pos_adyen/controllers/main.py @@ -2,7 +2,7 @@ import logging import pprint import json -from odoo import fields, http +from odoo import http from odoo.http import request _logger = logging.getLogger(__name__) diff --git a/addons/pos_loyalty/views/res_config_settings_view.xml b/addons/pos_loyalty/views/res_config_settings_view.xml index c524c517322..ce831cd3cfe 100644 --- a/addons/pos_loyalty/views/res_config_settings_view.xml +++ b/addons/pos_loyalty/views/res_config_settings_view.xml @@ -7,13 +7,13 @@
-
+
-
+
diff --git a/addons/pos_online_payment_self_order/__manifest__.py b/addons/pos_online_payment_self_order/__manifest__.py index 38c5dc42cda..edde1a3278e 100644 --- a/addons/pos_online_payment_self_order/__manifest__.py +++ b/addons/pos_online_payment_self_order/__manifest__.py @@ -14,10 +14,10 @@ 'installable': True, 'auto_install': True, 'assets': { - 'pos_self_order.assets_self_order': [ + 'pos_self_order.assets_mobile': [ 'pos_online_payment_self_order/static/src/**/*', ], - 'pos_self_order.assets_tests': [ + 'pos_self_order.assets_mobile_tests': [ 'pos_online_payment_self_order/static/tests/tours/**/*', ], }, diff --git a/addons/pos_online_payment_self_order/static/src/pages/order_cart/order_cart.js b/addons/pos_online_payment_self_order/static/src/pages/order_cart/order_cart.js index f1a9c3c412a..db22d9592d2 100644 --- a/addons/pos_online_payment_self_order/static/src/pages/order_cart/order_cart.js +++ b/addons/pos_online_payment_self_order/static/src/pages/order_cart/order_cart.js @@ -1,6 +1,6 @@ /** @odoo-module */ import { patch } from "@web/core/utils/patch"; -import { OrderCart } from "@pos_self_order/pages/order_cart/order_cart"; +import { OrderCart } from "@pos_self_order/mobile/pages/order_cart/order_cart"; import { _t } from "@web/core/l10n/translation"; patch(OrderCart.prototype, { diff --git a/addons/pos_online_payment_self_order/static/src/self_order_service.js b/addons/pos_online_payment_self_order/static/src/self_order_service.js index 8fef6d060e2..cd133f6621b 100644 --- a/addons/pos_online_payment_self_order/static/src/self_order_service.js +++ b/addons/pos_online_payment_self_order/static/src/self_order_service.js @@ -1,6 +1,6 @@ /** @odoo-module */ import { patch } from "@web/core/utils/patch"; -import { SelfOrder } from "@pos_self_order/self_order_service"; +import { SelfOrder } from "@pos_self_order/mobile/self_order_mobile_service"; import { session } from "@web/session"; patch(SelfOrder.prototype, { diff --git a/addons/pos_online_payment_self_order/views/res_config_settings_views.xml b/addons/pos_online_payment_self_order/views/res_config_settings_views.xml index 2ab222baeb2..0d2440a2b0b 100644 --- a/addons/pos_online_payment_self_order/views/res_config_settings_views.xml +++ b/addons/pos_online_payment_self_order/views/res_config_settings_views.xml @@ -3,7 +3,7 @@ res.config.settings.view.form.inherit.pos_online_payment.view res.config.settings - +
diff --git a/addons/pos_restaurant/models/pos_config.py b/addons/pos_restaurant/models/pos_config.py index 2015029eeb1..4c3f75234ca 100644 --- a/addons/pos_restaurant/models/pos_config.py +++ b/addons/pos_restaurant/models/pos_config.py @@ -33,7 +33,7 @@ class PosConfig(models.Model): # we ignore last_order_preparation_change when it is set to false. # In future, pos_self_order will send the various changes to the order. if not line.order_id.last_order_preparation_change: - continue + line.order_id.last_order_preparation_change = '{}' last_order_preparation_change = json.loads(line.order_id.last_order_preparation_change) prep_change = {} diff --git a/addons/pos_restaurant/views/res_config_settings_views.xml b/addons/pos_restaurant/views/res_config_settings_views.xml index 8d688b56b3b..3580ecf82b0 100644 --- a/addons/pos_restaurant/views/res_config_settings_views.xml +++ b/addons/pos_restaurant/views/res_config_settings_views.xml @@ -6,37 +6,35 @@
- - - -
-
-
-
-
+ + +
+
+
- - - - - - - - - - - - -
-
- Save this page and come back here to set up the feature. -
+
+
- - +
+
+ + + + + + + + + + + +
+
+ Save this page and come back here to set up the feature. +
+
+
diff --git a/addons/pos_sale/views/res_config_settings_views.xml b/addons/pos_sale/views/res_config_settings_views.xml index 5bb0915bf6d..d610c40702e 100644 --- a/addons/pos_sale/views/res_config_settings_views.xml +++ b/addons/pos_sale/views/res_config_settings_views.xml @@ -11,7 +11,7 @@ - + ", "/kiosk//"], auth="public", website=True, sitemap=True) + def pos_self_order_kiosk_start(self, config_id=None, access_token=None): + if not config_id or not access_token: + raise werkzeug.exceptions.NotFound() + + pos_config_sudo = request.env["pos.config"].sudo().search([ + ("id", "=", config_id), + ('access_token', '=', access_token)], limit=1) + + if not pos_config_sudo: + raise werkzeug.exceptions.NotFound() + + company = pos_config_sudo.company_id + user = pos_config_sudo.current_session_id.user_id + pos_config = reduce_privilege(pos_config_sudo, company, user) + session_info = request.env["ir.http"].get_frontend_session_info() + + return request.render( + 'pos_self_order.kiosk_index', + { + 'session_info': { + **session_info, + 'currencies': request.env["ir.http"].get_currencies(), + 'pos_self_order_data': { + 'access_token': pos_config.access_token, + **pos_config._get_self_order_kiosk_data(), + }, + } + } + ) + + @http.route( + "/kiosk/get-category-image/", + methods=["GET"], + type="http", + auth="public", + ) + def pos_self_order_get_image(self, category_id: int): + category = request.env["pos.category"].sudo().browse(category_id) + + if not category.has_image: + raise werkzeug.exceptions.NotFound() + + return ( + request.env["ir.binary"] + ._get_image_stream_from( + category, + field_name="image_128", + ) + .get_response() + ) diff --git a/addons/pos_self_order/controllers/entry.py b/addons/pos_self_order/controllers/mobile_entry.py similarity index 74% rename from addons/pos_self_order/controllers/entry.py rename to addons/pos_self_order/controllers/mobile_entry.py index 2f352313831..d3940e662d7 100644 --- a/addons/pos_self_order/controllers/entry.py +++ b/addons/pos_self_order/controllers/mobile_entry.py @@ -33,12 +33,18 @@ class PosQRMenuController(http.Controller): def pos_self_order_start(self, config_id=None, access_token=None, table_identifier=None): pos_config_sudo = False table_sudo = False + config_access_token = False if config_id and config_id.isnumeric() and access_token: pos_config_sudo = request.env["pos.config"].sudo().search([ ("id", "=", config_id), ('access_token', '=', access_token)], limit=1) + if pos_config_sudo: + config_access_token = pos_config_sudo.access_token + else: + raise werkzeug.exceptions.Unauthorized() + if pos_config_sudo and pos_config_sudo.has_active_session and pos_config_sudo.self_order_table_mode: table_sudo = table_identifier and ( request.env["restaurant.table"] @@ -60,7 +66,7 @@ class PosQRMenuController(http.Controller): raise werkzeug.exceptions.NotFound() return request.render( - 'pos_self_order.index', + 'pos_self_order.mobile_index', { 'session_info': { **request.env["ir.http"].get_frontend_session_info(), @@ -68,8 +74,8 @@ class PosQRMenuController(http.Controller): 'pos_self_order_data': { 'self_order_mode': pos_config.self_order_pay_after, 'table': table._get_self_order_data() if table else False, - 'access_token': pos_config.access_token, - **pos_config._get_self_order_data(), + 'access_token': config_access_token, + **pos_config._get_self_order_mobile_data(), }, "base_url": request.env['pos.session'].get_base_url(), } @@ -85,13 +91,9 @@ class PosQRMenuController(http.Controller): type="http", auth="public", ) - def pos_self_order_get_image(self, product_id: int, image_size: Optional[int] = 128): - """ - This is the route that the POS Self Order App uses to GET THE PRODUCT IMAGES. - :return: the image of the product (or a default one in case no product is found) - or an exception if there is no pos configured as self order. - :rtype: binary - """ + def pos_self_order_get_image(self, product_id, image_size=128, **kw): + # This controller is public and does not require an access code (access_token) because the user + # needs to see the product image in "menu" mode. In this mode, the user has no access_token. if not get_any_pos_config_sudo(): raise werkzeug.exceptions.Unauthorized() @@ -103,23 +105,3 @@ class PosQRMenuController(http.Controller): ) .get_response() ) - - @http.route( - "/menu/get-bg-image/", methods=["GET"], type="http", auth="public" - ) - def pos_self_order_get_bg_image(self, pos_config_id: int): - """ - Gets the background image for this self order - :return: the bg image - :rtype: binary - """ - pos_config_sudo = request.env["pos.config"].sudo().browse(pos_config_id) - - if not pos_config_sudo.self_order_image: - raise werkzeug.exceptions.NotFound() - - return ( - request.env["ir.binary"] - ._get_image_stream_from(pos_config_sudo, field_name="self_order_image") - .get_response() - ) diff --git a/addons/pos_self_order/controllers/orders.py b/addons/pos_self_order/controllers/orders.py index 7ff30a0bc1b..45f6958b117 100644 --- a/addons/pos_self_order/controllers/orders.py +++ b/addons/pos_self_order/controllers/orders.py @@ -1,30 +1,31 @@ # -*- coding: utf-8 -*- - -from datetime import timedelta +import re import uuid -from odoo import http, fields, Command +from datetime import timedelta +from odoo import http, fields from odoo.http import request from odoo.addons.pos_self_order.controllers.utils import reduce_privilege 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, access_token, table_identifier): + @http.route("/pos-self-order/process-new-order//", auth="public", type="json", website=True) + def process_new_order(self, order, access_token, table_identifier, device_type): lines = order.get('lines') pos_config, table = self._verify_authorization(access_token, table_identifier) pos_session = pos_config.current_session_id - sequence_number = self._get_sequence_number(table.id, pos_session.id) - unique_id = self._generate_unique_id(pos_session.id, table.id, sequence_number) + ir_sequence = pos_config.env['ir.sequence'].with_context(company_id=pos_config.company_id.id).next_by_code(f'pos.order_{pos_session.id}') + sequence_number = re.findall(r'\d+', ir_sequence)[0] + order_reference = self._generate_unique_id(pos_session.id, table.id if table else 0, sequence_number, device_type) # 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, + 'name': order_reference, 'sequence_number': sequence_number, + 'uuid': order.get('uuid'), + 'take_away': order.get('take_away'), + 'user_id': request.session.uid, 'access_token': uuid.uuid4().hex, 'pos_session_id': pos_session.id, 'table_id': table.id if table else False, @@ -37,6 +38,7 @@ class PosSelfOrderController(http.Controller): 'amount_total': 0, 'amount_paid': 0, 'amount_return': 0, + 'tracking_number': self._generate_unique_tracking_number(pos_config, order), # FIXME this is not unique }, 'to_invoice': False, 'session_id': pos_session.id, @@ -46,30 +48,45 @@ class PosSelfOrderController(http.Controller): posted_order_id = pos_config.env['pos.order'].create_from_ui([order], draft=True)[0].get('id') # Process the lines and get their prices computed - lines = self._process_lines(lines, pos_config, posted_order_id) + lines = self._process_lines(lines, pos_config, posted_order_id, order.get('take_away')) # Compute the order prices amount_total, amount_untaxed = self._get_order_prices(lines) # Update the order with the computed prices and lines order = pos_config.env["pos.order"].browse(posted_order_id) + + classic_lines = [] + combo_lines = [] + for line in lines: + if line["combo_parent_uuid"]: + combo_lines.append(line) + else: + classic_lines.append(line) + + # combo lines must be created after classic_line, as they need the classic line identifier + lines = pos_config.env['pos.order.line'].create(classic_lines) + lines += pos_config.env['pos.order.line'].create(combo_lines) order.write({ - 'lines': [Command.create(line) for line in lines], + 'lines': lines, 'amount_tax': amount_total - amount_untaxed, 'amount_total': amount_total, }) + order.send_table_count_notification(order.table_id) return order._export_for_self_order() @http.route('/pos-self-order/get-orders-taxes', auth='public', type='json', website=True) def get_order_taxes(self, order, access_token): pos_config = self._verify_pos_config(access_token) - lines = self._process_lines(order.get('lines'), pos_config, 0) + lines = self._process_lines(order.get('lines'), pos_config, 0, order.get('take_away')) amount_total, amount_untaxed = self._get_order_prices(lines) return { 'lines': [{ 'uuid': line.get('uuid'), + 'price_unit': line.get('price_unit'), + 'price_extra': line.get('price_extra'), 'price_subtotal': line.get('price_subtotal'), 'price_subtotal_incl': line.get('price_subtotal_incl'), } for line in lines], @@ -87,7 +104,6 @@ class PosSelfOrderController(http.Controller): pos_order = session.order_ids.filtered_domain([ ('id', '=', order_id), ('access_token', '=', order_access_token), - ('table_id', '=', table.id) ]) if not pos_order: @@ -95,17 +111,19 @@ class PosSelfOrderController(http.Controller): elif pos_order.state != 'draft': raise Unauthorized("Order is not in draft state") - lines = self._process_lines(order.get('lines'), pos_config, pos_order.id) + lines = self._process_lines(order.get('lines'), pos_config, pos_order.id, order.get('take_away')) for line in lines: if line.get('id'): - order_line = pos_order.lines.browse(line.get('id')) + # we need to find by uuid because each time we update the order, id of orderlines changed. + order_line = pos_order.lines.filtered(lambda l: l.uuid == line.get('uuid')) if line.get('qty') < order_line.qty: line.set('qty', order_line.qty) - order_line.write({ - **line, - }) + if order_line: + order_line.write({ + **line, + }) else: pos_order.lines.create(line) @@ -114,7 +132,7 @@ class PosSelfOrderController(http.Controller): 'amount_tax': amount_total - amount_untaxed, 'amount_total': amount_total, }) - + pos_order.send_table_count_notification(pos_order.table_id) return pos_order._export_for_self_order() @http.route('/pos-self-order/get-orders', auth='public', type='json', website=True) @@ -129,11 +147,11 @@ class PosSelfOrderController(http.Controller): if not orders: raise NotFound("Orders not found") - orders = [] + orders_for_ui = [] for order in orders: - orders.append(order._export_for_self_order()) + orders_for_ui.append(order._export_for_self_order()) - return orders + return orders_for_ui @http.route('/pos-self-order/get-tables', auth='public', type='json', website=True) def get_tables(self, access_token): @@ -145,18 +163,100 @@ class PosSelfOrderController(http.Controller): return tables - def _process_lines(self, lines, pos_config, pos_order_id): + + @http.route('/kiosk/payment//', auth='public', type='json', website=True) + def pos_self_order_kiosk_payment(self, pos_config_id, order, payment_method_id, access_token, device_type): + order_dict = self.process_new_order(order, access_token, None, device_type) + + if not order_dict.get('id'): + raise BadRequest("Something went wrong") + + # access_token verified in process_new_order + order_sudo = request.env['pos.order'].sudo().browse(order_dict.get('id')) + payment_method_sudo = request.env["pos.payment.method"].sudo().browse(payment_method_id) + if not order_sudo or not payment_method_sudo or payment_method_sudo not in order_sudo.config_id.payment_method_ids: + raise NotFound("Order or payment method not found") + + status = payment_method_sudo.payment_request_from_kiosk(order_sudo) + + if not status: + raise BadRequest("Something went wrong") + + return order_sudo._export_for_self_order() + + def _generate_unique_tracking_number(self, pos_config, order_dict): + last_tracking_generated = pos_config.env['pos.order'].search([ + ('tracking_number', '!=', False), + ('date_order', '>=', fields.Datetime.now() - timedelta(days=1)), + ], order='tracking_number desc', limit=1) + + return int(last_tracking_generated.tracking_number) + 1 if last_tracking_generated else 1 + + def _process_lines(self, lines, pos_config, pos_order_id, take_away=False): + appended_uuid = [] newLines = [] pricelist = pos_config.pricelist_id + if take_away and pos_config.self_order_kiosk: + config_fiscal_pos = pos_config.self_order_kiosk_alternative_fp_id + else: + config_fiscal_pos = pos_config.default_fiscal_position_id for line in lines: - product = pos_config.env['product.product'].browse(line.get('product_id')) + if line.get('uuid') in appended_uuid: + continue + product = pos_config.env['product.product'].browse(int(line.get('product_id'))) # todo take into account the price extra price_unit = pricelist._get_product_price(product, quantity=line.get('qty')) if pricelist else product.lst_price - - config_fiscal_pos = pos_config.default_fiscal_position_id selected_account_tax = config_fiscal_pos.map_tax(product.taxes_id) if config_fiscal_pos else product.taxes_id + # parent_product_taxe_ids = None + children = [l for l in lines if l.get('combo_parent_uuid') == line.get('uuid')] + if len(children) > 0: + total_price = 0 + unit_price_by_id = {} + for child in children: + product = pos_config.env['product.product'].browse(int(child.get('product_id'))) + child_selected_account_tax = config_fiscal_pos.map_tax(product.taxes_id) if config_fiscal_pos else product.taxes_id + tax_results = child_selected_account_tax.compute_all( + pricelist._get_product_price(product, quantity=line.get('qty')) if pricelist else product.lst_price, + pos_config.currency_id, + child.get('qty'), + product, + ) + unit_price_by_id[child['uuid']] = pricelist._get_product_price(product, quantity=line.get('qty')) if pricelist else product.lst_price + total_price += tax_results.get('total_included') if pos_config.iface_tax_included == 'total' else tax_results.get('total_excluded') + ratio = (price_unit / total_price) + for child in children: + child_line_combo = pos_config.env['pos.combo'].browse(int(child.get('combo_id'))) + child_line_combo_line = child_line_combo.combo_line_ids.filtered(lambda l: l.product_id.id == child.get('product_id'))[0] + child_product = pos_config.env["product.product"].browse(int(child.get('product_id'))) + child_price_unit = ratio * unit_price_by_id[child['uuid']] + child_line_combo_line.price + child_selected_account_tax = config_fiscal_pos.map_tax(child_product.taxes_id) if config_fiscal_pos else child_product.taxes_id + child_tax_results = child_selected_account_tax.compute_all( + child_price_unit, + pos_config.currency_id, + child.get('qty'), + child_product, + ) + newLines.append({ + 'price_unit': price_unit, + 'price_subtotal': child_tax_results.get('total_excluded'), + 'price_subtotal_incl': child_tax_results.get('total_included'), + 'id': child.get('id'), + 'order_id': pos_order_id, + 'tax_ids': product.taxes_id, + 'uuid': child.get('uuid'), + 'product_id': child.get('product_id'), + 'qty': child.get('qty'), + 'customer_note': child.get('customer_note'), + 'selected_attributes': child.get('selected_attributes'), + 'full_product_name': child.get('full_product_name'), + 'combo_parent_uuid': child.get('combo_parent_uuid'), + 'combo_id': child.get('combo_id'), + }) + appended_uuid.append(child.get('uuid')) + price_unit = 0 + tax_results = selected_account_tax.compute_all( price_unit, pos_config.currency_id, @@ -168,7 +268,6 @@ class PosSelfOrderController(http.Controller): '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.taxes_id, @@ -178,7 +277,10 @@ class PosSelfOrderController(http.Controller): 'customer_note': line.get('customer_note'), 'selected_attributes': line.get('selected_attributes'), 'full_product_name': line.get('full_product_name'), + 'combo_parent_uuid': line.get('combo_parent_uuid'), + 'combo_id': line.get('combo_id'), }) + appended_uuid.append(line.get('uuid')) return newLines @@ -192,20 +294,13 @@ class PosSelfOrderController(http.Controller): # 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: + def _generate_unique_id(self, pos_session_id, table_id, sequence_number, device_type): 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, session_id): - 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 + device = "Kiosk" if device_type == "kiosk" else "Self-Order" + return f"{device} {first_part}-{second_part}-{third_part}" def _verify_pos_config(self, access_token): """ @@ -213,7 +308,7 @@ class PosSelfOrderController(http.Controller): The record is has no sudo access and is in the context of the record's company and current pos.session's user. """ pos_config_sudo = request.env['pos.config'].sudo().search([('access_token', '=', access_token)], limit=1) - if not pos_config_sudo or not pos_config_sudo.self_order_table_mode or not pos_config_sudo.has_active_session: + if not pos_config_sudo or (not pos_config_sudo.self_order_table_mode and not pos_config_sudo.self_order_kiosk) or not pos_config_sudo.has_active_session: raise Unauthorized("Invalid access token") company = pos_config_sudo.company_id user = pos_config_sudo.current_session_id.user_id @@ -224,11 +319,12 @@ class PosSelfOrderController(http.Controller): Similar to _verify_pos_config but also looks for the restaurant.table of the given identifier. The restaurant.table record is also returned with reduced privileges. """ + pos_config = self._verify_pos_config(access_token) table_sudo = request.env["restaurant.table"].sudo().search([('identifier', '=', table_identifier)], limit=1) - if not table_sudo: + + if not table_sudo and not pos_config.self_order_kiosk: raise Unauthorized("Table not found") - pos_config = self._verify_pos_config(access_token) company = pos_config.company_id user = pos_config.current_session_id.user_id table = reduce_privilege(table_sudo, company, user) diff --git a/addons/pos_self_order/controllers/utils.py b/addons/pos_self_order/controllers/utils.py index 8aed1a0c58c..9d4ebd5edb9 100644 --- a/addons/pos_self_order/controllers/utils.py +++ b/addons/pos_self_order/controllers/utils.py @@ -12,7 +12,7 @@ def get_any_pos_config_sudo() -> PosConfig: or raises a NotFound otherwise """ return ( - request.env["pos.config"].sudo().search([("self_order_view_mode", "=", True)], limit=1) + request.env["pos.config"].sudo().search([('|'), ("self_order_view_mode", "=", True), ("self_order_kiosk", "=", True)], limit=1) ) or _raise(werkzeug.exceptions.NotFound()) def _raise(e): diff --git a/addons/pos_self_order/models/__init__.py b/addons/pos_self_order/models/__init__.py index 3b6f3f5d121..adbaac71920 100644 --- a/addons/pos_self_order/models/__init__.py +++ b/addons/pos_self_order/models/__init__.py @@ -4,6 +4,7 @@ from . import ir_http from . import pos_config from . import pos_order from . import pos_restaurant +from . import pos_payment_method from . import pos_self_order_custom_link from . import product_product from . import res_config_settings diff --git a/addons/pos_self_order/models/pos_config.py b/addons/pos_self_order/models/pos_config.py index 844170d39b5..9db21abc961 100644 --- a/addons/pos_self_order/models/pos_config.py +++ b/addons/pos_self_order/models/pos_config.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from typing import Optional, List, Dict, Callable +import io import uuid -from werkzeug.urls import url_quote import base64 +from PIL import Image +from typing import Optional, List, Dict +from werkzeug.urls import url_quote +from odoo.tools import image_to_base64 - -from odoo import api, fields, models, modules +from odoo import api, fields, models, modules, _ from odoo.tools import file_open, split_every -from odoo.addons.pos_self_order.models.product_product import ProductProduct - class PosConfig(models.Model): _inherit = "pos.config" @@ -25,6 +25,84 @@ class PosConfig(models.Model): ) return base64.b64encode(file_open(image_path, "rb").read()) + def _self_order_kiosk_default_languages(self): + return self.env["res.lang"].get_installed() + + status = fields.Selection( + [("inactive", "Inactive"), ("active", "Active")], + string="Status", + compute="_compute_status", + store=False, + ) + self_order_kiosk_url = fields.Char(compute="_compute_self_order_kiosk_url") + self_order_kiosk = fields.Boolean( + string="Kiosk", + help="Enable the kiosk mode for the Point of Sale", + ) + self_order_kiosk_takeaway = fields.Boolean( + string="Takeaway", + help="Allow customers to order for takeaway", + ) + self_order_kiosk_alternative_fp_id = fields.Many2one( + 'account.fiscal.position', + string='Alternative Fiscal Position', + help='This is useful for restaurants with onsite and take-away services that imply specific tax rates.', + ) + self_order_kiosk_mode = fields.Selection( + [("counter", "Pickup Counter"), ("table", "Service at Table")], + string="Kiosk Mode", + default="counter", + help="Choose the kiosk mode", + required=True, + ) + self_order_kiosk_available_language_ids = fields.Many2many( + "res.lang", + string="Available Languages", + help="Languages available for the kiosk mode", + default=_self_order_kiosk_default_languages, + ) + self_order_kiosk_default_language = fields.Many2one( + "res.lang", + string="Default Language", + help="Default language for the kiosk mode", + default=lambda self: self.env["res.lang"].search( + [("code", "=", self.env.lang)], limit=1 + ), + ) + self_order_kiosk_image_home = fields.Image( + string="Self Order Kiosk Image Home", + help="Image to display on the self order screen", + max_width=1080, + max_height=1920, + default=_self_order_default_image, + ) + self_order_kiosk_image_eat = fields.Image( + string="Self Order Kiosk Image Eat", + help="Image to display on the self order screen", + max_width=1080, + max_height=1920, + default=_self_order_default_image, + ) + self_order_kiosk_image_brand = fields.Image( + string="Self Order Kiosk Image Brand", + help="Image to display on the self order screen", + max_width=1200, + max_height=250, + ) + self_order_kiosk_image_home_name = fields.Char( + string="Self Order Kiosk Image Home Name", + help="Name of the image to display on the self order screen", + default=_self_order_default_image_name, + ) + self_order_kiosk_image_eat_name = fields.Char( + string="Self Order Kiosk Image Eat Name", + help="Name of the image to display on the self order screen", + default=_self_order_default_image_name, + ) + self_order_kiosk_image_brand_name = fields.Char( + string="Self Order Kiosk Image Brand Name", + help="Name of the image to display on the self order screen", + ) self_order_view_mode = fields.Boolean( string="QR Code Menu", help="Allow customers to view the menu on their phones by scanning the QR code on the table", @@ -68,6 +146,13 @@ class PosConfig(models.Model): self.access_token = self._get_access_token() self.floor_ids.table_ids._update_identifier() + @api.onchange("self_order_kiosk") + def _self_order_kiosk_change(self): + for record in self: + if record.self_order_kiosk: + record.self_order_view_mode = False + record.self_order_table_mode = False + @api.model def _init_access_token(self): pos_config_ids = self.env["pos.config"].search([]) @@ -101,6 +186,40 @@ class PosConfig(models.Model): record.self_order_view_mode = False record.self_order_table_mode = False + def _get_qr_code_data(self): + self.ensure_one() + + table_qr_code = [] + if self.self_order_table_mode: + table_qr_code.extend([{ + 'name': floor.name, + 'type': 'table', + 'tables': [ + { + 'identifier': table.identifier, + 'id': table.id, + 'name': table.name, + 'url': self._get_self_order_url(table.id), + } + for table in floor.table_ids.filtered("active") + ] + } + for floor in self.floor_ids] + ) + + # Here we use "range" to determine the number of QR codes to generate from + # this list, which will then be inserted into a PDF. + table_qr_code.extend([{ + 'name': 'Generic', + 'type': 'default', + 'tables': [{ + 'id': i, + 'url': self._get_self_order_url(), + } for i in range(0, 11)] + }]) + + return table_qr_code + def _get_self_order_route(self, table_id: Optional[int] = None) -> str: self.ensure_one() base_route = f"/menu/{self.id}" @@ -155,10 +274,7 @@ class PosConfig(models.Model): ) ) - def _get_available_products(self) -> ProductProduct: - """ - This function returns the products that are available in the given PosConfig - """ + def _get_available_products(self): self.ensure_one() return ( self.env["product.product"] @@ -175,47 +291,144 @@ class PosConfig(models.Model): ) ) + def _get_available_categories(self): + self.ensure_one() + return ( + self.env["pos.category"] + .search( + [ + *( + self.limit_categories + and self.iface_available_categ_ids + and [("id", "in", self.iface_available_categ_ids.ids)] + or [] + ), + ], + order="sequence", + ) + ) + def _get_self_order_data(self) -> Dict: self.ensure_one() return { "pos_config_id": self.id, + "iface_start_categ_id": self.iface_start_categ_id.id, "company_name": self.company_id.name, + "company_color": self.company_id.color, "currency_id": self.currency_id.id, "show_prices_with_tax_included": self.iface_tax_included == "total", "custom_links": self._get_self_order_custom_links(), "products": self._get_available_products()._get_self_order_data(self), - "pos_category": self.env['pos.category'].search_read(fields=["name", "sequence"], order="sequence"), + "combos": self._get_combos_data(), + "pos_category": self._get_available_categories().read(["name", "sequence", "has_image"]), "has_active_session": self.has_active_session, } - def _get_qr_code_data(self): + def _get_combos_data(self): + self.ensure_one() + combos = self.env["pos.combo"].search([]) + + return[{ + 'id': combo.id, + 'name': combo.name, + 'combo_line_ids': combo.combo_line_ids.read(['product_id', 'price', 'lst_price', 'combo_id']) + } for combo in combos] + + def _get_self_order_mobile_data(self): + self.ensure_one() + return { + **self._get_self_order_data(), + "mobile_image_home": self._get_kiosk_image(self.self_order_image), + } + + def _get_self_order_kiosk_data(self): + self.ensure_one() + payment_search_params = self.current_session_id._loader_params_pos_payment_method() + payment_methods = self.payment_method_ids.filtered(lambda p: p.use_payment_terminal == 'adyen').read(payment_search_params['search_params']['fields']) + default_language = self.self_order_kiosk_default_language.read(["code", "name", "iso_code", "flag_image_url"]) + + return { + **self._get_self_order_data(), + "avaiable_payment_methods": self.payment_method_ids.ids, + "pos_payment_methods": payment_methods, + "pos_session": self.current_session_id.read(["id", "access_token"])[0] if self.current_session_id else [], + "kiosk_mode": self.self_order_kiosk_mode, + "kiosk_takeaway": self.self_order_kiosk_takeaway, + "kiosk_alternative_fp": self.self_order_kiosk_alternative_fp_id.id, + "kiosk_image_home": self._get_kiosk_image(self.self_order_kiosk_image_home), + "kiosk_image_eat": self._get_kiosk_image(self.self_order_kiosk_image_eat), + "kiosk_image_brand": self._get_kiosk_image(self.self_order_kiosk_image_brand), + "kiosk_default_language": default_language[0] if default_language else [], + "kiosk_available_languages": self.self_order_kiosk_available_language_ids.read(["code", "display_name", "iso_code", "flag_image_url"]), + } + + def _get_kiosk_image(self, image): + image = Image.open(io.BytesIO(base64.b64decode(image))) if image else False + return image_to_base64(image, 'PNG').decode('utf-8') if image else False + + def _split_qr_codes_list(self, floors: List[Dict], cols: int) -> List[Dict]: + """ + :floors: the list of floors + :cols: the number of qr codes per row + """ + self.ensure_one() + return [ + { + "name": floor.get("name"), + "rows_of_tables": list(split_every(cols, floor["tables"], list)), + } + for floor in floors + ] + + def _compute_self_order_kiosk_url(self): + for record in self: + domain = self.get_base_url() + record.self_order_kiosk_url = '%s/kiosk/%d?access_token=%s' % (domain, record.id, record.access_token) + + def action_close_kiosk_session(self): + current_session_id = self.current_session_id + + if current_session_id: + if current_session_id.order_ids: + current_session_id.order_ids.filtered(lambda o: o.state not in ['paid', 'invoiced']).unlink() + + self.env['bus.bus']._sendone(f'pos_config-{self.access_token}', 'status', { + 'status': 'closed', + }) + + return current_session_id.action_pos_session_closing_control() + + def _compute_status(self): + for record in self: + record.status = 'active' if record.has_active_session else 'inactive' + + def action_open_kiosk(self): self.ensure_one() - table_qr_code = [] - if self.self_order_table_mode: - table_qr_code.extend([{ - 'name': floor.name, - 'type': 'table', - 'tables': [ - { - 'identifier': table.identifier, - 'id': table.id, - 'name': table.name, - 'url': self._get_self_order_url(table.id), - } - for table in floor.table_ids.filtered("active") - ] - } - for floor in self.floor_ids] - ) + return { + 'name': _('Self Kiosk'), + 'type': 'ir.actions.act_url', + 'url': self.self_order_kiosk_url, + } - table_qr_code.extend([{ - 'name': 'Generic', - 'type': 'default', - 'tables': [{ - 'id': i, - 'url': self._get_self_order_url(), - } for i in range(0, 11)] - }]) + def action_open_wizard(self): + self.ensure_one() - return table_qr_code + if not self.current_session_id: + pos_session = self.env['pos.session'].create({'user_id': self.env.uid, 'config_id': self.id}) + pos_session._ensure_access_token() + self.env['bus.bus']._sendone(f'pos_config-{self.access_token}', 'status', { + 'status': 'open', + 'pos_session': pos_session.read(['id', 'access_token'])[0], + }) + + return { + 'name': _('Self Kiosk'), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'pos.config', + 'res_id': self.id, + 'target': 'new', + 'views': [(self.env.ref('pos_self_order.pos_self_order_kiosk_read_only_form_dialog').id, 'form')], + } diff --git a/addons/pos_self_order/models/pos_order.py b/addons/pos_self_order/models/pos_order.py index 2d76dda0a44..d0aed71c332 100644 --- a/addons/pos_self_order/models/pos_order.py +++ b/addons/pos_self_order/models/pos_order.py @@ -11,6 +11,29 @@ class PosOrderLine(models.Model): # For the moment we need this to keep attributes consistency between the server and client_side. selected_attributes = fields.Json(string="Selected Attributes") + combo_parent_id = fields.Many2one('pos.order.line', string='Combo Parent') + combo_line_ids = fields.One2many('pos.order.line', 'combo_parent_id', string='Combo Lines') + combo_id = fields.Many2one('pos.combo', string='Combo line reference') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if (vals.get('combo_parent_uuid')): + vals.update([ + ('combo_parent_id', self.search([('uuid', '=', vals.get('combo_parent_uuid'))]).id) + ]) + if 'combo_parent_uuid' in vals: + del vals['combo_parent_uuid'] + return super().create(vals_list) + + def write(self, vals): + if (vals.get('combo_parent_uuid')): + vals.update([ + ('combo_parent_id', self.search([('uuid', '=', vals.get('combo_parent_uuid'))]).id) + ]) + if 'combo_parent_uuid' in vals: + del vals['combo_parent_uuid'] + return super().write(vals) # FIXME: uuid already pass in pos and move note in pos_restaurant. def _export_for_ui(self, orderline): @@ -24,6 +47,9 @@ class PosOrderLine(models.Model): class PosOrder(models.Model): _inherit = "pos.order" + tracking_number = fields.Integer(string="Tracking Number") + take_away = fields.Boolean(string="Take Away", default=False) + @api.model def create_from_ui(self, orders, draft=False): orders = super().create_from_ui(orders, draft) @@ -39,6 +65,14 @@ class PosOrder(models.Model): return super().remove_from_ui(server_ids) + def _order_fields(self, ui_order): + fields = super()._order_fields(ui_order) + fields.update({ + 'tracking_number': ui_order.get('tracking_number'), + 'take_away': ui_order.get('take_away'), + }) + return fields + def _send_notification(self, order_ids): for order in order_ids: if order.access_token and order.state != 'draft': @@ -53,8 +87,10 @@ class PosOrder(models.Model): return { "id": self.id, "pos_config_id": self.config_id.id, + "take_away": self.take_away, "pos_reference": self.pos_reference, "access_token": self.access_token, + "tracking_number": self.tracking_number, "state": self.state, "date_order": str(self.date_order), "amount_total": self.amount_total, diff --git a/addons/pos_self_order/models/pos_payment_method.py b/addons/pos_self_order/models/pos_payment_method.py new file mode 100644 index 00000000000..2a619307e39 --- /dev/null +++ b/addons/pos_self_order/models/pos_payment_method.py @@ -0,0 +1,9 @@ +from odoo import models + + +class PosPaymentMethod(models.Model): + _inherit = "pos.payment.method" + + # will be overridden. + def payment_request_from_kiosk(self, order): + pass diff --git a/addons/pos_self_order/models/product_product.py b/addons/pos_self_order/models/product_product.py index 169db5f1ba5..5cf496ce303 100644 --- a/addons/pos_self_order/models/product_product.py +++ b/addons/pos_self_order/models/product_product.py @@ -66,9 +66,6 @@ class ProductProduct(models.Model): def _get_price_info( self, pos_config: PosConfig, price: Optional[float] = None, qty: int = 1 ) -> Dict[str, float]: - """ - Function that returns a dict with the price info of a given product - """ self.ensure_one() # if price == None it means that a price was not passed as a parameter, so we use the product's list price # it could happen that a price was passed, but it was 0; in that case we want to use this 0 as the argument, @@ -77,32 +74,36 @@ class ProductProduct(models.Model): price = pos_config.pricelist_id._get_product_price( self, qty, currency=pos_config.currency_id ) + price_info = pos_config.default_fiscal_position_id.map_tax(self.taxes_id).compute_all( price, pos_config.currency_id, qty, product=self ) + display_price = price_info["total_included"] if pos_config.iface_tax_included == "total" else price_info["total_excluded"] + display_price = self.lst_price if self.combo_ids else display_price + return { - "list_price": price_info["total_included"] - if pos_config.iface_tax_included == "total" - else price_info["total_excluded"], - "price_without_tax": price_info["total_excluded"], - "price_with_tax": price_info["total_included"], + "display_price": display_price, + "lst_price": self.lst_price } - def _get_self_order_data(self, pos_config: PosConfig) -> List[Dict]: - """ - returns the list of products with the necessary info for the self order app - """ - return [ - { - "price_info": product._get_price_info(pos_config), - "has_image": bool(product.image_1920), - "attributes": product._get_attributes(pos_config), - "name": product._get_name(), - "id": product.id, - "description_sale": product.description_sale, - "pos_categ_ids": product.pos_categ_ids.mapped("name") or ["Other"], - "is_pos_groupable": product.uom_id.is_pos_groupable, + def _get_product_for_ui(self, pos_config): + self.ensure_one() + return { + "price_info": self._get_price_info(pos_config), + "has_image": bool(self.image_1920), + "attributes": self._get_attributes(pos_config), + "name": self._get_name(), + "id": self.id, + "description_sale": self.description_sale, + "pos_categ_ids": self.pos_categ_ids.mapped("name") or ["Other"], + "pos_combo_ids": self.combo_ids.mapped("id") or False, + "is_pos_groupable": self.uom_id.is_pos_groupable, + "write_date": self.write_date.timestamp(), } + + def _get_self_order_data(self, pos_config: PosConfig) -> List[Dict]: + return [ + product._get_product_for_ui(pos_config) for product in self ] diff --git a/addons/pos_self_order/models/res_config_settings.py b/addons/pos_self_order/models/res_config_settings.py index 690de0e0ae9..ba8b2c55ade 100644 --- a/addons/pos_self_order/models/res_config_settings.py +++ b/addons/pos_self_order/models/res_config_settings.py @@ -8,6 +8,18 @@ from odoo.tools.misc import split_every class ResConfigSettings(models.TransientModel): _inherit = "res.config.settings" + pos_self_order_kiosk = fields.Boolean(related="pos_config_id.self_order_kiosk", readonly=False) + pos_self_order_kiosk_mode = fields.Selection(related="pos_config_id.self_order_kiosk_mode", readonly=False) + pos_self_order_kiosk_takeaway = fields.Boolean(related="pos_config_id.self_order_kiosk_takeaway", readonly=False) + pos_self_order_kiosk_alternative_fp_id = fields.Many2one(related="pos_config_id.self_order_kiosk_alternative_fp_id", readonly=False) + pos_self_order_kiosk_image_home = fields.Image(related="pos_config_id.self_order_kiosk_image_home", readonly=False) + pos_self_order_kiosk_image_eat = fields.Image(related="pos_config_id.self_order_kiosk_image_eat", readonly=False) + pos_self_order_kiosk_image_brand = fields.Image(related="pos_config_id.self_order_kiosk_image_brand", readonly=False) + pos_self_order_kiosk_image_home_name = fields.Char(related="pos_config_id.self_order_kiosk_image_home_name", readonly=False) + pos_self_order_kiosk_image_eat_name = fields.Char(related="pos_config_id.self_order_kiosk_image_eat_name", readonly=False) + pos_self_order_kiosk_image_brand_name = fields.Char(related="pos_config_id.self_order_kiosk_image_brand_name", readonly=False) + pos_self_order_kiosk_available_language_ids = fields.Many2many(related="pos_config_id.self_order_kiosk_available_language_ids", readonly=False) + pos_self_order_kiosk_default_language = fields.Many2one(related="pos_config_id.self_order_kiosk_default_language", readonly=False) pos_self_order_view_mode = fields.Boolean( compute="_compute_pos_module_pos_self_order", store=True, readonly=False ) @@ -33,6 +45,22 @@ class ResConfigSettings(models.TransientModel): compute="_compute_pos_module_pos_self_order", store=True, readonly=False ) + @api.onchange("pos_self_order_kiosk_default_language", "pos_self_order_kiosk_available_language_ids") + def _onchange_pos_self_order_kiosk_default_language(self): + if self.pos_self_order_kiosk_default_language not in self.pos_self_order_kiosk_available_language_ids: + self.pos_self_order_kiosk_available_language_ids = self.pos_self_order_kiosk_available_language_ids + self.pos_self_order_kiosk_default_language + if not self.pos_self_order_kiosk_default_language and self.pos_self_order_kiosk_available_language_ids: + self.pos_self_order_kiosk_default_language = self.pos_self_order_kiosk_available_language_ids[0] + + @api.onchange("pos_self_order_kiosk") + def _self_order_kiosk_change(self): + for record in self: + record.is_kiosk_mode = record.pos_self_order_kiosk + + if record.pos_config_id.self_order_kiosk: + record.pos_config_id.self_order_view_mode = False + record.pos_config_id.self_order_table_mode = False + @api.depends("pos_config_id") def _compute_pos_module_pos_self_order(self): for res_config in self: diff --git a/addons/pos_self_order/static/img/eatin.svg b/addons/pos_self_order/static/img/eatin.svg new file mode 100644 index 00000000000..9c1b11294da --- /dev/null +++ b/addons/pos_self_order/static/img/eatin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/pos_self_order/static/img/takeAway.svg b/addons/pos_self_order/static/img/takeAway.svg new file mode 100644 index 00000000000..8f00f7ad026 --- /dev/null +++ b/addons/pos_self_order/static/img/takeAway.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/pos_self_order/static/src/hooks/use_scroll_direction.js b/addons/pos_self_order/static/src/common/hooks/use_scroll_direction.js similarity index 100% rename from addons/pos_self_order/static/src/hooks/use_scroll_direction.js rename to addons/pos_self_order/static/src/common/hooks/use_scroll_direction.js diff --git a/addons/pos_self_order/static/src/common/models/combo.js b/addons/pos_self_order/static/src/common/models/combo.js new file mode 100644 index 00000000000..957ed50045d --- /dev/null +++ b/addons/pos_self_order/static/src/common/models/combo.js @@ -0,0 +1,22 @@ +/** @odoo-module **/ +import { Reactive } from "@web/core/utils/reactive"; +import { ComboLine } from "./combo_line"; + +export class Combo extends Reactive { + constructor({ id, name, combo_line_ids }) { + super(); + this.setup(...arguments); + } + + setup(combo) { + // server only data (recovered after first send to server) + this.id = combo.id || null; + this.name = combo.name || null; + this.combo_line_ids = combo.combo_line_ids || []; + this.initLines(); + } + + initLines() { + this.combo_line_ids = this.combo_line_ids.map((line) => new ComboLine(line)); + } +} diff --git a/addons/pos_self_order/static/src/common/models/combo_line.js b/addons/pos_self_order/static/src/common/models/combo_line.js new file mode 100644 index 00000000000..5d847f84103 --- /dev/null +++ b/addons/pos_self_order/static/src/common/models/combo_line.js @@ -0,0 +1,18 @@ +/** @odoo-module **/ +import { Reactive } from "@web/core/utils/reactive"; + +export class ComboLine extends Reactive { + constructor({ id, lst_price, price, product_id, combo_id }) { + super(); + this.setup(...arguments); + } + + setup(comboLine) { + // server only data (recovered after first send to server) + this.id = comboLine.id || null; + this.lst_price = comboLine.lst_price || null; + this.price = comboLine.price || null; + this.product_id = comboLine.product_id || null; + this.combo_id = comboLine.combo_id || null; + } +} diff --git a/addons/pos_self_order/static/src/models/line.js b/addons/pos_self_order/static/src/common/models/line.js similarity index 61% rename from addons/pos_self_order/static/src/models/line.js rename to addons/pos_self_order/static/src/common/models/line.js index f366a00e30b..fd63ac48ad8 100644 --- a/addons/pos_self_order/static/src/models/line.js +++ b/addons/pos_self_order/static/src/common/models/line.js @@ -10,9 +10,13 @@ export class Line extends Reactive { qty, customer_note, full_product_name, + price_unit, price_subtotal_incl, price_subtotal, selected_attributes, + combo_parent_uuid, + combo_id, + child_lines, }) { super(); this.setup(...arguments); @@ -26,9 +30,13 @@ export class Line extends Reactive { this.product_id = line.product_id; this.qty = line.qty ? line.qty : 0; this.customer_note = line.customer_note; + this.price_unit = line.price_unit || 0; this.price_subtotal_incl = line.price_subtotal_incl || 0; this.price_subtotal = line.price_subtotal || 0; this.selected_attributes = line.selected_attributes || {}; + this.combo_parent_uuid = line.combo_parent_uuid || null; + this.combo_id = line.combo_id || null; + this.child_lines = line.child_lines || []; } isChange(lastChange) { @@ -40,4 +48,21 @@ export class Line extends Reactive { return false; } + + get attributes() { + return Object.entries(this.selected_attributes).map(([key, value]) => { + return { name: key, value }; + }); + } + + get displayPrice() { + // for the moment we use price with tax included but we should look at the config setup + return this.price_subtotal_incl; + } + + updateDataFromServer(data) { + for (const key in data) { + this[key] = data[key]; + } + } } diff --git a/addons/pos_self_order/static/src/models/order.js b/addons/pos_self_order/static/src/common/models/order.js similarity index 73% rename from addons/pos_self_order/static/src/models/order.js rename to addons/pos_self_order/static/src/common/models/order.js index b55f8a0b35b..2a86c494b04 100644 --- a/addons/pos_self_order/static/src/models/order.js +++ b/addons/pos_self_order/static/src/common/models/order.js @@ -13,7 +13,9 @@ export class Order extends Reactive { date, amount_total, amount_tax, + tracking_number, lastChangesSent, + take_away, }) { super(); this.setup(...arguments); @@ -30,6 +32,8 @@ export class Order extends Reactive { this.amount_total = order.amount_total || 0; this.amount_tax = order.amount_tax || 0; this.lines = order.lines || []; + this.tracking_number = order.tracking_number || null; + this.take_away = order.take_away || false; // data this.lastChangesSent = order.lastChangesSent || {}; @@ -45,10 +49,23 @@ export class Order extends Reactive { return this.lines.reduce((acc, line) => (acc += line.qty), 0); } + get isSavedOnServer() { + return this.isAlreadySent && !this.hasNotAllLinesSent(); + } + initLines() { this.lines = this.lines.map((line) => new Line(line)); } + removeLine(lineUuid) { + this.lines = this.lines.filter((line) => line.uuid !== lineUuid); + for (const line of this.lines) { + if (line.combo_parent_uuid === lineUuid) { + this.removeLine(line.uuid); + } + } + } + updateLastChanges() { for (const changeIdx in this.lastChangesSent) { const changeFound = this.lines.find((line) => line.uuid === changeIdx); @@ -78,7 +95,20 @@ export class Order extends Reactive { return lastSend.qty !== line.qty || line.isChange(lastSend); }); } - get isSavedOnServer() { - return this.isAlreadySent && !this.hasNotAllLinesSent(); + + updateDataFromServer(data) { + for (const key in data) { + if (key !== "lines") { + this[key] = data[key]; + } + } + + for (const lines of this.lines) { + const lineFound = data.lines.find((line) => line.uuid === lines.uuid); + + if (lineFound) { + lines.updateDataFromServer(lineFound); + } + } } } diff --git a/addons/pos_self_order/static/src/models/product.js b/addons/pos_self_order/static/src/common/models/product.js similarity index 79% rename from addons/pos_self_order/static/src/models/product.js rename to addons/pos_self_order/static/src/common/models/product.js index bf4cff5e9bc..18dbbe3112b 100644 --- a/addons/pos_self_order/static/src/models/product.js +++ b/addons/pos_self_order/static/src/common/models/product.js @@ -11,7 +11,9 @@ export class Product extends Reactive { id, description_sale, pos_categ_ids, + pos_combo_ids, is_pos_groupable, + write_date, }, showPriceTaxIncluded ) { @@ -28,15 +30,19 @@ export class Product extends Reactive { this.name = product.name; this.description_sale = product.description_sale; this.pos_categ_ids = product.pos_categ_ids; + this.pos_combo_ids = product.pos_combo_ids; this.is_pos_groupable = product.is_pos_groupable; + this.write_date = product.write_date; // data this.showPriceTaxIncluded = showPriceTaxIncluded; } get prices() { - return this.showPriceTaxIncluded - ? this.price_info.price_with_tax - : this.price_info.price_without_tax; + return this.price_info.display_price; + } + + get isCombo() { + return this.pos_combo_ids; } } diff --git a/addons/pos_self_order/static/src/router.js b/addons/pos_self_order/static/src/common/router.js similarity index 96% rename from addons/pos_self_order/static/src/router.js rename to addons/pos_self_order/static/src/common/router.js index 88901bcce30..b27ad367823 100644 --- a/addons/pos_self_order/static/src/router.js +++ b/addons/pos_self_order/static/src/common/router.js @@ -3,7 +3,6 @@ import { Component, onWillRender, useState, xml } from "@odoo/owl"; import { escapeRegExp } from "@web/core/utils/strings"; import { zip } from "@web/core/utils/arrays"; -import { useSelfOrder } from "./self_order_service"; import { useService } from "@web/core/utils/hooks"; function parseParams(matches, paramSpecs) { @@ -27,7 +26,6 @@ export class Router extends Component { static template = xml``; setup() { - this.selfOrder = useSelfOrder(); this.router = useState(useService("router")); this.activeSlot = "default"; this.slotProps = {}; @@ -71,12 +69,14 @@ export class Router extends Component { const match = path.match(regex); if (match) { const parsedParams = parseParams(match.slice(1), paramSpecs); + this.router.activeSlot = routeName; this.activeSlot = routeName; this.slotProps = parsedParams; return; } } + this.router.activeSlot = "default"; this.router.navigate("default"); } } diff --git a/addons/pos_self_order/static/src/self_order_router_service.js b/addons/pos_self_order/static/src/common/self_order_router_service.js similarity index 96% rename from addons/pos_self_order/static/src/self_order_router_service.js rename to addons/pos_self_order/static/src/common/self_order_router_service.js index b0c36f221a7..17c1c974df3 100644 --- a/addons/pos_self_order/static/src/self_order_router_service.js +++ b/addons/pos_self_order/static/src/common/self_order_router_service.js @@ -16,7 +16,7 @@ export class SelfOrderRouter extends Reactive { this.path = window.location.pathname; this.registeredRoutes = {}; this.historyPage = ""; - + this.activeSlot = null; window.addEventListener("popstate", (event) => { this.path = window.location.pathname; }); @@ -24,7 +24,7 @@ export class SelfOrderRouter extends Reactive { addTableIdentifier(table) { const url = new URL(browser.location.href); - url.searchParams.append("table_identifier", table.identifier); + url.searchParams.set("table_identifier", table.identifier); history.replaceState({}, "", url); } diff --git a/addons/pos_self_order/static/src/common/self_order_service.js b/addons/pos_self_order/static/src/common/self_order_service.js new file mode 100644 index 00000000000..c432a43fb75 --- /dev/null +++ b/addons/pos_self_order/static/src/common/self_order_service.js @@ -0,0 +1,110 @@ +/** @odoo-module */ +import { Reactive } from "@web/core/utils/reactive"; +import { ConnectionLostError, RPCError } from "@web/core/network/rpc_service"; +import { _t } from "@web/core/l10n/translation"; +import { formatMonetary } from "@web/views/fields/formatters"; +import { Product } from "@pos_self_order/common/models/product"; +import { session } from "@web/session"; +import { getColor } from "@web/core/colors/colors"; +import { categorySorter } from "@pos_self_order/common/utils"; + +export class selfOrderCommon extends Reactive { + async setup(env, rpc, notification, router, bus_service, cookie) { + // services + this.env = env; + this.router = router; + this.rpc = rpc; + this.notification = notification; + this.bus_service = bus_service; + this.cookie = cookie; + + // data + Object.assign(this, { + ...session.pos_self_order_data, + }); + this.color = getColor(this.company_color); + this.priceLoading = false; + this.productByIds = {}; + this.productsGroupedByCategory = {}; + this.currentProduct = 0; + this.lastEditedProductId = null; + this.initData(); + this.categoryList = new Set( + this.pos_category + .sort((a, b) => a.sequence - b.sequence) + .filter((c) => this.productsGroupedByCategory[c.name]) + .sort((a, b) => categorySorter(a, b, this.iface_start_categ_id)) + ); + } + + initData() { + this.products = this.products.map((p) => { + const product = new Product(p, this.show_prices_with_tax_included); + this.productByIds[product.id] = product; + return product; + }); + + this.productsGroupedByCategory = this.products.reduce((acc, product) => { + product.pos_categ_ids.map((pos_categ_ids) => { + acc[pos_categ_ids] = acc[pos_categ_ids] || []; + acc[pos_categ_ids].push(product); + }); + return acc; + }, {}); + } + + async getPricesFromServer() { + try { + if (!this.currentOrder) { + return; + } + + this.priceLoading = true; + const taxes = await this.rpc(`/pos-self-order/get-orders-taxes/`, { + order: this.currentOrder, + access_token: this.access_token, + }); + + this.currentOrder.updateDataFromServer(taxes); + } catch (error) { + this.handleErrorNotification(error); + } finally { + this.priceLoading = false; + } + } + + handleErrorNotification(error, accessToken = []) { + let message = _t("An error has occurred"); + let cleanOrders = false; + + if (error instanceof RPCError) { + if (error.data.name === "werkzeug.exceptions.Unauthorized") { + message = _t("You're not authorized to perform this action"); + cleanOrders = true; + } else if (error.data.name === "werkzeug.exceptions.NotFound") { + message = _t("Orders not found on server"); + cleanOrders = true; + } + } else if (error instanceof ConnectionLostError) { + message = _t("Connection lost, please try again later"); + } + + this.notification.add(message, { + type: "danger", + }); + + if (accessToken && cleanOrders) { + this.editedOrder = null; + + for (const index in this.orders) { + if (accessToken.includes(this.orders[index].access_token)) { + this.orders.splice(index, 1); + } + } + } + } + + formatMonetary(price) { + return formatMonetary(price, { currencyId: this.currency_id }); + } +} diff --git a/addons/pos_self_order/static/src/common/utils.js b/addons/pos_self_order/static/src/common/utils.js new file mode 100644 index 00000000000..83753e6ae27 --- /dev/null +++ b/addons/pos_self_order/static/src/common/utils.js @@ -0,0 +1,14 @@ +/** @odoo-module */ + +export const categorySorter = (a, b, start_categ_id) => { + if (a.id === start_categ_id && b.id !== start_categ_id) { + return -1; // 'a' should come before 'b' + } + if (a.id !== start_categ_id && b.id === start_categ_id) { + return 1; // 'b' should come before 'a' + } + if (a.sequence !== b.sequence) { + return a.sequence - b.sequence; // sort by sequence + } + return a.id - b.id; // sort by id if sequences are the same +}; diff --git a/addons/pos_self_order/static/src/kiosk/bootstrap_overridden.scss b/addons/pos_self_order/static/src/kiosk/bootstrap_overridden.scss new file mode 100644 index 00000000000..a1732d096e1 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/bootstrap_overridden.scss @@ -0,0 +1,2 @@ +$border-radius: .65rem !default; +$border-radius-lg: .85rem !default; diff --git a/addons/pos_self_order/static/src/kiosk/components/attribute_selection/attribute_selection.js b/addons/pos_self_order/static/src/kiosk/components/attribute_selection/attribute_selection.js new file mode 100644 index 00000000000..1977890f851 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/attribute_selection/attribute_selection.js @@ -0,0 +1,67 @@ +/** @odoo-module */ + +import { Component, useState } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; + +export class AttributeSelection extends Component { + static template = "pos_self_order.AttributeSelection"; + static props = ["product", "productState"]; + + setup() { + this.selfOrder = useselfOrder(); + this.numberOfAttributes = this.props.product.attributes.length; + this.currentAttribute = 0; + + this.state = useState({ + currentAttribute: 0, + editMode: false, + showResume: false, + }); + } + + get attribute() { + return this.props.product.attributes[this.state.currentAttribute]; + } + + next() { + if (this.state.currentAttribute !== this.numberOfAttributes - 1) { + this.state.currentAttribute++; + } else { + this.state.showResume = true; + this.props.productState.showQtyButtons = true; + } + } + + prev() { + if (this.state.currentAttribute !== 0) { + this.state.currentAttribute--; + } + } + + attributeClicked() { + if (this.state.editMode) { + this.state.editMode = false; + this.state.showResume = true; + this.props.productState.showQtyButtons = true; + return; + } + this.next(); + } + + editAttribute(attributeName) { + const index = this.props.product.attributes.findIndex((a) => a.name === attributeName); + this.state.showResume = false; + this.state.editMode = true; + this.state.currentAttribute = index; + this.props.productState.showQtyButtons = false; + } + + get attributeSelected() { + return Object.entries(this.props.productState.selectedVariants).map(([key, value]) => { + return { + name: key, + value: value, + }; + }); + } +} diff --git a/addons/pos_self_order/static/src/kiosk/components/attribute_selection/attribute_selection.scss b/addons/pos_self_order/static/src/kiosk/components/attribute_selection/attribute_selection.scss new file mode 100644 index 00000000000..06a2c36daa1 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/attribute_selection/attribute_selection.scss @@ -0,0 +1,17 @@ +.o_kiosk-attribute_selection { + + .attribute-option { + height: 250px; + aspect-ratio: 1/1; + + &.active { + border: 1px solid #714B67; + } + } +} + +.attribute-selection-content { + .o_kiosk-card { + aspect-ratio: 1/1; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/components/attribute_selection/attribute_selection.xml b/addons/pos_self_order/static/src/kiosk/components/attribute_selection/attribute_selection.xml new file mode 100644 index 00000000000..588fca48bae --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/attribute_selection/attribute_selection.xml @@ -0,0 +1,57 @@ + + + +
+
+ +

Choose your

+
+ +

Your resume

+
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+
+
+

+ +

+ +
+
+
+
+
+ + diff --git a/addons/pos_self_order/static/src/kiosk/components/cancel_popup/cancel_popup.js b/addons/pos_self_order/static/src/kiosk/components/cancel_popup/cancel_popup.js new file mode 100644 index 00000000000..b951667b6b7 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/cancel_popup/cancel_popup.js @@ -0,0 +1,12 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; + +export class CancelPopup extends Component { + static template = "pos_self_order.CancelPopup"; + + confirm() { + this.props.close(); + this.props.confirm(); + } +} diff --git a/addons/pos_self_order/static/src/kiosk/components/cancel_popup/cancel_popup.scss b/addons/pos_self_order/static/src/kiosk/components/cancel_popup/cancel_popup.scss new file mode 100644 index 00000000000..3ed795a95cd --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/cancel_popup/cancel_popup.scss @@ -0,0 +1,6 @@ +.o_kiosk-cancel_popup { + .modal-dialog { + height: auto !important; + top: 40%; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/components/cancel_popup/cancel_popup.xml b/addons/pos_self_order/static/src/kiosk/components/cancel_popup/cancel_popup.xml new file mode 100644 index 00000000000..558ffc5b316 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/cancel_popup/cancel_popup.xml @@ -0,0 +1,23 @@ + + + +
+ +
+
+
diff --git a/addons/pos_self_order/static/src/kiosk/components/combo_selection/combo_selection.js b/addons/pos_self_order/static/src/kiosk/components/combo_selection/combo_selection.js new file mode 100644 index 00000000000..2b4a04cb653 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/combo_selection/combo_selection.js @@ -0,0 +1,24 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { AttributeSelection } from "@pos_self_order/kiosk/components/attribute_selection/attribute_selection"; + +export class ComboSelection extends Component { + static template = "pos_self_order.ComboSelection"; + static props = ["combo", "comboState", "next"]; + static components = { AttributeSelection }; + + setup() { + this.selfOrder = useselfOrder(); + } + + productClicked(lineId) { + const comboLine = this.props.combo.combo_line_ids.find((line) => line.id == lineId); + const productSelected = this.selfOrder.productByIds[comboLine.product_id[0]]; + this.props.comboState.selectedProduct = productSelected; + if (productSelected.attributes.length === 0) { + this.props.next(); + } + } +} diff --git a/addons/pos_self_order/static/src/kiosk/components/combo_selection/combo_selection.xml b/addons/pos_self_order/static/src/kiosk/components/combo_selection/combo_selection.xml new file mode 100644 index 00000000000..346e6f65f86 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/combo_selection/combo_selection.xml @@ -0,0 +1,43 @@ + + + +
+ +
+

Choose your

+
+
+
+ +
+
+ + Product image +
+

+ + + + +

+
+
+
+
+
+
+ + + +
+
+
diff --git a/addons/pos_self_order/static/src/kiosk/components/language_popup/language_popup.js b/addons/pos_self_order/static/src/kiosk/components/language_popup/language_popup.js new file mode 100644 index 00000000000..77411704703 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/language_popup/language_popup.js @@ -0,0 +1,27 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { useService } from "@web/core/utils/hooks"; + +export class LanguagePopup extends Component { + static template = "pos_self_order.LanguagePopup"; + + setup() { + this.selfOrder = useselfOrder(); + this.cookie = useService("cookie"); + } + + get languages() { + return this.selfOrder.kiosk_available_languages; + } + + get currentLanguage() { + return this.selfOrder.currentLanguage; + } + + onClickLanguage(language) { + this.cookie.setCookie("frontend_lang", language.code); + window.location.reload(); + } +} diff --git a/addons/pos_self_order/static/src/kiosk/components/language_popup/language_popup.scss b/addons/pos_self_order/static/src/kiosk/components/language_popup/language_popup.scss new file mode 100644 index 00000000000..c9d49fba1c8 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/language_popup/language_popup.scss @@ -0,0 +1,10 @@ +.o_kiosk-language_popup { + .modal-dialog { + top: 40%; + } + + img { + max-width: 64px; + aspect-ratio: 4/3; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/components/language_popup/language_popup.xml b/addons/pos_self_order/static/src/kiosk/components/language_popup/language_popup.xml new file mode 100644 index 00000000000..8911de244ea --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/language_popup/language_popup.xml @@ -0,0 +1,28 @@ + + + +
+ +
+
+
diff --git a/addons/pos_self_order/static/src/kiosk/components/product_card/product_card.js b/addons/pos_self_order/static/src/kiosk/components/product_card/product_card.js new file mode 100644 index 00000000000..cc7678c87c5 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/product_card/product_card.js @@ -0,0 +1,104 @@ +/** @odoo-module */ + +import { Component, useRef } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { useService, useForwardRefToParent } from "@web/core/utils/hooks"; +import { Line } from "@pos_self_order/common/models/line"; + +export class ProductCard extends Component { + static template = "pos_self_order.ProductCard"; + static props = ["product", "currentProductCard?"]; + + selfRef = useRef("selfProductCard"); + currentProductCardRef = useRef("currentProductCard"); + + setup() { + this.selfOrder = useselfOrder(); + this.router = useService("router"); + + useForwardRefToParent("currentProductCard"); + } + + flyToCart() { + const productCardEl = this.selfRef.el; + if (!productCardEl) return; + + const cartButton = document.querySelector(".cart"); + if (!cartButton || window.getComputedStyle(cartButton).display === 'none') { + return; + } + + const clonedCard = productCardEl.cloneNode(true); + const cardRect = productCardEl.getBoundingClientRect(); + const cartRect = cartButton.getBoundingClientRect(); + + clonedCard.style.position = "absolute"; + clonedCard.style.top = `${cardRect.top}px`; + clonedCard.style.left = `${cardRect.left}px`; + clonedCard.style.width = `${cardRect.width}px`; + clonedCard.style.height = `${cardRect.height}px`; + clonedCard.style.opacity = ".85"; + clonedCard.style.transition = "top 1s cubic-bezier(0.95, 0.1, 0.25, 0.95), left 1s cubic-bezier(0.95, 0.1, 0.25, 0.95), width 1s cubic-bezier(0.95, 0.1, 0.25, 0.95), height 1s cubic-bezier(0.95, 0.1, 0.25, 0.95), opacity 0.6s 0.2s ease-in-out"; + + + document.body.appendChild(clonedCard); + + requestAnimationFrame(() => { + clonedCard.style.top = `${cartRect.top}px`; + clonedCard.style.left = `${cartRect.left * 1.25}px`; + clonedCard.style.width = `${cardRect.width * 0.80}px`; + clonedCard.style.height = `${cardRect.height * 0.80}px`; + clonedCard.style.opacity = "0"; // Fading out the card + }); + + clonedCard.addEventListener('transitionend', () => { + clonedCard.remove(); + }); + } + + scaleUpPrice() { + const priceElement = document.querySelector(".total-price"); + console.log(priceElement); + + if (!priceElement) return; + + priceElement.classList.add('scale-up'); + + setTimeout(() => { + priceElement.classList.remove('scale-up'); + }, 600); + } + + async selectProduct() { + const product = this.props.product; + + if (product.isCombo) { + this.router.navigate("combo", { id: product.id }); + } else if (product.attributes.length > 0) { + this.router.navigate("product", { id: product.id }); + } else { + this.flyToCart(); + this.scaleUpPrice(); + const isProductInCart = this.selfOrder.currentOrder.lines.find( + (line) => line.product_id === product.id + ); + + if (isProductInCart) { + isProductInCart.qty++; + } else { + const lines = this.selfOrder.currentOrder.lines; + + lines.push( + new Line({ + id: null, + uuid: null, + qty: 1, + product_id: product.id, + full_product_name: product.name, + }) + ); + } + await this.selfOrder.getPricesFromServer(); + } + } +} diff --git a/addons/pos_self_order/static/src/kiosk/components/product_card/product_card.scss b/addons/pos_self_order/static/src/kiosk/components/product_card/product_card.scss new file mode 100644 index 00000000000..cf542437a00 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/product_card/product_card.scss @@ -0,0 +1,12 @@ +.o_kiosk_product_card { + + .o_self_order_item_card_image { + object-fit: contain; + aspect-ratio:1/1; + } +} + +.scale-up { + transform: scale(1.2); + transition: transform .75s ease-in-out; +} diff --git a/addons/pos_self_order/static/src/kiosk/components/product_card/product_card.xml b/addons/pos_self_order/static/src/kiosk/components/product_card/product_card.xml new file mode 100644 index 00000000000..f503d912afa --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/components/product_card/product_card.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/addons/pos_self_order/static/src/kiosk/pages/closed/closed.js b/addons/pos_self_order/static/src/kiosk/pages/closed/closed.js new file mode 100644 index 00000000000..ed74198922d --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/closed/closed.js @@ -0,0 +1,9 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; +import { KioskTemplate } from "@pos_self_order/kiosk/template/kiosk_template"; + +export class Closed extends Component { + static template = "pos_self_order.Closed"; + static components = { KioskTemplate }; +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/closed/closed.xml b/addons/pos_self_order/static/src/kiosk/pages/closed/closed.xml new file mode 100644 index 00000000000..e1bc5904462 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/closed/closed.xml @@ -0,0 +1,12 @@ + + + + + +
+ The kiosk is closed +
+
+
+
+
diff --git a/addons/pos_self_order/static/src/kiosk/pages/combo/combo.js b/addons/pos_self_order/static/src/kiosk/pages/combo/combo.js new file mode 100644 index 00000000000..3fd234e967b --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/combo/combo.js @@ -0,0 +1,142 @@ +/** @odoo-module */ + +import { Component, useState } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { KioskTemplate } from "@pos_self_order/kiosk/template/kiosk_template"; +import { ComboSelection } from "@pos_self_order/kiosk/components/combo_selection/combo_selection"; +import { useService } from "@web/core/utils/hooks"; +import { Line } from "@pos_self_order/common/models/line"; + +export class Combo extends Component { + static template = "pos_self_order.Combo"; + static props = ["product"]; + static components = { KioskTemplate, ComboSelection }; + + setup() { + this.selfOrder = useselfOrder(); + this.selfOrder.lastEditedProductId = this.props.product.id; + this.router = useService("router"); + + if (!this.props.product) { + this.router.navigate("productList"); + return; + } + + this.state = useState({ + currentComboIndex: 0, + selectedCombos: [], + showResume: false, + selectedProduct: null, + selectedVariants: {}, + qty: 1, + showQtyButtons: false, + editMode: false, + }); + } + + get currentComboId() { + return this.props.product.pos_combo_ids[this.state.currentComboIndex]; + } + + get currentCombo() { + return this.selfOrder.comboByIds[this.currentComboId]; + } + + resetState() { + this.state.selectedProduct = null; + this.state.selectedVariants = {}; + this.state.showQtyButtons = false; + } + + next() { + const combo = this.currentCombo; + const index = this.state.selectedCombos.findIndex((c) => c.id === combo.id); + if (index !== -1) { + this.state.selectedCombos[index] = { + id: combo.id, + name: combo.name, + product: { + id: this.state.selectedProduct.id, + name: this.state.selectedProduct.name, + variants: Object.entries(this.state.selectedVariants), + }, + }; + } else { + this.state.selectedCombos.push({ + id: combo.id, + name: combo.name, + product: { + id: this.state.selectedProduct.id, + name: this.state.selectedProduct.name, + variants: Object.entries(this.state.selectedVariants), + }, + }); + } + this.resetState(); + if (this.state.editMode) { + this.state.editMode = false; + this.state.showResume = true; + this.state.showQtyButtons = true; + return; + } + this.state.currentComboIndex++; + if (this.state.currentComboIndex == this.props.product.pos_combo_ids.length) { + this.state.showResume = true; + } + } + + back() { + this.router.navigate("product_list"); + } + + changeQuantity(increase) { + if (!increase && this.state.qty === 1) { + return; + } + + return increase ? this.state.qty++ : this.state.qty--; + } + + async addToCart() { + const lines = this.selfOrder.currentOrder.lines; + const parent_line = new Line({ + id: null, + uuid: null, + qty: this.state.qty, + price_unit: this.props.product.price_info.price_without_tax, + product_id: this.props.product.id, + full_product_name: this.props.product.name, + selected_attributes: {}, + combo_parent_uuid: null, + combo_id: null, + }); + lines.push(parent_line); + for (const combo of this.state.selectedCombos) { + const child_line = new Line({ + id: null, + uuid: null, + qty: this.state.qty, + product_id: combo.product.id, + full_product_name: combo.product.name, + selected_attributes: Object.fromEntries(combo.product.variants), + combo_parent_uuid: parent_line.uuid, + combo_id: combo.id, + }); + lines.push(child_line); + parent_line.child_lines.push(child_line); + } + + await this.selfOrder.getPricesFromServer(); + + this.router.back(); + } + + editCombo(combo_id) { + this.state.currentComboIndex = this.state.selectedCombos.findIndex( + (c) => c.id === combo_id + ); + this.state.showResume = false; + this.state.editMode = true; + this.state.showQtyButtons = false; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/combo/combo.scss b/addons/pos_self_order/static/src/kiosk/pages/combo/combo.scss new file mode 100644 index 00000000000..b0c7efccd9d --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/combo/combo.scss @@ -0,0 +1,56 @@ +.breadcrumb { + max-height: 99px; + + div:first-child { + margin-left: -15px; + } + + div:last-child { + padding-right: 15px; + > span:after, > span:before { + border: none; + } + } + + div > span { + padding: 1% 0 1% 60px; + padding-right: 20px; + + &:after, + &:before { + content: " "; + border-top: 50px solid transparent; + border-bottom: 50px solid transparent; + position: absolute; + top: 50%; + margin-top: -50px; + margin-left: -1px; + } + + &:after { + border-left: 30px solid $o-gray-200; + left: 100%; + z-index: 2; + } + + &:before { + border-left: 30px solid $o-gray-300; + margin-left: 1px; + left: 100%; + z-index: 1; + } + } + + div.active-step span { + &:after { + border-left: 30px solid $o-gray-100; + } + } +} + +.o_kiosk-combo { + .o_self_order_item_card_image { + aspect-ratio: 1/1; + object-fit: contain; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/combo/combo.xml b/addons/pos_self_order/static/src/kiosk/pages/combo/combo.xml new file mode 100644 index 00000000000..b3cda416336 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/combo/combo.xml @@ -0,0 +1,95 @@ + + + + + +
+
+ +
+
+ +
+ +
+
+
+ Product image +
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+

Your Selection

+
+
+
+
+ + Product image +
+

+
+ - : +
+
+ +
+
+
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ + + +
+
+ + + diff --git a/addons/pos_self_order/static/src/kiosk/pages/eating_location/eating_location.js b/addons/pos_self_order/static/src/kiosk/pages/eating_location/eating_location.js new file mode 100644 index 00000000000..488d71e5032 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/eating_location/eating_location.js @@ -0,0 +1,27 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { useService } from "@web/core/utils/hooks"; +import { KioskTemplate } from "@pos_self_order/kiosk/template/kiosk_template"; + +export class EatingLocation extends Component { + static template = "pos_self_order.EatingLocation"; + static components = { KioskTemplate }; + + setup() { + this.selfOrder = useselfOrder(); + this.router = useService("router"); + } + + back() { + this.router.navigate("default"); + } + + selectLocation(loc) { + if (loc === "out") { + this.selfOrder.currentOrder.take_away = true; + } + this.router.navigate("product_list"); + } +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/eating_location/eating_location.xml b/addons/pos_self_order/static/src/kiosk/pages/eating_location/eating_location.xml new file mode 100644 index 00000000000..8a0f4c16210 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/eating_location/eating_location.xml @@ -0,0 +1,36 @@ + + + + + +
+
+

Choose your eating location

+
+ + +
+
+
+
+ +
+ +
+
+
+
+
diff --git a/addons/pos_self_order/static/src/kiosk/pages/landing_page/landing_page.js b/addons/pos_self_order/static/src/kiosk/pages/landing_page/landing_page.js new file mode 100644 index 00000000000..32f063d4fb6 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/landing_page/landing_page.js @@ -0,0 +1,44 @@ +/** @odoo-module */ + +import { Component, onWillStart } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { useService } from "@web/core/utils/hooks"; +import { KioskTemplate } from "@pos_self_order/kiosk/template/kiosk_template"; +import { LanguagePopup } from "@pos_self_order/kiosk/components/language_popup/language_popup"; +import { Order } from "@pos_self_order/common/models/order"; + +export class LandingPage extends Component { + static template = "pos_self_order.LandingPage"; + static components = { KioskTemplate }; + + setup() { + this.selfOrder = useselfOrder(); + this.router = useService("router"); + this.dialog = useService("dialog"); + + onWillStart(() => { + this.selfOrder.currentOrder = new Order({}); + this.tablePadNumber = null; + }); + } + + start() { + if (this.selfOrder.kiosk_takeaway) { + this.router.navigate("location"); + } else { + this.router.navigate("product_list"); + } + } + + openLanguages() { + this.dialog.add(LanguagePopup); + } + + get currentLanguage() { + return this.selfOrder.currentLanguage; + } + + get languages() { + return this.selfOrder.kiosk_available_languages; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/landing_page/landing_page.scss b/addons/pos_self_order/static/src/kiosk/pages/landing_page/landing_page.scss new file mode 100644 index 00000000000..a98b9906ede --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/landing_page/landing_page.scss @@ -0,0 +1,11 @@ +.o_kiosk_language_selector { + z-index: 100; + + img { + height: 2em; + aspect-ratio: 3/2; + } +} +.o_kiosk-landing_image { + object-fit: cover; +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/landing_page/landing_page.xml b/addons/pos_self_order/static/src/kiosk/pages/landing_page/landing_page.xml new file mode 100644 index 00000000000..c5cf166ec4c --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/landing_page/landing_page.xml @@ -0,0 +1,26 @@ + + + + + +
+ + +
+
+ + +
+
+ + + +
+
+
diff --git a/addons/pos_self_order/static/src/kiosk/pages/order_cart/order_cart.js b/addons/pos_self_order/static/src/kiosk/pages/order_cart/order_cart.js new file mode 100644 index 00000000000..242e72eca2c --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/order_cart/order_cart.js @@ -0,0 +1,72 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { KioskTemplate } from "@pos_self_order/kiosk/template/kiosk_template"; + +export class OrderCart extends Component { + static template = "pos_self_order.OrderCart"; + static components = { KioskTemplate }; + + setup() { + this.selfOrder = useselfOrder(); + this.router = useService("router"); + } + + get lines() { + const lines = this.selfOrder.currentOrder.lines; + return lines ? lines : []; + } + + backToMenu() { + this.router.navigate("product_list"); + } + + pay() { + if (this.selfOrder.kiosk_mode === "table" && !this.selfOrder.currentOrder.take_away) { + this.router.navigate("stand_number"); + } else { + this.router.navigate("payment"); + } + } + + async removeLine(lineUuid) { + this.selfOrder.currentOrder.removeLine(lineUuid); + await this.selfOrder.getPricesFromServer(); + } + + getChildLines(line) { + return this.lines.filter((l) => l.combo_parent_uuid === line.uuid); + } + + getPrice(line) { + const childLines = this.getChildLines(line); + if (childLines.length == 0) { + return line.price_subtotal_incl; + } else { + let price = 0; + for (const child of childLines) { + price += child.price_subtotal_incl; + } + return price; + } + } + + async changeQuantity(line, increase, fetch = true) { + if (!increase && line.qty === 1) { + this.removeLine(line.uuid); + return; + } + increase ? line.qty++ : line.qty--; + for (const cline of this.selfOrder.currentOrder.lines) { + if (cline.combo_parent_uuid === line.uuid) { + this.changeQuantity(cline, increase, false); + } + } + if (fetch) { + await this.selfOrder.getPricesFromServer(); + } + return; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/order_cart/order_cart.scss b/addons/pos_self_order/static/src/kiosk/pages/order_cart/order_cart.scss new file mode 100644 index 00000000000..367ae947664 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/order_cart/order_cart.scss @@ -0,0 +1,38 @@ +.o_kiosk-order-cart { + .o_kiosk_product_card { + height: 100% + } + .order-lines { + height: 150px; + } + .o_self_order_item_card_image { + object-fit: contain; + aspect-ratio:1/1; + min-width: 128px; + } + + .product-wrapper { + min-height: 128px; + } +} + + +.upsale-product .o_kiosk_product_card { + height: auto; +} + +.upsale-product .o_self_order_item_card_image { + height: 320px; +} + +@media (max-height: 1280px) { + .upsale-product .o_self_order_item_card_image { + height: 120px; + } +} + +@media (max-height: 1024px) { + .upsale-product .o_self_order_item_card_image { + height: 60px; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/order_cart/order_cart.xml b/addons/pos_self_order/static/src/kiosk/pages/order_cart/order_cart.xml new file mode 100644 index 00000000000..2407b6c3cbe --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/order_cart/order_cart.xml @@ -0,0 +1,77 @@ + + + + + +
+

Verify your Order

+
+
+ +
+
+ +
+ + + Product image +
+
+
+
+ +
+ - () +
+
+ - : +
+
+
+
+ - : +
+
+
+ +
+ +
+ +
+ +
+
+
+
+
+
+
+
+

Total:

+

Taxes:

+
+
+
+ + +
+ + +
+
+ + + diff --git a/addons/pos_self_order/static/src/kiosk/pages/payment/payment.js b/addons/pos_self_order/static/src/kiosk/pages/payment/payment.js new file mode 100644 index 00000000000..fc3573e3271 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/payment/payment.js @@ -0,0 +1,65 @@ +/** @odoo-module */ + +import { Component, onWillStart, onWillUnmount, useState } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { useService } from "@web/core/utils/hooks"; +import { KioskTemplate } from "@pos_self_order/kiosk/template/kiosk_template"; + +export class Payment extends Component { + static template = "pos_self_order.Payment"; + static components = { KioskTemplate }; + + setup() { + this.selfOrder = useselfOrder(); + this.selfOrder.isOrder(); + this.router = useService("router"); + this.rpc = useService("rpc"); + this.state = useState({ + selection: true, + paymentMethodId: null, + }); + + onWillUnmount(() => { + this.selfOrder.paymentError = false; + }); + + onWillStart(async () => { + if (this.selfOrder.pos_payment_methods.length === 0) { + const order = await this.rpc("/pos-self-order/process-new-order/kiosk", { + order: this.selfOrder.currentOrder, + access_token: this.selfOrder.access_token, + table_identifier: null, + }); + this.selfOrder.updateOrderFromServer(order); + this.router.navigate("payment_success"); + } else if (this.selfOrder.pos_payment_methods.length === 1) { + this.selectMethod(this.selfOrder.pos_payment_methods[0].id); + } + }); + } + + selectMethod(methodId) { + this.state.selection = false; + this.state.paymentMethodId = methodId; + this.startPayment(); + } + + async startPayment() { + this.selfOrder.paymentError = false; + try { + const order = await this.rpc(`/kiosk/payment/${this.selfOrder.pos_config_id}/kiosk`, { + order: this.selfOrder.currentOrder, + access_token: this.selfOrder.access_token, + payment_method_id: this.state.paymentMethodId, + }); + this.selfOrder.updateOrderFromServer(order); + } catch (error) { + this.selfOrder.handleErrorNotification(error); + this.selfOrder.paymentError = true; + } + } + + get showFooterBtn() { + return this.selfOrder.paymentError || this.state.selection; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/payment/payment.xml b/addons/pos_self_order/static/src/kiosk/pages/payment/payment.xml new file mode 100644 index 00000000000..7f5d8ffac5e --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/payment/payment.xml @@ -0,0 +1,42 @@ + + + + + +
+
+
+ +
+
+ +
+
+
+
+
+ +

Follow instructions
on the terminal

+ +
+ +

The payment has failed
please retry

+ +
+
+
+
+ +
+ + +
+
+
+
+
diff --git a/addons/pos_self_order/static/src/kiosk/pages/payment_success/payment_success.js b/addons/pos_self_order/static/src/kiosk/pages/payment_success/payment_success.js new file mode 100644 index 00000000000..5e7f484d176 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/payment_success/payment_success.js @@ -0,0 +1,47 @@ +/** @odoo-module */ + +import { Component, onMounted, useState } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { useService } from "@web/core/utils/hooks"; +import { KioskTemplate } from "@pos_self_order/kiosk/template/kiosk_template"; + +export class PaymentSuccess extends Component { + static template = "pos_self_order.PaymentSuccess"; + static components = { KioskTemplate }; + + setup() { + this.selfOrder = useselfOrder(); + this.selfOrder.isOrder(); + this.router = useService("router"); + this.cookie = useService("cookie"); + this.state = useState({ + onReload: false, + }); + + onMounted(() => { + setTimeout(() => { + this.setDefautLanguage(); + }, 5000); + }); + } + + backToHome() { + if (!this.setDefautLanguage()) { + this.router.navigate("default"); + } + } + + setDefautLanguage() { + if ( + this.selfOrder.currentLanguage.code !== this.selfOrder.kiosk_default_language.code && + !this.state.onReload + ) { + this.cookie.setCookie("frontend_lang", this.selfOrder.kiosk_default_language.code); + window.location.reload(); + this.state.onReload = true; + return true; + } + + return this.state.onReload; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/payment_success/payment_success.scss b/addons/pos_self_order/static/src/kiosk/pages/payment_success/payment_success.scss new file mode 100644 index 00000000000..9ad6f3117ce --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/payment_success/payment_success.scss @@ -0,0 +1,6 @@ +.number { + font-size: 6rem; +} +.o_kiosk-success_loader { + z-index: 10; +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/payment_success/payment_success.xml b/addons/pos_self_order/static/src/kiosk/pages/payment_success/payment_success.xml new file mode 100644 index 00000000000..07d3f65cf2b --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/payment_success/payment_success.xml @@ -0,0 +1,38 @@ + + + + + +
+
+
+ Loading... +
+
+
+
+ +
+

We're preparing
your order!

+
+ + Your order number + + + + Your table number + +

Order number:

+
+
+

Get your ticket below

+
+
+ +
+ +
+
+
+
+
diff --git a/addons/pos_self_order/static/src/kiosk/pages/product/product.js b/addons/pos_self_order/static/src/kiosk/pages/product/product.js new file mode 100644 index 00000000000..93d61dc74c2 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/product/product.js @@ -0,0 +1,90 @@ +/** @odoo-module */ + +import { Component, useState } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { AttributeSelection } from "@pos_self_order/kiosk/components/attribute_selection/attribute_selection"; +import { KioskTemplate } from "@pos_self_order/kiosk/template/kiosk_template"; +import { Line } from "@pos_self_order/common/models/line"; +import { useService } from "@web/core/utils/hooks"; + +export class Product extends Component { + static template = "pos_self_order.Product"; + static props = ["product", "back?", "onValidate?"]; + static components = { AttributeSelection, KioskTemplate }; + + setup() { + this.selfOrder = useselfOrder(); + this.router = useService("router"); + + if (!this.props.product) { + this.router.navigate("productList"); + return; + } + + this.selfOrder.lastEditedProductId = this.props.product.id; + this.state = useState({ + qty: 1, + customer_note: "", + selectedVariants: {}, + showQtyButtons: false, + product: this.props.product, + }); + } + + get product() { + return this.state.product; + } + + back() { + this.router.navigate("product_list"); + } + + changeQuantity(increase) { + const currentQty = this.state.qty; + + if (!increase && currentQty === 1) { + return; + } + + return increase ? this.state.qty++ : this.state.qty--; + } + + get fullProductName() { + const productAttributeString = Object.values(this.state.selectedVariants).join(", "); + let name = `${this.product.name}`; + + if (productAttributeString) { + name += ` (${productAttributeString})`; + } + + return name; + } + + async addToCart() { + const lines = this.selfOrder.currentOrder.lines; + + lines.push( + new Line({ + id: null, + uuid: null, + qty: this.state.qty, + product_id: this.product.id, + full_product_name: this.fullProductName, + customer_note: this.state.customer_note, + selected_attributes: this.state.selectedVariants, + }) + ); + + // If a command line does not have a quantity greater than 0, we consider it deleted + await this.selfOrder.getPricesFromServer(); + this.router.back(); + } + + get attributeSelection() { + if (this.product.attributes.length === 0) { + this.state.showQtyButtons = true; + } + + return this.product.attributes.length > 0; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/product/product.xml b/addons/pos_self_order/static/src/kiosk/pages/product/product.xml new file mode 100644 index 00000000000..3cfab084edb --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/product/product.xml @@ -0,0 +1,56 @@ + + + + + + + +
+
+ Product image +
+

+ + +

+
+ +
+
+ +
+
+
+
+ +
+ +
+
+
+ + +
+ + +
+
+ + + diff --git a/addons/pos_self_order/static/src/kiosk/pages/product_list/product_list.js b/addons/pos_self_order/static/src/kiosk/pages/product_list/product_list.js new file mode 100644 index 00000000000..8aafe13d4d6 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/product_list/product_list.js @@ -0,0 +1,58 @@ +/** @odoo-module */ + +import { Component, onMounted, useRef } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { ProductCard } from "@pos_self_order/kiosk/components/product_card/product_card"; +import { CancelPopup } from "@pos_self_order/kiosk/components/cancel_popup/cancel_popup"; +import { KioskTemplate } from "@pos_self_order/kiosk/template/kiosk_template"; +import { useService, useChildRef } from "@web/core/utils/hooks"; +import { _t } from "@web/core/l10n/translation"; + +export class ProductList extends Component { + static template = "pos_self_order.ProductList"; + static components = { ProductCard, KioskTemplate }; + + setup() { + this.selfOrder = useselfOrder(); + this.dialog = useService("dialog"); + this.router = useService("router"); + this.productsList = useRef("productsList"); + this.currentProductCard = useChildRef(); + + onMounted(() => { + if (this.selfOrder.lastEditedProductId) { + this.scrollTo(this.currentProductCard, { behavior: "instant" }); + } + }); + } + + categoryClicked(categoryName) { + this.selfOrder.currentCategory = categoryName; + } + + get productCategory() { + const productByCat = this.selfOrder.productsGroupedByCategory; + const currentCategory = this.selfOrder.currentCategory; + return productByCat[currentCategory] ? productByCat[currentCategory] : []; + } + + scrollTo(ref = null, { behavior = "smooth" } = {}) { + this.productsList.el.scroll({ + top: ref?.el ? ref.el.offsetTop - this.productsList.el.offsetTop : 0, + behavior, + }); + } + + review() { + this.router.navigate("cart"); + } + + cancelOrder() { + this.dialog.add(CancelPopup, { + title: _t("Cancel order"), + confirm: () => { + this.router.navigate("default"); + }, + }); + } +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/product_list/product_list.scss b/addons/pos_self_order/static/src/kiosk/pages/product_list/product_list.scss new file mode 100644 index 00000000000..9b356f7bcf5 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/product_list/product_list.scss @@ -0,0 +1,28 @@ +.o_kiosk_product_list { + .category-list { + height: 225px; + overflow-y: hidden; + + .category-item { + width: 150px; + height: 150px; + } + + img { + object-fit: cover; + } + } + + .product-list { + overflow-y: scroll; + height: calc(100% - 225px); + + .product-list-category { + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + } + + h2:first-of-type { + padding-top: 2rem !important; + } + } +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/product_list/product_list.xml b/addons/pos_self_order/static/src/kiosk/pages/product_list/product_list.xml new file mode 100644 index 00000000000..0e3b9f0fb12 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/product_list/product_list.xml @@ -0,0 +1,54 @@ + + + + + + +
+ + +
+
+ +
+
+
+

+
+ + + +
+

+
+
+
+ + +
+ + +
+
+ + + diff --git a/addons/pos_self_order/static/src/kiosk/pages/stand_number/stand_number.js b/addons/pos_self_order/static/src/kiosk/pages/stand_number/stand_number.js new file mode 100644 index 00000000000..455b76851f3 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/stand_number/stand_number.js @@ -0,0 +1,43 @@ +/** @odoo-module */ + +import { Component, useState } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { useService } from "@web/core/utils/hooks"; +import { KioskTemplate } from "@pos_self_order/kiosk/template/kiosk_template"; + +export class StandNumber extends Component { + static template = "pos_self_order.StandNumber"; + static components = { KioskTemplate }; + + setup() { + this.selfOrder = useselfOrder(); + this.selfOrder.isOrder(); + this.router = useService("router"); + this.state = useState({ + standNumber: "", + }); + } + + numberClick(event) { + const key = event.target.attributes.data.value; + + if (key === "reset") { + this.state.standNumber = this.state.standNumber.slice(0, -1); + } else if (key === "clear") { + this.state.standNumber = ""; + } else { + this.state.standNumber += key; + } + } + + confirm() { + if (this.state.standNumber.length > 0) { + this.selfOrder.tablePadNumber = this.state.standNumber; + this.router.navigate("payment"); + } + } + + get tableInput() { + return this.state.standNumber ? this.state.standNumber : "_ _"; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/pages/stand_number/stand_number.xml b/addons/pos_self_order/static/src/kiosk/pages/stand_number/stand_number.xml new file mode 100644 index 00000000000..f7b33269ee4 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/pages/stand_number/stand_number.xml @@ -0,0 +1,43 @@ + + + + + +
+
+
+

Pick a plate and write your number here

+
+ +
+
+
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
+ +
+
0
+
+ +
+
+
+
+
+ +
+ + +
+
+
+
+
diff --git a/addons/pos_self_order/static/src/kiosk/self_order_kiosk_index.js b/addons/pos_self_order/static/src/kiosk/self_order_kiosk_index.js new file mode 100644 index 00000000000..81daf1f5bcd --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/self_order_kiosk_index.js @@ -0,0 +1,58 @@ +/** @odoo-module */ +import { Component, whenReady, App } from "@odoo/owl"; +import { makeEnv, startServices } from "@web/env"; +import { setLoadXmlDefaultApp, templates } from "@web/core/assets"; +import { _t } from "@web/core/l10n/translation"; +import { LandingPage } from "@pos_self_order/kiosk/pages/landing_page/landing_page"; +import { EatingLocation } from "@pos_self_order/kiosk/pages/eating_location/eating_location"; +import { Product } from "@pos_self_order/kiosk/pages/product/product"; +import { ProductList } from "@pos_self_order/kiosk/pages/product_list/product_list"; +import { OrderCart } from "@pos_self_order/kiosk/pages/order_cart/order_cart"; +import { Closed } from "@pos_self_order/kiosk/pages/closed/closed"; +import { Payment } from "@pos_self_order/kiosk/pages/payment/payment"; +import { Combo } from "@pos_self_order/kiosk/pages/combo/combo"; +import { PaymentSuccess } from "@pos_self_order/kiosk/pages/payment_success/payment_success"; +import { StandNumber } from "@pos_self_order/kiosk/pages/stand_number/stand_number"; +import { MainComponentsContainer } from "@web/core/main_components_container"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { Router } from "@pos_self_order/common/router"; + +class selfOrderIndex extends Component { + static template = "pos_self_order.selfOrderIndex"; + static props = []; + static components = { + Payment, + EatingLocation, + ProductList, + OrderCart, + PaymentSuccess, + Closed, + Combo, + StandNumber, + Product, + LandingPage, + Router, + MainComponentsContainer, + }; + + setup() { + this.selfOrder = useselfOrder(); + } +} + +export async function createPublicRoot() { + await whenReady(); + const env = makeEnv(); + await startServices(env); + const app = new App(selfOrderIndex, { + templates, + env: env, + dev: env.debug, + translateFn: _t, + translatableAttributes: ["data-tooltip"], + }); + setLoadXmlDefaultApp(app); + return app.mount(document.body); +} +createPublicRoot(); +export default { selfOrderIndex, createPublicRoot }; diff --git a/addons/pos_self_order/static/src/kiosk/self_order_kiosk_index.scss b/addons/pos_self_order/static/src/kiosk/self_order_kiosk_index.scss new file mode 100644 index 00000000000..0d686885379 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/self_order_kiosk_index.scss @@ -0,0 +1,26 @@ +html, +body { + width: 100%; + height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow: hidden; + font-size: 24px; + -webkit-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; +} + +.h-10 { + height: 10%; +} + +.h-90 { + height: 90%; +} + +.o_kiosk-card img { + object-fit: contain; + aspect-ratio: 1/1; +} diff --git a/addons/pos_self_order/static/src/kiosk/self_order_kiosk_index.xml b/addons/pos_self_order/static/src/kiosk/self_order_kiosk_index.xml new file mode 100644 index 00000000000..aa4d706c1a1 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/self_order_kiosk_index.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/pos_self_order/static/src/kiosk/self_order_kiosk_service.js b/addons/pos_self_order/static/src/kiosk/self_order_kiosk_service.js new file mode 100644 index 00000000000..612926d7a79 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/self_order_kiosk_service.js @@ -0,0 +1,108 @@ +/** @odoo-module */ +import { useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { Order } from "@pos_self_order/common/models/order"; +import { Combo } from "@pos_self_order/common/models/combo"; +import { selfOrderCommon } from "@pos_self_order/common/self_order_service"; + +export class selfOrder extends selfOrderCommon { + constructor(...args) { + super(...args); + this.ready = this.setup(...args).then(() => this); + } + + async setup(...args) { + this.comboByIds = {}; + + await super.setup(...args); + + this.bus_service.addChannel(`pos_config-${this.access_token}`); + this.bus_service.addEventListener("notification", ({ detail }) => { + for (const message of detail) { + this.dispatchMessage(message); + } + }); + + this.currentLanguage = this.kiosk_available_languages.find( + (l) => l.code === this.cookie.current.frontend_lang + ); + + if (this.kiosk_default_language && !this.currentLanguage) { + this.currentLanguage = this.kiosk_default_language; + } + + this.cookie.setCookie("frontend_lang", this.currentLanguage.code); + + this.currentCategory = this.pos_category.length > 0 ? this.pos_category[0].name : null; + // required information to send an order + this.paymentError = false; + this.tablePadNumber = null; + this.currentOrder = new Order({}); + this.eatingLocation = "in"; // (in, out) in by default because out can be disabled in the config + } + + dispatchMessage(message) { + const payload = message.payload; + const type = message.type; + + if (type === "status") { + if (payload.status === "closed") { + this.pos_session = []; + } else { + // reload to get potential new settings + // more easier than RPC for now + window.location.reload(); + } + this.isSession(); + } else if (type === "payment_status") { + if (payload.payment_result === "Success") { + this.updateOrderFromServer(payload.order); + this.router.navigate("payment_success"); + } else { + this.paymentError = true; + } + } + } + + updateOrderFromServer(order) { + this.currentOrder.updateDataFromServer(order); + } + + initData() { + super.initData(); + + this.combos = this.combos.map((c) => { + const combo = new Combo(c); + this.comboByIds[combo.id] = combo; + return combo; + }); + } + + isSession() { + if (!this.pos_session || !this.pos_session.id) { + this.router.navigate("closed"); + } else if (this.router.activeSlot === "closed") { + this.router.navigate("default"); + } + } + + isOrder() { + if (!this.currentOrder || !this.currentOrder.lines.length) { + this.router.navigate("default"); + } + } +} + +export const selfOrderService = { + dependencies: ["rpc", "notification", "router", "bus_service", "cookie"], + async start(env, { rpc, notification, router, bus_service, cookie }) { + return new selfOrder(env, rpc, notification, router, bus_service, cookie).ready; + }, +}; + +registry.category("services").add("self_order_kiosk", selfOrderService); + +export function useselfOrder() { + return useState(useService("self_order_kiosk")); +} diff --git a/addons/pos_self_order/static/src/kiosk/template/kiosk_template.js b/addons/pos_self_order/static/src/kiosk/template/kiosk_template.js new file mode 100644 index 00000000000..f26f8ed65a1 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/template/kiosk_template.js @@ -0,0 +1,54 @@ +/** @odoo-module */ + +import { Component, markup, onMounted } from "@odoo/owl"; +import { useselfOrder } from "@pos_self_order/kiosk/self_order_kiosk_service"; +import { useService } from "@web/core/utils/hooks"; +import { escape } from "@web/core/utils/strings"; + +export class KioskTemplate extends Component { + static template = "pos_self_order.KioskTemplate"; + static props = ["headerHeight", "contentHeight", "footerHeight", "slots"]; + + setup() { + this.selfOrder = useselfOrder(); + this.selfOrder.isSession(); + this.router = useService("router"); + this.idleTimer = null; + + onMounted(() => { + const style = document.createElement("style"); + const color = escape(this.selfOrder.color); + style.innerHTML = markup(`.btn-primary,.text-bg-primary,.bg-primary { + background-color: ${color} !important; + border-color: ${color} !important; + } .border-primary { + border-color: ${color} !important; + }`); + + document.getElementsByTagName("head")[0].appendChild(style); + window.addEventListener("touchstart", () => this.idleDetector()); + window.addEventListener("mousedown", () => this.idleDetector()); + window.addEventListener("click", () => this.idleDetector()); + window.addEventListener("load", () => this.idleDetector()); + + // disable right click, on your mobile (tactil kiosk), + // you can right-click while holding down the key + document.addEventListener("contextmenu", (event) => { + event.preventDefault(); + }); + }); + } + + get isHeaderBanner() { + return this.selfOrder.kiosk_image_home; + } + + idleDetector() { + clearTimeout(this.idleTimer); + this.idleTimer = setTimeout(() => this.router.navigate("default"), 3 * 60 * 1000); // 5 minutes + } + + get showShadow() { + return this.router.activeSlot === "default" ? false : true; + } +} diff --git a/addons/pos_self_order/static/src/kiosk/template/kiosk_template.scss b/addons/pos_self_order/static/src/kiosk/template/kiosk_template.scss new file mode 100644 index 00000000000..7eace8d75c8 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/template/kiosk_template.scss @@ -0,0 +1,13 @@ +.o_kiosk-template { + .header-section { + .header-image { + background-position: center; + background-size: cover; + } + } + .content-section { + } + .o_footer_shadow { + box-shadow: 0 -0.125rem 0.25rem rgba(0, 0, 0, 0.075); + } +} diff --git a/addons/pos_self_order/static/src/kiosk/template/kiosk_template.xml b/addons/pos_self_order/static/src/kiosk/template/kiosk_template.xml new file mode 100644 index 00000000000..774ca39dca5 --- /dev/null +++ b/addons/pos_self_order/static/src/kiosk/template/kiosk_template.xml @@ -0,0 +1,16 @@ + + + +
+
+ +
+
+ +
+
+ +
+
+
+
diff --git a/addons/pos_self_order/static/src/components/lines/lines.js b/addons/pos_self_order/static/src/mobile/components/lines/lines.js similarity index 92% rename from addons/pos_self_order/static/src/components/lines/lines.js rename to addons/pos_self_order/static/src/mobile/components/lines/lines.js index c2d296c5de8..393a99a6522 100644 --- a/addons/pos_self_order/static/src/components/lines/lines.js +++ b/addons/pos_self_order/static/src/mobile/components/lines/lines.js @@ -1,7 +1,7 @@ /** @odoo-module */ import { Component } from "@odoo/owl"; -import { useSelfOrder } from "@pos_self_order/self_order_service"; +import { useSelfOrder } from "@pos_self_order/mobile/self_order_mobile_service"; import { _t } from "@web/core/l10n/translation"; import { extractProductNameAndAttributes } from "../../utils"; import { useService } from "@web/core/utils/hooks"; diff --git a/addons/pos_self_order/static/src/components/lines/lines.xml b/addons/pos_self_order/static/src/mobile/components/lines/lines.xml similarity index 100% rename from addons/pos_self_order/static/src/components/lines/lines.xml rename to addons/pos_self_order/static/src/mobile/components/lines/lines.xml diff --git a/addons/pos_self_order/static/src/components/navbar/navbar.js b/addons/pos_self_order/static/src/mobile/components/navbar/navbar.js similarity index 89% rename from addons/pos_self_order/static/src/components/navbar/navbar.js rename to addons/pos_self_order/static/src/mobile/components/navbar/navbar.js index 873daf5b40c..bd437685f25 100644 --- a/addons/pos_self_order/static/src/components/navbar/navbar.js +++ b/addons/pos_self_order/static/src/mobile/components/navbar/navbar.js @@ -1,7 +1,7 @@ /** @odoo-module */ import { Component } from "@odoo/owl"; -import { useSelfOrder } from "@pos_self_order/self_order_service"; +import { useSelfOrder } from "@pos_self_order/mobile/self_order_mobile_service"; import { useService } from "@web/core/utils/hooks"; export class NavBar extends Component { static template = "pos_self_order.NavBar"; diff --git a/addons/pos_self_order/static/src/components/navbar/navbar.scss b/addons/pos_self_order/static/src/mobile/components/navbar/navbar.scss similarity index 100% rename from addons/pos_self_order/static/src/components/navbar/navbar.scss rename to addons/pos_self_order/static/src/mobile/components/navbar/navbar.scss diff --git a/addons/pos_self_order/static/src/components/navbar/navbar.xml b/addons/pos_self_order/static/src/mobile/components/navbar/navbar.xml similarity index 100% rename from addons/pos_self_order/static/src/components/navbar/navbar.xml rename to addons/pos_self_order/static/src/mobile/components/navbar/navbar.xml diff --git a/addons/pos_self_order/static/src/components/popup_table/popup_table.js b/addons/pos_self_order/static/src/mobile/components/popup_table/popup_table.js similarity index 94% rename from addons/pos_self_order/static/src/components/popup_table/popup_table.js rename to addons/pos_self_order/static/src/mobile/components/popup_table/popup_table.js index 976fa7d546c..5c2ffc786cf 100644 --- a/addons/pos_self_order/static/src/components/popup_table/popup_table.js +++ b/addons/pos_self_order/static/src/mobile/components/popup_table/popup_table.js @@ -1,7 +1,7 @@ /** @odoo-module */ import { Component, onWillStart, useState } from "@odoo/owl"; -import { useSelfOrder } from "@pos_self_order/self_order_service"; +import { useSelfOrder } from "@pos_self_order/mobile/self_order_mobile_service"; import { useService } from "@web/core/utils/hooks"; import { groupBy } from "@web/core/utils/arrays"; diff --git a/addons/pos_self_order/static/src/components/popup_table/popup_table.scss b/addons/pos_self_order/static/src/mobile/components/popup_table/popup_table.scss similarity index 100% rename from addons/pos_self_order/static/src/components/popup_table/popup_table.scss rename to addons/pos_self_order/static/src/mobile/components/popup_table/popup_table.scss diff --git a/addons/pos_self_order/static/src/components/popup_table/popup_table.xml b/addons/pos_self_order/static/src/mobile/components/popup_table/popup_table.xml similarity index 100% rename from addons/pos_self_order/static/src/components/popup_table/popup_table.xml rename to addons/pos_self_order/static/src/mobile/components/popup_table/popup_table.xml diff --git a/addons/pos_self_order/static/src/components/price_details/price_details.js b/addons/pos_self_order/static/src/mobile/components/price_details/price_details.js similarity index 77% rename from addons/pos_self_order/static/src/components/price_details/price_details.js rename to addons/pos_self_order/static/src/mobile/components/price_details/price_details.js index 38e4808db5b..2185534ee10 100644 --- a/addons/pos_self_order/static/src/components/price_details/price_details.js +++ b/addons/pos_self_order/static/src/mobile/components/price_details/price_details.js @@ -1,7 +1,7 @@ /** @odoo-module */ import { Component } from "@odoo/owl"; -import { useSelfOrder } from "@pos_self_order/self_order_service"; +import { useSelfOrder } from "@pos_self_order/mobile/self_order_mobile_service"; export class PriceDetails extends Component { static template = "pos_self_order.PriceDetails"; diff --git a/addons/pos_self_order/static/src/components/price_details/price_details.xml b/addons/pos_self_order/static/src/mobile/components/price_details/price_details.xml similarity index 100% rename from addons/pos_self_order/static/src/components/price_details/price_details.xml rename to addons/pos_self_order/static/src/mobile/components/price_details/price_details.xml diff --git a/addons/pos_self_order/static/src/components/product_card/product_card.js b/addons/pos_self_order/static/src/mobile/components/product_card/product_card.js similarity index 93% rename from addons/pos_self_order/static/src/components/product_card/product_card.js rename to addons/pos_self_order/static/src/mobile/components/product_card/product_card.js index 908f0cf6807..c33b819ca81 100644 --- a/addons/pos_self_order/static/src/components/product_card/product_card.js +++ b/addons/pos_self_order/static/src/mobile/components/product_card/product_card.js @@ -1,7 +1,7 @@ /** @odoo-module */ import { Component } from "@odoo/owl"; -import { useSelfOrder } from "@pos_self_order/self_order_service"; +import { useSelfOrder } from "@pos_self_order/mobile/self_order_mobile_service"; import { useForwardRefToParent, useService } from "@web/core/utils/hooks"; export class ProductCard extends Component { diff --git a/addons/pos_self_order/static/src/components/product_card/product_card.scss b/addons/pos_self_order/static/src/mobile/components/product_card/product_card.scss similarity index 95% rename from addons/pos_self_order/static/src/components/product_card/product_card.scss rename to addons/pos_self_order/static/src/mobile/components/product_card/product_card.scss index b0bc5977b4c..1e70a46b752 100644 --- a/addons/pos_self_order/static/src/components/product_card/product_card.scss +++ b/addons/pos_self_order/static/src/mobile/components/product_card/product_card.scss @@ -11,10 +11,7 @@ min-width: 90px; min-height: 90px; height: 90px !important; - - & img { - object-fit: cover; - } + object-fit: cover; } .o_self_order_item_card { border-right: 1px solid $border-color; diff --git a/addons/pos_self_order/static/src/components/product_card/product_card.xml b/addons/pos_self_order/static/src/mobile/components/product_card/product_card.xml similarity index 96% rename from addons/pos_self_order/static/src/components/product_card/product_card.xml rename to addons/pos_self_order/static/src/mobile/components/product_card/product_card.xml index 1c4cdf7c0fc..31dcc95b0b5 100644 --- a/addons/pos_self_order/static/src/components/product_card/product_card.xml +++ b/addons/pos_self_order/static/src/mobile/components/product_card/product_card.xml @@ -24,7 +24,7 @@
Product image diff --git a/addons/pos_self_order/static/src/hooks/use_detection.js b/addons/pos_self_order/static/src/mobile/hooks/use_detection.js similarity index 87% rename from addons/pos_self_order/static/src/hooks/use_detection.js rename to addons/pos_self_order/static/src/mobile/hooks/use_detection.js index ec1344aaab0..2f3cc14cdfc 100644 --- a/addons/pos_self_order/static/src/hooks/use_detection.js +++ b/addons/pos_self_order/static/src/mobile/hooks/use_detection.js @@ -13,7 +13,7 @@ import { useEffect, useState } from "@odoo/owl"; export function useDetection(root, targets, getDeps) { const detected = useState({ name: "" }); useEffect((stop) => { - if (stop) { + if (stop || !root) { return; } /** @type{IntersectionObserverEntry[]} */ @@ -31,11 +31,7 @@ export function useDetection(root, targets, getDeps) { delete allVisible[name]; } - if (Object.keys(allVisible).length > 0) { - selectedName = Object.entries(allVisible).sort((a, b) => { - return b[1] - a[1]; - })[0][0]; - } + selectedName = Object.keys(allVisible).reverse()[0]; }); if (selectedName) { diff --git a/addons/pos_self_order/static/src/pages/landing_page/landing_page.js b/addons/pos_self_order/static/src/mobile/pages/landing_page/landing_page.js similarity index 89% rename from addons/pos_self_order/static/src/pages/landing_page/landing_page.js rename to addons/pos_self_order/static/src/mobile/pages/landing_page/landing_page.js index 286dff3e579..eb48d0c3e13 100644 --- a/addons/pos_self_order/static/src/pages/landing_page/landing_page.js +++ b/addons/pos_self_order/static/src/mobile/pages/landing_page/landing_page.js @@ -1,7 +1,7 @@ /** @odoo-module */ import { Component } from "@odoo/owl"; -import { useSelfOrder } from "@pos_self_order/self_order_service"; +import { useSelfOrder } from "@pos_self_order/mobile/self_order_mobile_service"; import { useService } from "@web/core/utils/hooks"; import { formatMonetary } from "@web/views/fields/formatters"; export class LandingPage extends Component { diff --git a/addons/pos_self_order/static/src/pages/landing_page/landing_page.xml b/addons/pos_self_order/static/src/mobile/pages/landing_page/landing_page.xml similarity index 97% rename from addons/pos_self_order/static/src/pages/landing_page/landing_page.xml rename to addons/pos_self_order/static/src/mobile/pages/landing_page/landing_page.xml index 48ab31e7589..0a108db83d3 100644 --- a/addons/pos_self_order/static/src/pages/landing_page/landing_page.xml +++ b/addons/pos_self_order/static/src/mobile/pages/landing_page/landing_page.xml @@ -3,7 +3,7 @@
diff --git a/addons/pos_self_order/static/src/pages/order_cart/order_cart.js b/addons/pos_self_order/static/src/mobile/pages/order_cart/order_cart.js similarity index 80% rename from addons/pos_self_order/static/src/pages/order_cart/order_cart.js rename to addons/pos_self_order/static/src/mobile/pages/order_cart/order_cart.js index cd3661bd934..c9a384a839c 100644 --- a/addons/pos_self_order/static/src/pages/order_cart/order_cart.js +++ b/addons/pos_self_order/static/src/mobile/pages/order_cart/order_cart.js @@ -1,12 +1,12 @@ /** @odoo-module */ import { Component, onWillStart, useState } from "@odoo/owl"; -import { NavBar } from "@pos_self_order/components/navbar/navbar"; -import { ProductCard } from "@pos_self_order/components/product_card/product_card"; -import { Lines } from "@pos_self_order/components/lines/lines"; -import { useSelfOrder } from "@pos_self_order/self_order_service"; -import { PriceDetails } from "@pos_self_order/components/price_details/price_details"; -import { PopupTable } from "@pos_self_order/components/popup_table/popup_table"; +import { NavBar } from "@pos_self_order/mobile/components/navbar/navbar"; +import { ProductCard } from "@pos_self_order/mobile/components/product_card/product_card"; +import { Lines } from "@pos_self_order/mobile/components/lines/lines"; +import { useSelfOrder } from "@pos_self_order/mobile/self_order_mobile_service"; +import { PriceDetails } from "@pos_self_order/mobile/components/price_details/price_details"; +import { PopupTable } from "@pos_self_order/mobile/components/popup_table/popup_table"; import { _t } from "@web/core/l10n/translation"; import { useService } from "@web/core/utils/hooks"; diff --git a/addons/pos_self_order/static/src/pages/order_cart/order_cart.scss b/addons/pos_self_order/static/src/mobile/pages/order_cart/order_cart.scss similarity index 100% rename from addons/pos_self_order/static/src/pages/order_cart/order_cart.scss rename to addons/pos_self_order/static/src/mobile/pages/order_cart/order_cart.scss diff --git a/addons/pos_self_order/static/src/pages/order_cart/order_cart.xml b/addons/pos_self_order/static/src/mobile/pages/order_cart/order_cart.xml similarity index 100% rename from addons/pos_self_order/static/src/pages/order_cart/order_cart.xml rename to addons/pos_self_order/static/src/mobile/pages/order_cart/order_cart.xml diff --git a/addons/pos_self_order/static/src/pages/orders_history/orders_history.js b/addons/pos_self_order/static/src/mobile/pages/orders_history/orders_history.js similarity index 64% rename from addons/pos_self_order/static/src/pages/orders_history/orders_history.js rename to addons/pos_self_order/static/src/mobile/pages/orders_history/orders_history.js index f5df995aaaf..a9898e5a22b 100644 --- a/addons/pos_self_order/static/src/pages/orders_history/orders_history.js +++ b/addons/pos_self_order/static/src/mobile/pages/orders_history/orders_history.js @@ -1,11 +1,11 @@ /** @odoo-module */ import { Component, useState } from "@odoo/owl"; -import { useSelfOrder } from "@pos_self_order/self_order_service"; -import { ProductCard } from "@pos_self_order/components/product_card/product_card"; -import { Lines } from "@pos_self_order/components/lines/lines"; -import { PriceDetails } from "@pos_self_order/components/price_details/price_details"; -import { NavBar } from "@pos_self_order/components/navbar/navbar"; +import { useSelfOrder } from "@pos_self_order/mobile/self_order_mobile_service"; +import { ProductCard } from "@pos_self_order/mobile/components/product_card/product_card"; +import { Lines } from "@pos_self_order/mobile/components/lines/lines"; +import { PriceDetails } from "@pos_self_order/mobile/components/price_details/price_details"; +import { NavBar } from "@pos_self_order/mobile/components/navbar/navbar"; import { useService } from "@web/core/utils/hooks"; export class OrdersHistory extends Component { @@ -13,14 +13,14 @@ export class OrdersHistory extends Component { static components = { NavBar, ProductCard, Lines, PriceDetails }; static props = []; - setup() { + async setup() { this.selfOrder = useSelfOrder(); this.router = useService("router"); this.state = useState({ loadingProgress: true, }); - this.loadOrder(); + await this.loadOrder(); } async loadOrder() { @@ -29,7 +29,7 @@ export class OrdersHistory extends Component { } get orders() { - return this.selfOrder.orders.filter((o) => o.access_token); + return this.selfOrder.orders.filter((o) => o.access_token).sort((a, b) => b.id - a.id); } editOrder(order) { diff --git a/addons/pos_self_order/static/src/pages/orders_history/orders_history.xml b/addons/pos_self_order/static/src/mobile/pages/orders_history/orders_history.xml similarity index 100% rename from addons/pos_self_order/static/src/pages/orders_history/orders_history.xml rename to addons/pos_self_order/static/src/mobile/pages/orders_history/orders_history.xml diff --git a/addons/pos_self_order/static/src/pages/product_list/product_list.js b/addons/pos_self_order/static/src/mobile/pages/product_list/product_list.js similarity index 84% rename from addons/pos_self_order/static/src/pages/product_list/product_list.js rename to addons/pos_self_order/static/src/mobile/pages/product_list/product_list.js index d0e6098bcc0..b0f36a46ab5 100644 --- a/addons/pos_self_order/static/src/pages/product_list/product_list.js +++ b/addons/pos_self_order/static/src/mobile/pages/product_list/product_list.js @@ -1,13 +1,13 @@ /** @odoo-module */ import { Component, onMounted, useEffect, useRef, useState } from "@odoo/owl"; -import { useSelfOrder } from "@pos_self_order/self_order_service"; +import { useSelfOrder } from "@pos_self_order/mobile/self_order_mobile_service"; import { useAutofocus, useChildRef, useService } from "@web/core/utils/hooks"; -import { NavBar } from "@pos_self_order/components/navbar/navbar"; -import { ProductCard } from "@pos_self_order/components/product_card/product_card"; +import { NavBar } from "@pos_self_order/mobile/components/navbar/navbar"; +import { ProductCard } from "@pos_self_order/mobile/components/product_card/product_card"; import { fuzzyLookup } from "@web/core/utils/search"; -import { useScrollDirection } from "@pos_self_order/hooks/use_scroll_direction"; -import { useDetection } from "@pos_self_order/hooks/use_detection"; +import { useScrollDirection } from "@pos_self_order/common/hooks/use_scroll_direction"; +import { useDetection } from "@pos_self_order/mobile/hooks/use_detection"; import { Transition } from "@web/core/transition"; export class ProductList extends Component { @@ -35,13 +35,13 @@ export class ProductList extends Component { this.productGroups = Object.fromEntries( Array.from(this.selfOrder.categoryList).map((category) => { - return [category, useRef(`productsWithCategory_${category}`)]; + return [category.name, useRef(`productsWithCategory_${category.name}`)]; }) ); this.categoryButton = Object.fromEntries( Array.from(this.selfOrder.categoryList).map((category) => { - return [category, useRef(`category_${category}`)]; + return [category.name, useRef(`category_${category.name}`)]; }) ); @@ -51,7 +51,7 @@ export class ProductList extends Component { } }); - // this is used to hide the navbar when the user is scrolling down + // This is used to hide the navbar when the user is scrolling down this.scroll = useScrollDirection(this.productsList); // This detects the product group where we are currently scrolled @@ -120,11 +120,7 @@ export class ProductList extends Component { // where the products with that category are displayed. // We don't have to manually mark the category as selected because the // useDetection hook will do it for us. - this.scrollTo( - this.currentProductGroup.name != category_name - ? this.productGroups[category_name] - : null - ); + this.scrollTo(this.productGroups[category_name]); } focusSearch() { diff --git a/addons/pos_self_order/static/src/pages/product_list/product_list.xml b/addons/pos_self_order/static/src/mobile/pages/product_list/product_list.xml similarity index 93% rename from addons/pos_self_order/static/src/pages/product_list/product_list.xml rename to addons/pos_self_order/static/src/mobile/pages/product_list/product_list.xml index 668ca14dc81..137833b591c 100644 --- a/addons/pos_self_order/static/src/pages/product_list/product_list.xml +++ b/addons/pos_self_order/static/src/mobile/pages/product_list/product_list.xml @@ -21,13 +21,13 @@
@@ -50,10 +50,10 @@

No products found

- -
-

- + +
+

+

+ t-esc="selfOrder.formatMonetary(props.product.price_info.display_price)"/>

this); } - async setup(env, rpc, notification, router) { - Object.assign(this, { - ...session.pos_self_order_data, - }); - - this.env = env; - this.router = router; - this.rpc = rpc; + async setup(...args) { this.orders = []; this.editedOrder = null; - this.productByIds = {}; this.ordering = false; - this.priceLoading = false; - this.currentProduct = 0; - this.lastEditedProductId = null; - this.productsGroupedByCategory = {}; - this.notification = notification; - this.initData(); - this.categoryList = new Set( - this.pos_category - .sort((a, b) => a.sequence - b.sequence) - .map((c) => c.name) - .filter((c) => this.productsGroupedByCategory[c]) - ); + + await super.setup(...args); if (this.self_order_mode !== "qr_code") { effect((state) => this.saveOrderToLocalStorage(state.orders), [this]); } - if (this.has_active_session && this.self_order_mode !== "qr_code") { + if (this.has_active_session && this.self_order_mode !== "qr_code" && this.access_token) { this.ordering = true; } @@ -60,11 +39,7 @@ export class SelfOrder { } initData() { - this.products = this.products.map((p) => { - const product = new Product(p, this.show_prices_with_tax_included); - this.productByIds[product.id] = product; - return product; - }); + super.initData(); if (this.self_order_mode !== "qr_code") { const orders = JSON.parse(localStorage.getItem("orders")) ?? []; @@ -76,14 +51,6 @@ export class SelfOrder { }) ); } - - this.productsGroupedByCategory = this.products.reduce((acc, product) => { - product.pos_categ_ids.map((pos_categ_ids) => { - acc[pos_categ_ids] = acc[pos_categ_ids] || []; - acc[pos_categ_ids].push(product); - }); - return acc; - }, {}); } saveOrderToLocalStorage(orders) { @@ -136,15 +103,11 @@ export class SelfOrder { } } - formatMonetary(price) { - return formatMonetary(price, { currencyId: this.currency_id }); - } - async sendDraftOrderToServer() { try { const rpcUrl = this.currentOrder.isAlreadySent ? "/pos-self-order/update-existing-order" - : "/pos-self-order/process-new-order"; + : "/pos-self-order/process-new-order/self"; const order = await this.rpc(rpcUrl, { order: this.currentOrder, @@ -196,15 +159,11 @@ export class SelfOrder { //FIXME, if the user refresh the page with not sent, we will lost this data. const accessTokensFromServer = orders.map((order) => order.access_token); - for (const idx in this.orders) { - const order = this.orders[idx]; - + for (const order of this.orders) { if (order.access_token) { const orderFromServer = orders.find((o) => o.access_token === order.access_token); - if (orderFromServer) { - this.orders[idx] = Object.assign(this.orders[idx], orderFromServer); - this.orders[idx].lines = orderFromServer.lines.map((l) => new Line(l)); + order.updateDataFromServer(orderFromServer); } } } @@ -219,65 +178,6 @@ export class SelfOrder { } } - async getPricesFromServer() { - this.priceLoading = true; - - try { - if (!this.currentOrder) { - return; - } - - const taxes = await this.rpc(`/pos-self-order/get-orders-taxes/`, { - order: this.currentOrder, - access_token: this.access_token, - }); - - for (const line of this.currentOrder.lines) { - const lineTaxes = taxes.lines.find((ol) => ol.uuid === line.uuid); - line.price_subtotal = lineTaxes.price_subtotal; - line.price_subtotal_incl = lineTaxes.price_subtotal_incl; - } - - this.editedOrder.amount_total = taxes.amount_total; - this.editedOrder.amount_tax = taxes.amount_tax; - } catch (error) { - this.handleErrorNotification(error); - } - - this.priceLoading = false; - } - - handleErrorNotification(error, accessToken = []) { - let message = _t("An error has occurred"); - let cleanOrders = false; - - if (error instanceof RPCError) { - if (error.data.name === "werkzeug.exceptions.Unauthorized") { - message = _t("You're not authorized to perform this action"); - cleanOrders = true; - } else if (error.data.name === "werkzeug.exceptions.NotFound") { - message = _t("Orders wasn't found on the server"); - cleanOrders = true; - } - } else if (error instanceof ConnectionLostError) { - message = _t("Connection lost, please try again later"); - } - - this.notification.add(message, { - type: "danger", - }); - - if (accessToken && cleanOrders) { - this.editedOrder = null; - - for (const index in this.orders) { - if (accessToken.includes(this.orders[index].access_token)) { - this.orders.splice(index, 1); - } - } - } - } - changeOrderState(access_token, state) { const order = this.orders.filter((o) => o.access_token === access_token); let message = _t("Your order status has been changed"); diff --git a/addons/pos_self_order/static/src/self_order_root.js b/addons/pos_self_order/static/src/mobile/self_order_root.js similarity index 72% rename from addons/pos_self_order/static/src/self_order_root.js rename to addons/pos_self_order/static/src/mobile/self_order_root.js index 0969e6a6252..f9b6d3e9862 100644 --- a/addons/pos_self_order/static/src/self_order_root.js +++ b/addons/pos_self_order/static/src/mobile/self_order_root.js @@ -3,15 +3,15 @@ import { Component, whenReady, App } from "@odoo/owl"; import { makeEnv, startServices } from "@web/env"; import { setLoadXmlDefaultApp, templates } from "@web/core/assets"; import { _t } from "@web/core/l10n/translation"; -import { LandingPage } from "@pos_self_order/pages/landing_page/landing_page"; -import { NavBar } from "@pos_self_order/components/navbar/navbar"; -import { ProductMainView } from "@pos_self_order/pages/product_main_view/product_main_view"; -import { OrderCart } from "@pos_self_order/pages/order_cart/order_cart"; -import { ProductList } from "@pos_self_order/pages/product_list/product_list"; -import { OrdersHistory } from "@pos_self_order/pages/orders_history/orders_history"; -import { Router } from "@pos_self_order/router"; +import { LandingPage } from "@pos_self_order/mobile/pages/landing_page/landing_page"; +import { NavBar } from "@pos_self_order/mobile/components/navbar/navbar"; +import { ProductMainView } from "@pos_self_order/mobile/pages/product_main_view/product_main_view"; +import { OrderCart } from "@pos_self_order/mobile/pages/order_cart/order_cart"; +import { ProductList } from "@pos_self_order/mobile/pages/product_list/product_list"; +import { OrdersHistory } from "@pos_self_order/mobile/pages/orders_history/orders_history"; +import { Router } from "@pos_self_order/common/router"; import { MainComponentsContainer } from "@web/core/main_components_container"; -import { useSelfOrder } from "./self_order_service"; +import { useSelfOrder } from "@pos_self_order/mobile/self_order_mobile_service"; class SelfOrderRoot extends Component { static template = "pos_self_order.SelfOrderRoot"; diff --git a/addons/pos_self_order/static/src/self_order_root.xml b/addons/pos_self_order/static/src/mobile/self_order_root.xml similarity index 89% rename from addons/pos_self_order/static/src/self_order_root.xml rename to addons/pos_self_order/static/src/mobile/self_order_root.xml index 9b96698e576..405c2b18ef9 100644 --- a/addons/pos_self_order/static/src/self_order_root.xml +++ b/addons/pos_self_order/static/src/mobile/self_order_root.xml @@ -1,7 +1,7 @@ -
+
The restaurant is currently closed
diff --git a/addons/pos_self_order/static/src/utils.js b/addons/pos_self_order/static/src/mobile/utils.js similarity index 100% rename from addons/pos_self_order/static/src/utils.js rename to addons/pos_self_order/static/src/mobile/utils.js diff --git a/addons/pos_self_order/views/point_of_sale_dashboard.xml b/addons/pos_self_order/views/point_of_sale_dashboard.xml index 8f728f3fddf..a057987f881 100644 --- a/addons/pos_self_order/views/point_of_sale_dashboard.xml +++ b/addons/pos_self_order/views/point_of_sale_dashboard.xml @@ -1,5 +1,41 @@ + + pos.self.order.kiosk.read.only.form.dialog + pos.config + + +
+
From your Kiosk, open this URL:
+ +
+
+ + +
+ +
+
+ + + pos.self.order.search.view + pos.config + + + + + + + + + + Kiosk + pos.config + kanban,list,tree + + + pos.config.kanban.view.inherit.self_order pos.config @@ -10,7 +46,7 @@ position="inside"> + + + +
+ + + +
+
+ + + self_order_kiosk +
-
\ No newline at end of file + diff --git a/addons/pos_self_order/views/pos_config_view.xml b/addons/pos_self_order/views/pos_config_view.xml new file mode 100644 index 00000000000..25318af1d38 --- /dev/null +++ b/addons/pos_self_order/views/pos_config_view.xml @@ -0,0 +1,13 @@ + + + + pos.self.pos.config.tree.view + pos.config + + + + + + + + diff --git a/addons/pos_self_order/views/pos_self_order_kiosk.index.xml b/addons/pos_self_order/views/pos_self_order_kiosk.index.xml new file mode 100644 index 00000000000..254194883b6 --- /dev/null +++ b/addons/pos_self_order/views/pos_self_order_kiosk.index.xml @@ -0,0 +1,25 @@ + + + + diff --git a/addons/pos_self_order/views/index.xml b/addons/pos_self_order/views/pos_self_order_mobile.index.xml similarity index 83% rename from addons/pos_self_order/views/index.xml rename to addons/pos_self_order/views/pos_self_order_mobile.index.xml index 653b244ce9b..7e68441f407 100644 --- a/addons/pos_self_order/views/index.xml +++ b/addons/pos_self_order/views/pos_self_order_mobile.index.xml @@ -1,6 +1,6 @@ -