diff --git a/pos_kiosk/README.rst b/pos_kiosk/README.rst new file mode 100644 index 0000000000..9180d20134 --- /dev/null +++ b/pos_kiosk/README.rst @@ -0,0 +1,113 @@ +========================== +Point of Sale - Kiosk Mode +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2990d060797f495b7bd63bf4a1782c2b706f93a9e9ee93b0b31454959bff03d4 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpos-lightgray.png?logo=github + :target: https://github.com/OCA/pos/tree/14.0/pos_kiosk + :alt: OCA/pos +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pos-14-0/pos-14-0-pos_kiosk + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/pos&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +[ This file must be max 2-3 paragraphs, and is required. ] + +This module extends the functionality of ... to support ... +and to allow you to ... + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +[ This file is optional, it should explain how to configure + the module before using it; it is aimed at advanced users. ] + +To configure this module, you need to: + +#. Go to ... + +.. figure:: https://raw.githubusercontent.com/OCA/pos/14.0/pos_kiosk/static/description/image.png + :alt: alternative description + :width: 600 px + +Usage +===== + +[ This file must be present and contains the usage instructions + for end-users. As all other rst files included in the README, + it MUST NOT contain reStructuredText sections + only body text (paragraphs, lists, tables, etc). Should you need + a more elaborate structure to explain the addon, please create a + Sphinx documentation (which may include this file as a "quick start" + section). ] + +To use this module, you need to: + +#. Go to ... + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* KMEE + +Contributors +~~~~~~~~~~~~ + +* Ygor Carvalho + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/pos `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pos_kiosk/__init__.py b/pos_kiosk/__init__.py new file mode 100644 index 0000000000..91c5580fed --- /dev/null +++ b/pos_kiosk/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/pos_kiosk/__manifest__.py b/pos_kiosk/__manifest__.py new file mode 100644 index 0000000000..00c386eb51 --- /dev/null +++ b/pos_kiosk/__manifest__.py @@ -0,0 +1,33 @@ +{ + "name": "Point of Sale - Kiosk Mode", + "version": "14.0.1.0.0", + "category": "Sales/Point of Sale", + "summary": "Kiosk mode extension for the Point of Sale ", + "author": "KMEE, Odoo Community Association (OCA)", + "depends": ["point_of_sale"], + "development_status": "Alpha", + "website": "https://github.com/OCA/pos", + "data": [ + "views/pos_config_view.xml", + "views/pos_assets_kiosk_common.xml", + "views/pos_assets_kiosk_index.xml", + ], + "qweb": [ + "static/src/xml/ChromeKiosk.xml", + "static/src/xml/Components/KioskHeader.xml", + "static/src/xml/Components/KioskCartFooter.xml", + "static/src/xml/Screens/WelcomeScreen.xml", + "static/src/xml/Screens/ProductScreen/KioskProductScreen.xml", + "static/src/xml/Screens/ProductScreen/KioskProductCategory.xml", + "static/src/xml/Screens/KioskPaymentScreen.xml", + "static/src/xml/Modals/InsertProductModal.xml", + "static/src/xml/Modals/InsertProductConfigurableModal.xml", + "static/src/xml/Modals/CartModal.xml", + "static/src/xml/Screens/KioskReceiptScreen.xml", + "static/src/xml/Screens/KioskClientScreen.xml", + "static/src/xml/Modals/ErrorModal.xml", + ], + "installable": True, + "auto_install": False, + "license": "LGPL-3", +} diff --git a/pos_kiosk/controllers/__init__.py b/pos_kiosk/controllers/__init__.py new file mode 100644 index 0000000000..12a7e529b6 --- /dev/null +++ b/pos_kiosk/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/pos_kiosk/controllers/main.py b/pos_kiosk/controllers/main.py new file mode 100644 index 0000000000..ded83cb061 --- /dev/null +++ b/pos_kiosk/controllers/main.py @@ -0,0 +1,48 @@ +import werkzeug.utils + +from odoo import http +from odoo.http import request +from odoo.osv.expression import AND + + +class PosKioskController(http.Controller): + @http.route(["/pos_kiosk/web", "/pos_kiosk/ui"], type="http", auth="user") + def pos_kiosk_web(self, config_id=False, **k): + domain = [ + ("state", "in", ["opening_control", "opened"]), + ("user_id", "=", request.session.uid), + ("rescue", "=", False), + ] + + if config_id: + domain = AND([domain, [("config_id", "=", config_id)]]) + pos_session = request.env["pos.session"].sudo().search(domain, limit=1) + + if not pos_session and config_id: + domain = [ + ("state", "in", ["opening_control", "opened"]), + ("rescue", "=", False), + ("config_id", "=", int(config_id)), + ] + pos_session = request.env["pos.session"].sudo().search(domain, limit=1) + + if not pos_session: + return werkzeug.utils.redirect( + "/web#action=point_of_sale.action_client_pos_menu" + ) + + company = pos_session.company_id + session_info = request.env["ir.http"].session_info() + session_info["user_context"]["allowed_company_ids"] = company.ids + session_info["user_companies"] = { + "current_company": (company.id, company.name), + "allowed_companies": [(company.id, company.name)], + } + context = { + "session_info": session_info, + "login_number": pos_session.login(), + } + + response = request.render("pos_kiosk.index", context) + response.headers["Cache-Control"] = "no-store" + return response diff --git a/pos_kiosk/models/__init__.py b/pos_kiosk/models/__init__.py new file mode 100644 index 0000000000..db8634ade1 --- /dev/null +++ b/pos_kiosk/models/__init__.py @@ -0,0 +1 @@ +from . import pos_config diff --git a/pos_kiosk/models/pos_config.py b/pos_kiosk/models/pos_config.py new file mode 100644 index 0000000000..94f2c64b20 --- /dev/null +++ b/pos_kiosk/models/pos_config.py @@ -0,0 +1,30 @@ +from odoo import fields, models + + +class PosConfig(models.Model): + _inherit = "pos.config" + + is_kiosk_mode = fields.Boolean( + string="Kiosk Mode", + help="If checked, the POS will be opened in kiosk mode.", + ) + + banner_image = fields.Binary( + string="Banner Image", + help="This field holds the image used as banner on the POS screen Kiosk Mode.", + ) + + top_banner_image = fields.Binary( + string="Top Banner Image", + help="This field holds the image used as top banner on the POS screen Kiosk Mode.", + ) + + logo_image = fields.Binary( + string="Logo Image", + help="This field holds the image used as logo on the POS header in Kiosk Mode.", + ) + + def _get_pos_base_url(self): + if self.is_kiosk_mode: + return "/pos_kiosk/web" if self._force_http() else "/pos_kiosk/ui" + return super(PosConfig, self)._get_pos_base_url() diff --git a/pos_kiosk/readme/CONFIGURE.rst b/pos_kiosk/readme/CONFIGURE.rst new file mode 100644 index 0000000000..754e51aeff --- /dev/null +++ b/pos_kiosk/readme/CONFIGURE.rst @@ -0,0 +1,10 @@ +[ This file is optional, it should explain how to configure + the module before using it; it is aimed at advanced users. ] + +To configure this module, you need to: + +#. Go to ... + +.. figure:: ../static/description/image.png + :alt: alternative description + :width: 600 px diff --git a/pos_kiosk/readme/CONTRIBUTORS.rst b/pos_kiosk/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..957041454b --- /dev/null +++ b/pos_kiosk/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Ygor Carvalho diff --git a/pos_kiosk/readme/DESCRIPTION.rst b/pos_kiosk/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..43cf54b776 --- /dev/null +++ b/pos_kiosk/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +[ This file must be max 2-3 paragraphs, and is required. ] + +This module extends the functionality of ... to support ... +and to allow you to ... diff --git a/pos_kiosk/readme/USAGE.rst b/pos_kiosk/readme/USAGE.rst new file mode 100644 index 0000000000..f4629c3d54 --- /dev/null +++ b/pos_kiosk/readme/USAGE.rst @@ -0,0 +1,11 @@ +[ This file must be present and contains the usage instructions + for end-users. As all other rst files included in the README, + it MUST NOT contain reStructuredText sections + only body text (paragraphs, lists, tables, etc). Should you need + a more elaborate structure to explain the addon, please create a + Sphinx documentation (which may include this file as a "quick start" + section). ] + +To use this module, you need to: + +#. Go to ... diff --git a/pos_kiosk/static/description/index.html b/pos_kiosk/static/description/index.html new file mode 100644 index 0000000000..5e1b7427c1 --- /dev/null +++ b/pos_kiosk/static/description/index.html @@ -0,0 +1,461 @@ + + + + + + +Point of Sale - Kiosk Mode + + + +
+

Point of Sale - Kiosk Mode

+ + +

Alpha License: LGPL-3 OCA/pos Translate me on Weblate Try me on Runboat

+

[ This file must be max 2-3 paragraphs, and is required. ]

+

This module extends the functionality of … to support … +and to allow you to …

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Configuration

+
+
[ This file is optional, it should explain how to configure
+
the module before using it; it is aimed at advanced users. ]
+
+

To configure this module, you need to:

+
    +
  1. Go to …
  2. +
+
+alternative description +
+
+
+

Usage

+
+
[ This file must be present and contains the usage instructions
+
for end-users. As all other rst files included in the README, +it MUST NOT contain reStructuredText sections +only body text (paragraphs, lists, tables, etc). Should you need +a more elaborate structure to explain the addon, please create a +Sphinx documentation (which may include this file as a “quick start” +section). ]
+
+

To use this module, you need to:

+
    +
  1. Go to …
  2. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • KMEE
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/pos project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/pos_kiosk/static/src/css/style.css b/pos_kiosk/static/src/css/style.css new file mode 100644 index 0000000000..baba2ebd73 --- /dev/null +++ b/pos_kiosk/static/src/css/style.css @@ -0,0 +1,1174 @@ +/* Tablet */ +@media (max-width: 834px) { + /* Banner Screen */ + .banner { + width: 100%; + height: 100%; + } + + .banner .image { + width: 100%; + height: 100%; + } + + .banner .container-button { + padding: 26px 41px; + left: 30%; + top: 75%; + } + + .banner .container-button .button { + font-size: 41px; + line-height: 41px; + } + + /* Main Screen */ + .payment { + width: 100%; + height: 820px; + } + + .payment .title-container { + width: 100%; + height: 128px; + margin-top: 7.5%; + padding: 5% 4%; + } + + .payment .title-container .main-title { + font-size: 30px; + line-height: 36px; + } + + .payment .title-container .sub-title { + font-size: 20px; + line-height: 28px; + } + + .payment .payment-container { + width: 75%; + height: 100%; + margin-top: 25%; + display: grid; + gap: 8px; + grid-template-columns: repeat(auto-fill, minmax(125px, 0.75fr)); + } + + .payment .payment-container .item { + width: 150px; + height: 100px; + } + + .payment .cart { + padding: 10px; + } + + .client { + width: 100%; + height: 820px; + } + + .client .title-container { + width: 100%; + height: 128px; + margin-top: 7.5%; + padding: 5% 4%; + } + + .client .title-container .main-title { + font-size: 30px; + line-height: 36px; + } + + .client .title-container .sub-title { + font-size: 20px; + line-height: 28px; + } + + .client .container-button { + display: flex; + flex-direction: column; + } + + /* .receipt { + width: 100%; + height: 820px; + } + + .receipt .title-container { + width: 100%; + height: 128px; + margin-top: 7.5%; + padding: 5% 4%; + } + + .receipt .title-container .main-title { + font-size: 30px; + line-height: 36px; + } + + .receipt .title-container .sub-title { + font-size: 20px; + line-height: 28px; + } */ + + .main-screen { + width: 100%; + height: 820px; + } + + .main-screen .category-container { + top: 5%; + margin-bottom: 10px; + overflow-x: auto; + margin-left: 2%; + margin-right: 2%; + } + + .grow.shrink.basis-0.h-\[100px\].justify-center.items-center.gap-4.flex { + display: flex; + margin-left: 5%; + margin-right: 5%; + } + + img.product-image.justify-center.relative.rounded-xl { + width: 100%; + } + + .main-screen .category-container .category { + width: 150px; + height: 100px; + } + + .main-screen .product-list { + max-height: 60%; + } + + .main-screen .product { + display: flex; + justify-content: center; + flex-direction: column; + align-content: flex-start; + flex-wrap: wrap; + width: 10rem; + height: 12rem; + } + + .main-screen .product .product-image { + height: 7rem; + } + + /* Estilos para o contêiner principal */ + .cart-container { + position: absolute; + bottom: 0; + width: 100%; + height: 96px; + padding-left: 32px; + padding-right: 32px; + padding-top: 16px; + padding-bottom: 16px; + background: #f3f4f6; + justify-content: flex-start; + align-items: center; + gap: 16px; + display: inline-flex; + } + + /* Estilos para o painel esquerdo */ + .left-panel { + flex: 1 1 0; + height: 64px; + justify-content: flex-start; + align-items: center; + gap: 32px; + display: flex; + } + + /* Estilos para o botão do carrinho */ + .button-container { + width: 150px; + padding-left: 32px; + padding-right: 32px; + padding-top: 20px; + padding-bottom: 20px; + border-radius: 8px; + overflow: hidden; + border: 1px #9ca3af solid; + justify-content: center; + align-items: center; + gap: 8px; + display: flex; + } + + /* Estilos para o ícone do carrinho */ + .icon-container { + justify-content: center; + align-items: center; + gap: 8px; + display: flex; + } + + .circle { + width: 32px; + height: 32px; + position: relative; + } + + .line { + width: 22.67px; + height: 22.67px; + left: 5.33px; + top: 5.33px; + position: absolute; + border: 1.5px #09090b solid; + } + + /* Estilos para o texto do carrinho */ + .text-container { + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 2px; + display: inline-flex; + } + + .item-count { + text-align: justify; + color: #1f2937; + font-size: 14px; + font-family: Inter; + font-weight: 700; + line-height: 14px; + word-wrap: break-word; + } + + .item-description { + text-align: justify; + color: #1f2937; + font-size: 12px; + font-family: Inter; + font-weight: 400; + line-height: 12px; + word-wrap: break-word; + } + + /* Estilos para o contêiner de preço */ + .price-container { + width: 173px; + flex: 1 1 0; + height: 64px; + padding-left: 15px; + padding-right: 15px; + padding-top: 26px; + padding-bottom: 26px; + border-radius: 8px; + overflow: hidden; + justify-content: space-between; + align-items: center; + display: flex; + } + + /* Estilos para o preço */ + .price { + text-align: justify; + color: #f9fafb; + font-size: 16px; + font-family: Inter; + font-weight: 600; + line-height: 24px; + word-wrap: break-word; + } + + /* Estilos para o contêiner de finalizar */ + .checkout-container { + justify-content: flex-start; + align-items: center; + gap: 8px; + display: flex; + } + + /* Estilos para o texto de finalizar */ + .checkout-text { + text-align: justify; + color: #f9fafb; + font-size: 18px; + font-family: Inter; + font-weight: 600; + line-height: 28px; + word-wrap: break-word; + } + + /* Estilos para a seta */ + .arrow-container { + width: 20px; + height: 20px; + position: relative; + } + + .arrow { + width: 15.43px; + height: 12px; + left: 17px; + top: 16px; + position: absolute; + transform: rotate(-180deg); + transform-origin: 0 0; + border: 2px #f9fafb solid; + } + + /* Estilos para o contêiner principal */ + .container { + width: 834px; + height: 332px; + background: white; + border-top-left-radius: 12px; + border-top-right-radius: 12px; + overflow: hidden; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + display: inline-flex; + } + + /* Estilos para o título "Seu carrinho" */ + .title { + color: #4b5563; + font-size: 18px; + font-family: Inter; + font-weight: 600; + line-height: 28px; + word-wrap: break-word; + } + + .heading { + width: 834px; + height: 88px; + padding-left: 32px; + justify-content: space-between; + align-items: center; + display: inline-flex; + } + + /* Estilos para as linhas do ícone do carrinho */ + .line { + width: 12px; + height: 12px; + left: 6px; + top: 6px; + position: absolute; + border: 2px #374151 solid; + } + + /* Estilos para os detalhes do produto */ + .product-details { + align-self: stretch; + height: 100px; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + display: flex; + } + + /* Estilos para o contêiner do produto */ + .product-container { + align-self: stretch; + padding-left: 32px; + padding-right: 32px; + padding-top: 16px; + padding-bottom: 16px; + justify-content: space-between; + align-items: center; + display: inline-flex; + } + + /* Estilos para a imagem do produto */ + .product-image { + width: 68px; + height: 68px; + position: relative; + background: #e5e7eb; + border-radius: 8px; + } + + /* Estilos para os detalhes do produto */ + .product-text { + flex-direction: column; + justify-content: center; + align-items: flex-start; + display: inline-flex; + } + + /* Estilos para o nome do produto */ + .product-name { + text-align: justify; + color: #111827; + font-size: 16px; + font-family: Inter; + font-weight: 400; + line-height: 24px; + word-wrap: break-word; + } + + /* Estilos para os preços do produto */ + .product-prices { + justify-content: center; + align-items: center; + gap: 8px; + display: inline-flex; + } + + /* Estilos para o preço original do produto */ + .original-price { + text-align: justify; + color: #4b5563; + font-size: 14px; + font-family: Inter; + font-weight: 400; + line-height: 20px; + word-wrap: break-word; + } + + /* Estilos para o preço atual do produto */ + .current-price { + text-align: justify; + color: #1f2937; + font-size: 14px; + font-family: Inter; + font-weight: 700; + line-height: 20px; + word-wrap: break-word; + } + + /* Estilos para o contêiner de quantidade */ + .quantity-container { + border-radius: 8px; + overflow: hidden; + border: 1px #9ca3af solid; + justify-content: flex-start; + align-items: center; + display: inline-flex; + } + + /* Estilos para o botão de quantidade */ + .quantity-button { + width: 48px; + height: 48px; + padding-top: 16px; + padding-bottom: 16px; + opacity: 0.5; + border-top-left-radius: 12px; + border-top-right-radius: 12px; + overflow: hidden; + justify-content: center; + align-items: center; + display: flex; + } + + /* Estilos para a seta do botão de quantidade */ + .arrow { + width: 13.87px; + height: 16px; + position: relative; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + display: flex; + } + + /* Estilos para as partes da seta */ + .arrow-part { + width: 5.33px; + height: 3.2px; + border: 1px #3b3b3b solid; + } + + /* Estilos para o número de itens */ + .item-number { + width: 48px; + height: 48px; + padding-left: 17px; + padding-right: 17px; + padding-top: 8px; + padding-bottom: 8px; + justify-content: center; + align-items: center; + gap: 8px; + display: flex; + } + + /* Estilos para o total e o botão de finalização */ + .total-and-checkout { + align-self: stretch; + height: 144px; + padding: 32px; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + gap: 8px; + display: flex; + } + + /* Estilos para o contêiner do total e do botão de finalização */ + .total-and-checkout-container { + align-self: stretch; + height: 80px; + padding-left: 32px; + padding-right: 32px; + border-radius: 8px; + overflow: hidden; + justify-content: space-between; + align-items: center; + display: inline-flex; + } + + /* Estilos para o texto do total e do preço */ + .total-and-price-text { + width: 490px; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + display: inline-flex; + } + + /* Estilos para o texto do total de itens */ + .total-items-text { + text-align: justify; + color: #f9fafb; + font-size: 16px; + font-family: Inter; + font-weight: 400; + line-height: 24px; + word-wrap: break-word; + } + + /* Estilos para o texto do preço */ + .total-price-text { + text-align: justify; + color: #f9fafb; + font-size: 18px; + font-family: Inter; + font-weight: 600; + line-height: 28px; + word-wrap: break-word; + } + + /* Estilos para o botão de finalização */ + .checkout-button { + justify-content: flex-start; + align-items: center; + gap: 8px; + display: flex; + } + + /* Estilos para o texto do botão de finalização */ + .checkout-text { + text-align: justify; + color: #f9fafb; + font-size: 20px; + font-family: Inter; + font-weight: 600; + line-height: 28px; + word-wrap: break-word; + } + + /* Estilos para a seta do botão de finalização */ + .checkout-arrow { + width: 24px; + height: 24px; + position: relative; + } + + /* Estilos para a parte da seta do botão de finalização */ + .checkout-arrow-part { + width: 18px; + height: 14px; + left: 21px; + top: 19px; + position: absolute; + transform: rotate(-180deg); + transform-origin: 0 0; + border: 2px #f9fafb solid; + } +} + +/* Kiosk */ +@media (min-width: 1080px) { + /* Banner Screen */ + .banner { + width: 100%; + height: 100%; + } + + .banner .image { + width: 100%; + height: 100%; + } + + .banner .container-button { + padding: 26px 41px; + left: 40%; + top: 85%; + justify-content: center; + } + + .banner .container-button .button { + font-size: 61px; + line-height: 61px; + } + + /* Main Screen */ + + .payment { + width: 100%; + height: 820px; + } + + .payment .title-container { + width: 100%; + height: 128px; + margin-top: 7.5%; + padding: 5% 4%; + } + + .payment .title-container .main-title { + font-size: 30px; + line-height: 36px; + } + + .payment .title-container .sub-title { + font-size: 20px; + line-height: 28px; + } + + .payment .payment-container { + width: 75%; + height: 100%; + margin-top: 10%; + } + + .payment .payment-container .item { + width: 150px; + height: 100px; + } + + .payment .cart { + padding: 10px; + } + + .client { + width: 100%; + height: 820px; + } + + .client .title-container { + width: 100%; + height: 128px; + margin-top: 7.5%; + padding: 5% 4%; + } + + .client .title-container .main-title { + font-size: 30px; + line-height: 36px; + } + + .client .title-container .sub-title { + font-size: 20px; + line-height: 28px; + } + + .client .container-button { + display: flex; + flex-direction: column; + } + + .receipt { + width: 100%; + height: 820px; + } + + .receipt .title-container { + width: 100%; + height: 128px; + margin-top: 7.5%; + padding: 5% 4%; + } + + .receipt .title-container .main-title { + font-size: 30px; + line-height: 36px; + } + + .receipt .title-container .sub-title { + font-size: 20px; + line-height: 28px; + } + + .main-screen { + width: 100%; + height: 820px; + } + + .main-screen .category-container { + top: 5%; + margin-bottom: 10px; + } + + .main-screen .category-container .category { + width: 250px; + height: 200px; + overflow-x: auto; + } + + .main-screen .product-list { + max-height: 80%; + } + + .main-screen .product { + display: flex; + justify-content: center; + flex-direction: column; + align-content: flex-start; + flex-wrap: wrap; + width: 20rem; + height: 22rem; + } + + .main-screen .product .product-image { + height: 14rem; + } + + /* Estilos para o contêiner principal */ + .cart-container { + position: absolute; + bottom: 0; + width: 100%; + height: 192px; + padding-left: 64px; + padding-right: 64px; + padding-top: 32px; + padding-bottom: 32px; + background: #f3f4f6; + justify-content: flex-start; + align-items: center; + gap: 32px; + display: inline-flex; + } + + /* Estilos para o painel esquerdo */ + .left-panel { + flex: 1 1 0; + height: 128px; + justify-content: flex-start; + align-items: center; + gap: 64px; + display: flex; + } + + /* Estilos para o botão do carrinho */ + .button-container { + width: 300px; + padding-left: 64px; + padding-right: 64px; + padding-top: 40px; + padding-bottom: 40px; + border-radius: 16px; + overflow: hidden; + border: 2px #9ca3af solid; + justify-content: center; + align-items: center; + gap: 16px; + display: flex; + } + + /* Estilos para o ícone do carrinho */ + .icon-container { + justify-content: center; + align-items: center; + gap: 16px; + display: flex; + } + + .circle { + width: 64px; + height: 64px; + position: relative; + } + + .line { + width: 45.34px; + height: 45.34px; + left: 10.66px; + top: 10.66px; + position: absolute; + border: 3px #09090b solid; + } + + /* Estilos para o texto do carrinho */ + .text-container { + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 4px; + display: inline-flex; + } + + .item-count { + text-align: justify; + color: #1f2937; + font-size: 28px; + font-family: Inter; + font-weight: 1400; + line-height: 28px; + word-wrap: break-word; + } + + .item-description { + text-align: justify; + color: #1f2937; + font-size: 24px; + font-family: Inter; + font-weight: 800; + line-height: 24px; + word-wrap: break-word; + } + + /* Estilos para o contêiner de preço */ + .price-container { + width: 346px; + flex: 2 2 0; + height: 128px; + padding-left: 30px; + padding-right: 30px; + padding-top: 52px; + padding-bottom: 52px; + border-radius: 16px; + overflow: hidden; + justify-content: space-between; + align-items: center; + display: flex; + } + + /* Estilos para o preço */ + .price { + text-align: justify; + color: #f9fafb; + font-size: 32px; + font-family: Inter; + font-weight: 1200; + line-height: 48px; + word-wrap: break-word; + } + + /* Estilos para o contêiner de finalizar */ + .checkout-container { + justify-content: flex-start; + align-items: center; + gap: 16px; + display: flex; + } + + /* Estilos para o texto de finalizar */ + .checkout-text { + text-align: justify; + color: #f9fafb; + font-size: 36px; + font-family: Inter; + font-weight: 1200; + line-height: 56px; + word-wrap: break-word; + } + + /* Estilos para a seta */ + .arrow-container { + width: 40px; + height: 40px; + position: relative; + } + + .arrow { + width: 30.86px; + height: 24px; + left: 34px; + top: 32px; + position: absolute; + transform: rotate(-180deg); + transform-origin: 0 0; + border: 4px #f9fafb solid; + } + + /* Estilos para o contêiner principal */ + .container { + width: 1668px; + height: 664px; + background: white; + border-top-left-radius: 24px; + border-top-right-radius: 24px; + overflow: hidden; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + display: inline-flex; + } + + /* Estilos para o título "Seu carrinho" */ + .title { + color: #4b5563; + font-size: 36px; + font-family: Inter; + font-weight: 1200; + line-height: 56px; + word-wrap: break-word; + } + + .heading { + width: 1668px; + height: 176px; + padding-left: 64px; + justify-content: space-between; + align-items: center; + display: inline-flex; + } + + /* Estilos para as linhas do ícone do carrinho */ + .line { + width: 24px; + height: 24px; + left: 12px; + top: 12px; + position: absolute; + border: 4px #374151 solid; + } + + /* Estilos para os detalhes do produto */ + .product-details { + align-self: stretch; + height: 200px; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + display: flex; + } + + /* Estilos para o contêiner do produto */ + .product-container { + align-self: stretch; + padding-left: 64px; + padding-right: 64px; + padding-top: 32px; + padding-bottom: 32px; + justify-content: space-between; + align-items: center; + display: inline-flex; + } + + /* Estilos para a imagem do produto */ + .product-image { + width: 132px; + height: 132px; + position: relative; + background: #e5e7eb; + border-radius: 16px; + } + + /* Estilos para os detalhes do produto */ + .product-text { + flex-direction: column; + justify-content: center; + align-items: flex-start; + display: inline-flex; + } + + /* Estilos para o nome do produto */ + .product-name { + text-align: justify; + color: #111827; + font-size: 32px; + font-family: Inter; + font-weight: 800; + line-height: 48px; + word-wrap: break-word; + } + + /* Estilos para os preços do produto */ + .product-prices { + justify-content: center; + align-items: center; + gap: 16px; + display: inline-flex; + } + + /* Estilos para o preço original do produto */ + .original-price { + text-align: justify; + color: #4b5563; + font-size: 28px; + font-family: Inter; + font-weight: 800; + line-height: 40px; + word-wrap: break-word; + } + + /* Estilos para o preço atual do produto */ + .current-price { + text-align: justify; + color: #1f2937; + font-size: 28px; + font-family: Inter; + font-weight: 1400; + line-height: 40px; + word-wrap: break-word; + } + + /* Estilos para o contêiner de quantidade */ + .quantity-container { + border-radius: 16px; + overflow: hidden; + border: 2px #9ca3af solid; + justify-content: flex-start; + align-items: center; + display: inline-flex; + } + + /* Estilos para o botão de quantidade */ + .quantity-button { + width: 96px; + height: 96px; + padding-top: 32px; + padding-bottom: 32px; + opacity: 0.5; + border-top-left-radius: 32px; + border-top-right-radius: 32px; + overflow: hidden; + justify-content: center; + align-items: center; + display: flex; + } + + /* Estilos para a seta do botão de quantidade */ + .arrow { + width: 27.74px; + height: 32px; + position: relative; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + display: flex; + } + + /* Estilos para as partes da seta */ + .arrow-part { + width: 10.66px; + height: 6.4px; + border: 2px #3b3b3b solid; + } + + /* Estilos para o número de itens */ + .item-number { + width: 96px; + height: 96px; + padding-left: 34px; + padding-right: 34px; + padding-top: 16px; + padding-bottom: 16px; + justify-content: center; + align-items: center; + gap: 16px; + display: flex; + } + + /* Estilos para o total e o botão de finalização */ + .total-and-checkout { + align-self: stretch; + height: 288px; + padding: 64px; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + gap: 16px; + display: flex; + } + + /* Estilos para o contêiner do total e do botão de finalização */ + .total-and-checkout-container { + align-self: stretch; + height: 120px; + padding-left: 64px; + padding-right: 64px; + border-radius: 16px; + overflow: hidden; + justify-content: space-between; + align-items: center; + display: inline-flex; + } + + /* Estilos para o texto do total e do preço */ + .total-and-price-text { + width: 980px; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + display: inline-flex; + } + + /* Estilos para o texto do total de itens */ + .total-items-text { + text-align: justify; + color: #f9fafb; + font-size: 32px; + font-family: Inter; + font-weight: 800; + line-height: 48px; + word-wrap: break-word; + } + + /* Estilos para o texto do preço */ + .total-price-text { + text-align: justify; + color: #f9fafb; + font-size: 36px; + font-family: Inter; + font-weight: 1200; + line-height: 56px; + word-wrap: break-word; + } + + /* Estilos para o botão de finalização */ + .checkout-button { + justify-content: flex-start; + align-items: center; + gap: 16px; + display: flex; + } + + /* Estilos para o texto do botão de finalização */ + .checkout-text { + text-align: justify; + color: #f9fafb; + font-size: 40px; + font-family: Inter; + font-weight: 1200; + line-height: 56px; + word-wrap: break-word; + } + + /* Estilos para a seta do botão de finalização */ + .checkout-arrow { + width: 48px; + height: 48px; + position: relative; + } + + /* Estilos para a parte da seta do botão de finalização */ + .checkout-arrow-part { + width: 36px; + height: 28px; + left: 44px; + top: 28px; + position: absolute; + transform: rotate(-180deg); + transform-origin: 0 0; + border: 4px #f9fafb solid; + } +} diff --git a/pos_kiosk/static/src/js/ChromeKiosk.js b/pos_kiosk/static/src/js/ChromeKiosk.js new file mode 100644 index 0000000000..a20d001bbe --- /dev/null +++ b/pos_kiosk/static/src/js/ChromeKiosk.js @@ -0,0 +1,375 @@ +odoo.define("pos_kiosk.ChromeKiosk", function (require) { + "use strict"; + + const {useState, useRef, useContext} = owl.hooks; + const {debounce} = owl.utils; + const {loadCSS} = require("web.ajax"); + const {useListener} = require("web.custom_hooks"); + const {CrashManager} = require("web.CrashManager"); + const {BarcodeEvents} = require("barcodes.BarcodeEvents"); + const PosComponent = require("point_of_sale.PosComponent"); + const NumberBuffer = require("point_of_sale.NumberBuffer"); + const PopupControllerMixin = require("point_of_sale.PopupControllerMixin"); + const Registries = require("point_of_sale.Registries"); + const contexts = require("point_of_sale.PosContext"); + + const models = require("point_of_sale.models"); + + class ChromeKiosk extends PopupControllerMixin(PosComponent) { + constructor() { + super(...arguments); + useListener("show-main-screen", this.__showScreen); + useListener("toggle-debug-widget", debounce(this._toggleDebugWidget, 100)); + useListener("show-temp-screen", this.__showTempScreen); + useListener("close-temp-screen", this.__closeTempScreen); + useListener("close-pos", this._closePos); + useListener("loading-skip-callback", () => this._loadingSkipCallback()); + useListener("set-sync-status", this._onSetSyncStatus); + NumberBuffer.activate(); + + this.chromeContext = useContext(contexts.chrome); + + this.state = useState({ + uiState: "LOADING", // 'LOADING' | 'READY' | 'CLOSING' + debugWidgetIsShown: true, + hasBigScrollBars: false, + sound: {src: null}, + }); + + this.loading = useState({ + message: "Loading", + skipButtonIsShown: false, + }); + + this.mainScreen = useState({name: null, component: null}); + this.mainScreenProps = {}; + + this.tempScreen = useState({isShown: false, name: null, component: null}); + this.tempScreenProps = {}; + + this.progressbar = useRef("progressbar"); + + this.previous_touch_y_coordinate = -1; + } + + mounted() { + $(document).off(); + $(window).off(); + $("html").off(); + $("body").off(); + BarcodeEvents.start(); + } + + willUnmount() { + BarcodeEvents.stop(); + } + + destroy() { + super.destroy(...arguments); + this.env.pos.destroy(); + } + + catchError(error) { + console.error(error); + } + + get startScreen() { + if (this.state.uiState !== "READY") { + console.warn( + `Accessing startScreen of Chrome component before 'state.uiState' to be 'READY' is not recommended.` + ); + } + return {name: "WelcomeScreen"}; + } + + async start() { + try { + const posModelDefaultAttributes = { + env: this.env, + rpc: this.rpc.bind(this), + session: this.env.session, + do_action: this.props.webClient.do_action.bind( + this.props.webClient + ), + setLoadingMessage: this.setLoadingMessage.bind(this), + showLoadingSkip: this.showLoadingSkip.bind(this), + setLoadingProgress: this.setLoadingProgress.bind(this), + }; + this.env.pos = new models.PosModel(posModelDefaultAttributes); + await this.env.pos.ready; + this._buildChrome(); + this._closeOtherTabs(); + this.env.pos.set( + "selectedCategoryId", + this.env.pos.config.iface_start_categ_id + ? this.env.pos.config.iface_start_categ_id[0] + : 0 + ); + this.state.uiState = "READY"; + this.env.pos.on("change:selectedOrder", this._showSavedScreen, this); + this._showStartScreen(); + if (_.isEmpty(this.env.pos.db.product_by_category_id)) { + this._loadDemoData(); + } + setTimeout(() => { + this.env.pos.push_orders(); + this._preloadImages(); + }); + } catch (error) { + if ( + error.message && + [100, 200, 404, -32098].includes(error.message.code) + ) { + if (error.message.code === -32098) { + title = "Network Failure (XmlHttpRequestError)"; + body = + "The Point of Sale could not be loaded due to a network problem.\n" + + "Please check your internet connection."; + } else if (error.message.code === 200) { + title = + error.message.data.message || this.env._t("Server Error"); + body = + error.message.data.debug || + this.env._t( + "The server encountered an error while receiving your order." + ); + } + } else if (error instanceof Error) { + title = error.message; + body = error.stack; + } + } + } + + _showStartScreen() { + const {name, props} = this.startScreen; + this.showScreen(name, props); + } + + _showSavedScreen(pos, newSelectedOrder) { + const {name, props} = this._getSavedScreen(newSelectedOrder); + this.showScreen(name, props); + } + + _getSavedScreen(order) { + return order.get_screen_data(); + } + + __showTempScreen(event) { + const {name, props, resolve} = event.detail; + this.tempScreen.isShown = true; + this.tempScreen.name = name; + this.tempScreen.component = this.constructor.components[name]; + this.tempScreenProps = Object.assign({}, props, {resolve}); + } + + __closeTempScreen() { + this.tempScreen.isShown = false; + } + + __showScreen({detail: {name, props = {}}}) { + const component = this.constructor.components[name]; + // 1. Set the information of the screen to display. + this.mainScreen.name = name; + this.mainScreen.component = component; + this.mainScreenProps = props; + } + + _setScreenData(name, props) { + const order = this.env.pos.get_order(); + if (order) { + order.set_screen_data({name, props}); + } + } + + async _closePos() { + if (!this.env.pos || this.env.pos.db.get_orders().length === 0) { + window.location = "/web#action=point_of_sale.action_client_pos_menu"; + } + + if (this.env.pos.db.get_orders().length) { + try { + await this.env.pos.push_orders(); + window.location = + "/web#action=point_of_sale.action_client_pos_menu"; + } catch (error) { + console.warn(error); + const reason = this.env.pos.get("failed") + ? this.env._t( + "Some orders could not be submitted to " + + "the server due to configuration errors. " + + "You can exit the Point of Sale, but do " + + "not close the session before the issue " + + "has been resolved." + ) + : this.env._t( + "Some orders could not be submitted to " + + "the server due to internet connection issues. " + + "You can exit the Point of Sale, but do " + + "not close the session before the issue " + + "has been resolved." + ); + const {confirmed} = await this.showPopup("ConfirmPopup", { + title: this.env._t("Offline Orders"), + body: reason, + }); + if (confirmed) { + this.state.uiState = "CLOSING"; + this.loading.skipButtonIsShown = false; + this.setLoadingMessage(this.env._t("Closing ...")); + window.location = + "/web#action=point_of_sale.action_client_pos_menu"; + } + } + } + } + + _toggleDebugWidget() { + this.state.debugWidgetIsShown = !this.state.debugWidgetIsShown; + } + + _onSetSyncStatus({detail: {status, pending}}) { + this.env.pos.set("synch", {status, pending}); + } + + setLoadingProgress(fac) { + if (this.progressbar.el) { + this.progressbar.el.style.width = `${Math.floor(fac * 100)}%`; + } + } + + setLoadingMessage(msg, progress) { + this.loading.message = msg; + if (typeof progress !== "undefined") { + this.setLoadingProgress(progress); + } + } + + showLoadingSkip(callback) { + if (callback) { + this.loading.skipButtonIsShown = true; + this._loadingSkipCallback = callback; + } + } + + async _loadDemoData() { + const {confirmed} = await this.showPopup("ConfirmPopup", { + title: this.env._t("You do not have any products"), + body: this.env._t("Would you like to load demo data?"), + }); + if (confirmed) { + await this.rpc({ + route: "/pos/load_onboarding_data", + }); + this.env.pos.load_server_data(); + } + } + + _preloadImages() { + for (const product of this.env.pos.db.get_product_by_category(0)) { + const image = new Image(); + image.src = `/web/image?model=product.product&field=image_128&id=${product.id}&write_date=${product.write_date}&unique=1`; + } + for (const category of Object.values(this.env.pos.db.category_by_id)) { + if (category.id == 0) continue; + const image = new Image(); + image.src = `/web/image?model=pos.category&field=image_128&id=${category.id}&write_date=${category.write_date}&unique=1`; + } + const staticImages = ["backspace.png", "bc-arrow-big.png"]; + for (const imageName of staticImages) { + const image = new Image(); + image.src = `/point_of_sale/static/src/img/${imageName}`; + } + } + + _buildChrome() { + if ($.browser.chrome) { + var chrome_version = $.browser.version.split(".")[0]; + if (parseInt(chrome_version, 10) >= 50) { + loadCSS("/point_of_sale/static/src/css/chrome50.css"); + } + } + + if (this.env.pos.config.iface_big_scrollbars) { + this.state.hasBigScrollBars = true; + } + + this._disableBackspaceBack(); + this._replaceCrashmanager(); + } + + // Replaces the error handling of the existing crashmanager which + // uses jquery dialog to display the error, to use the pos popup + // instead + _replaceCrashmanager() { + var self = this; + CrashManager.include({ + show_warning: function (error) { + if (self.env.pos) { + // Self == this component + self.showPopup("ErrorPopup", { + title: error.data.title.toString(), + body: error.data.message, + }); + } else { + // This == CrashManager instance + this._super(error); + } + }, + show_error: function (error) { + if (self.env.pos) { + // Self == this component + self.showPopup("ErrorPopup", { + title: error.type, + body: error.message + "\n" + error.data.debug + "\n", + }); + } else { + // This == CrashManager instance + this._super(error); + } + }, + }); + } + + _disableBackspaceBack() { + $(document).on("keydown", function (e) { + if (e.which === 8 && !$(e.target).is("input, textarea")) { + e.preventDefault(); + } + }); + } + + _closeOtherTabs() { + localStorage.message = ""; + localStorage.message = JSON.stringify({ + message: "close_tabs", + session: this.env.pos.pos_session.id, + }); + + window.addEventListener( + "storage", + (event) => { + if (event.key === "message" && event.newValue) { + const msg = JSON.parse(event.newValue); + if ( + msg.message === "close_tabs" && + msg.session == this.env.pos.pos_session.id + ) { + console.info( + "POS / Session opened in another window. EXITING POS" + ); + this._closePos(); + } + } + }, + false + ); + } + } + + ChromeKiosk.template = "ChromeKiosk"; + + Registries.Component.add(ChromeKiosk); + + return ChromeKiosk; +}); diff --git a/pos_kiosk/static/src/js/Components/KioskCartFooter.js b/pos_kiosk/static/src/js/Components/KioskCartFooter.js new file mode 100644 index 0000000000..b0f83fe7fb --- /dev/null +++ b/pos_kiosk/static/src/js/Components/KioskCartFooter.js @@ -0,0 +1,50 @@ +odoo.define("pos_kiosk.KioskCartFooter", function (require) { + "use strict"; + + const PosComponent = require("point_of_sale.PosComponent"); + const Registries = require("point_of_sale.Registries"); + + class KioskCartFooter extends PosComponent { + getTotalItems() { + const orderlines = this.env.pos.get_order().orderlines; + const count = orderlines.reduce( + (total, orderline) => total + orderline.quantity, + 0 + ); + return count; + } + + getTotalValue() { + const orderlines = this.env.pos.get_order().orderlines; + const totalValue = orderlines.reduce( + (total, orderline) => total + orderline.get_price_with_tax(), + 0 + ); + return this.env.pos.format_currency(totalValue); + } + + haveProduct() { + return true ? this.env.pos.get_order().orderlines.length > 0 : false; + } + + openCart() { + this.env.pos.get_order().screen_data = {current: "KioskProductScreen"}; + this.showPopup("CartModal", { + order: this.env.pos.get_order(), + }); + } + + openPaymentScreen() { + if (this.haveProduct()) { + this.env.pos.get_order().screen_data = {current: "KioskPaymentScreen"}; + this.showScreen("KioskPaymentScreen"); + } + } + } + + KioskCartFooter.template = "KioskCartFooter"; + + Registries.Component.add(KioskCartFooter); + + return KioskCartFooter; +}); diff --git a/pos_kiosk/static/src/js/Components/KioskHeader.js b/pos_kiosk/static/src/js/Components/KioskHeader.js new file mode 100644 index 0000000000..8aaa46219e --- /dev/null +++ b/pos_kiosk/static/src/js/Components/KioskHeader.js @@ -0,0 +1,39 @@ +odoo.define("pos_kiosk.KioskHeader", function (require) { + "use strict"; + + const PosComponent = require("point_of_sale.PosComponent"); + const Registries = require("point_of_sale.Registries"); + + class KioskHeader extends PosComponent { + syncOrders() { + this.env.pos.push_orders(null, {show_error: true}); + this.render(); + } + + get topBannerLogo() { + const {id, write_date} = this.env.pos.config; + return `/web/image?model=pos.config&field=top_banner_image&id=${id}&write_date=${write_date}&unique=1`; + } + + get blockedOrders() { + if (this.env.pos.db.cache.orders) { + return this.env.pos.db.cache.orders.length > 0; + } + } + + get blockedOrdersNumber() { + return this.env.pos.db.cache.orders.length; + } + + get logoHeaderURL() { + const {id, write_date} = this.env.pos.config; + return `/web/image?model=pos.config&field=logo_image&id=${id}&write_date=${write_date}&unique=1`; + } + } + + KioskHeader.template = "KioskHeader"; + + Registries.Component.add(KioskHeader); + + return KioskHeader; +}); diff --git a/pos_kiosk/static/src/js/Modals/CartModal.js b/pos_kiosk/static/src/js/Modals/CartModal.js new file mode 100644 index 0000000000..e687bca8fa --- /dev/null +++ b/pos_kiosk/static/src/js/Modals/CartModal.js @@ -0,0 +1,93 @@ +odoo.define("pos_kiosk.CartModal", function (require) { + "use strict"; + + const AbstractAwaitablePopup = require("point_of_sale.AbstractAwaitablePopup"); + const Registries = require("point_of_sale.Registries"); + + // Formerly ConfirmPopupWidget + class CartModal extends AbstractAwaitablePopup { + constructor() { + super(...arguments); + this.order = this.props.order; + this.orderlines = this.order.orderlines.models; + } + + getProductPrice(product) { + return this.order.pos.format_currency(product.lst_price); + } + + getTotalValue() { + return this.order.pos.format_currency(this.order.get_total_with_tax()); + } + + getTotalItens() { + let count = 0; + for (let i = 0; i < this.orderlines.length; i++) { + const element = this.orderlines[i]; + count += element.quantity; + } + return count; + } + + removeQuantity(orderline) { + if (orderline.quantity > 1) { + orderline.set_quantity(orderline.quantity - 1); + } else { + this.order.remove_orderline(orderline); + } + + if (this.order.paymentlines.length > 0) { + this.order.paymentlines.models[0].set_amount( + this.order.get_total_with_tax() + ); + } + + if (this.order.orderlines.length === 0) { + this.trigger("close-popup"); + } + this.render(); + } + + addQuantity(orderline) { + orderline.set_quantity(orderline.quantity + 1); + + if (this.order.paymentlines.length > 0) { + this.order.paymentlines.models[0].set_amount( + this.order.get_total_with_tax() + ); + } + + this.render(); + } + + openPaymentScreen() { + this.trigger("close-popup"); + this.order.screen_data = {current: "KioskPaymentScreen"}; + this.showScreen("KioskPaymentScreen"); + } + + checkCurrentScreen() { + return !( + this.env.pos.get_order().screen_data.current === "KioskPaymentScreen" + ); + } + + productImageURL(product) { + return `/web/image?model=product.product&field=image_128&id=${product.id}&write_date=${product.write_date}&unique=1`; + } + + productName(orderline) { + let full_name = orderline.product.display_name; + if (this.description) { + full_name += ` (${this.description})`; + } + return full_name; + } + } + CartModal.template = "CartModal"; + CartModal.defaultProps = {}; + + Registries.Component.add(CartModal); + + return CartModal; +}); diff --git a/pos_kiosk/static/src/js/Modals/ErrorModal.js b/pos_kiosk/static/src/js/Modals/ErrorModal.js new file mode 100644 index 0000000000..4cddb93257 --- /dev/null +++ b/pos_kiosk/static/src/js/Modals/ErrorModal.js @@ -0,0 +1,19 @@ +odoo.define("point_of_sale.ErrorModal", function (require) { + "use strict"; + + const AbstractAwaitablePopup = require("point_of_sale.AbstractAwaitablePopup"); + const Registries = require("point_of_sale.Registries"); + + class ErrorPopup extends AbstractAwaitablePopup {} + ErrorPopup.template = "ErrorModal"; + ErrorPopup.defaultProps = { + confirmText: "Ok", + cancelText: "Cancel", + title: "Error", + body: "", + }; + + Registries.Component.add(ErrorPopup); + + return ErrorPopup; +}); diff --git a/pos_kiosk/static/src/js/Modals/InsertProductConfigurableModal.js b/pos_kiosk/static/src/js/Modals/InsertProductConfigurableModal.js new file mode 100644 index 0000000000..05610cc9a4 --- /dev/null +++ b/pos_kiosk/static/src/js/Modals/InsertProductConfigurableModal.js @@ -0,0 +1,93 @@ +odoo.define("pos_kiosk.InsertProductConfigurableModal", function (require) { + "use strict"; + + const AbstractAwaitablePopup = require("point_of_sale.AbstractAwaitablePopup"); + const Registries = require("point_of_sale.Registries"); + const {useState} = owl.hooks; + + class InsertProductConfigurableModal extends AbstractAwaitablePopup { + constructor() { + super(...arguments); + this.product = this.props.product; + this.attributes = this.props.attributes; + this.state = { + selected_value: 0, + }; + } + + async confirm() { + if (this.checkSelectedAttribute()) { + super.confirm(); + } + } + + checkSelectedAttribute() { + var selected = false; + this.attributes.forEach((attribute) => { + attribute.values.forEach((value) => { + if (value.id === parseFloat(this.state.selected_value)) { + selected = true; + } + }); + }); + return selected; + } + + getPayload() { + var selected_attributes = []; + var price_extra = 0.0; + + this.attributes.forEach((attribute) => { + attribute.values.forEach((value) => { + if (value.id === parseFloat(this.state.selected_value)) { + selected_attributes.push(value.name); + price_extra += value.price_extra; + } + }); + }); + + return { + productQuantity: this.props.productQuantity, + selectedAttributes: selected_attributes, + priceExtra: price_extra, + }; + } + + get productQuantity() { + return this.props.productQuantity; + } + + addQuantity() { + this.props.productQuantity++; + this.render(); + } + + removeQuantity() { + if (this.props.productQuantity > 1) { + this.props.productQuantity--; + this.render(); + } + } + + get productName() { + return this.product.display_name; + } + + get productPrice() { + return this.env.pos.format_currency(this.product.lst_price); + } + + get productImageURL() { + const product = this.props.product; + return `/web/image?model=product.product&field=image_1920&id=${product.id}&write_date=${product.write_date}&unique=1`; + } + } + InsertProductConfigurableModal.template = "InsertProductConfigurableModal"; + InsertProductConfigurableModal.defaultProps = { + productQuantity: 1, + }; + + Registries.Component.add(InsertProductConfigurableModal); + + return InsertProductConfigurableModal; +}); diff --git a/pos_kiosk/static/src/js/Modals/InsertProductModal.js b/pos_kiosk/static/src/js/Modals/InsertProductModal.js new file mode 100644 index 0000000000..49aa80370d --- /dev/null +++ b/pos_kiosk/static/src/js/Modals/InsertProductModal.js @@ -0,0 +1,61 @@ +odoo.define("pos_kiosk.InsertProductModal", function (require) { + "use strict"; + + const AbstractAwaitablePopup = require("point_of_sale.AbstractAwaitablePopup"); + const Registries = require("point_of_sale.Registries"); + + // Formerly ConfirmPopupWidget + class InsertProductModal extends AbstractAwaitablePopup { + constructor() { + super(...arguments); + this.product = this.props.product; + } + + get productName() { + return this.product.display_name; + } + + get productPrice() { + return this.env.pos.format_currency(this.product.lst_price); + } + + get productQuantity() { + return this.props.productQuantity; + } + + get productTotalPrice() { + return this.env.pos.format_currency( + this.product.lst_price * this.productQuantity + ); + } + + get productImageURL() { + const product = this.props.product; + return `/web/image?model=product.product&field=image_1920&id=${product.id}&write_date=${product.write_date}&unique=1`; + } + + addQuantity() { + this.props.productQuantity++; + this.render(); + } + + removeQuantity() { + if (this.props.productQuantity > 1) { + this.props.productQuantity--; + this.render(); + } + } + + async getPayload() { + return {productQuantity: this.props.productQuantity}; + } + } + InsertProductModal.template = "InsertProductModal"; + InsertProductModal.defaultProps = { + productQuantity: 1, + }; + + Registries.Component.add(InsertProductModal); + + return InsertProductModal; +}); diff --git a/pos_kiosk/static/src/js/Screens/KioskClientScreen.js b/pos_kiosk/static/src/js/Screens/KioskClientScreen.js new file mode 100644 index 0000000000..77150695ec --- /dev/null +++ b/pos_kiosk/static/src/js/Screens/KioskClientScreen.js @@ -0,0 +1,67 @@ +odoo.define("pos_kiosk.KioskClientScreen", function (require) { + "use strict"; + const PosComponent = require("point_of_sale.PosComponent"); + const Registries = require("point_of_sale.Registries"); + + class KioskClientScreen extends PosComponent { + constructor() { + super(...arguments); + this.order = this.env.pos.get_order(); + } + + get currentOrder() { + return this.env.pos.get_order(); + } + + async finalizeOrder() { + const currentOrder = this.order; + + if (!currentOrder.client_name) { + return; + } + + currentOrder.initialize_validation_date(); + currentOrder.finalized = true; + + // eslint-disable-next-line no-unused-vars + let syncedOrderBackendIds = []; + + try { + syncedOrderBackendIds = await this.env.pos.push_single_order( + currentOrder + ); + } catch (error) { + if (error instanceof Error) { + throw error; + } else { + throw new Error(error.message); + } + } + + this.showScreen("KioskReceiptScreen"); + } + + changeName(event) { + this.currentOrder.client_name = event.target.value; + this.render(); + } + + changeVat(event) { + this.currentOrder.vat = event.target.value.replace(/[^\w\s]/g, ""); + } + + backScreen() { + this.showScreen("KioskPaymentScreen"); + } + + hasClientName() { + return this.currentOrder.client_name !== ""; + } + } + + KioskClientScreen.template = "KioskClientScreen"; + + Registries.Component.add(KioskClientScreen); + + return KioskClientScreen; +}); diff --git a/pos_kiosk/static/src/js/Screens/KioskPaymentScreen.js b/pos_kiosk/static/src/js/Screens/KioskPaymentScreen.js new file mode 100644 index 0000000000..1316e37639 --- /dev/null +++ b/pos_kiosk/static/src/js/Screens/KioskPaymentScreen.js @@ -0,0 +1,81 @@ +odoo.define("pos_kiosk.KioskPaymentScreen", function (require) { + "use strict"; + + const PosComponent = require("point_of_sale.PosComponent"); + const Registries = require("point_of_sale.Registries"); + + class KioskPaymentScreen extends PosComponent { + constructor() { + super(...arguments); + this.payment_methods = this.env.pos.payment_methods; + this.order = this.env.pos.get_order(); + } + + backScreen() { + this.order.remove_paymentline(this.order.paymentlines.models[0]); + this.showScreen("KioskProductScreen"); + } + + countTotalItens() { + let count = 0; + for (const line of this.order.get_orderlines()) { + count += line.get_quantity(); + } + return count; + } + + getTotalValue() { + let count = 0; + for (let i = 0; i < this.env.pos.get_order().orderlines.length; i++) { + const element = this.env.pos.get_order().orderlines.models[i]; + count += element.get_price_with_tax(); + } + return this.env.pos.format_currency(count); + } + + openCartModal() { + this.showPopup("CartModal", { + order: this.env.pos.get_order(), + }); + } + + addPaymentLine(payment_method) { + if (this.order.paymentlines.length === 0) { + this.order.add_paymentline(payment_method); + } else { + this.order.remove_paymentline(this.order.paymentlines.models[0]); + this.order.add_paymentline(payment_method); + } + this.render(); + } + + hasPaymentline() { + return this.order.paymentlines.length > 0; + } + + async nextScreen() { + if (this.order.paymentlines.length === 0) { + this.showPopup("ErrorPopup", { + title: this.env._t("Payment Error"), + body: this.env._t("Please select a payment method"), + }); + return; + } + + this.showScreen("KioskClientScreen"); + } + + get methodPaymentLine() { + if (!this.order.paymentlines.length === 0) { + return false; + } + return this.order.paymentlines.models[0].payment_method.id; + } + } + + KioskPaymentScreen.template = "KioskPaymentScreen"; + + Registries.Component.add(KioskPaymentScreen); + + return KioskPaymentScreen; +}); diff --git a/pos_kiosk/static/src/js/Screens/KioskReceiptScreen.js b/pos_kiosk/static/src/js/Screens/KioskReceiptScreen.js new file mode 100644 index 0000000000..8a5e87d864 --- /dev/null +++ b/pos_kiosk/static/src/js/Screens/KioskReceiptScreen.js @@ -0,0 +1,67 @@ +odoo.define("pos_kiosk.KioskReceiptScreen", function (require) { + "use strict"; + + const {useRef} = owl.hooks; + const {useErrorHandlers, onChangeOrder} = require("point_of_sale.custom_hooks"); + const Registries = require("point_of_sale.Registries"); + const AbstractReceiptScreen = require("point_of_sale.AbstractReceiptScreen"); + + const KioskReceiptScreen = (AbstractReceiptScreen) => { + class KioskReceiptScreen extends AbstractReceiptScreen { + constructor() { + super(...arguments); + useErrorHandlers(); + onChangeOrder(null, (newOrder) => newOrder && this.render()); + this.orderReceipt = useRef("order-receipt"); + } + + mounted() { + setTimeout(async () => await this.handleAutoPrint(), 0); + } + + get currentOrder() { + return this.env.pos.get_order(); + } + + async handleAutoPrint() { + if (this._shouldAutoPrint()) { + await this.printReceipt(); + } + } + + _shouldAutoPrint() { + return ( + this.env.pos.config.iface_print_auto && !this.currentOrder._printed + ); + } + + async printReceipt() { + const currentOrder = this.currentOrder; + const isPrinted = await this._printReceipt(); + if (isPrinted) { + currentOrder._printed = true; + } + } + + makeOtherOrder() { + this.env.pos.get_order().destroy(); + this.env.pos.set("selectedCategoryId", false); + this.showScreen("KioskProductScreen"); + } + + endedSession() { + this.env.pos.get_order().destroy(); + this.env.pos.set("selectedCategoryId", false); + this.showScreen("WelcomeScreen"); + } + } + + KioskReceiptScreen.template = "KioskReceiptScreen"; + + return KioskReceiptScreen; + }; + + Registries.Component.addByExtending(KioskReceiptScreen, AbstractReceiptScreen); + + return KioskReceiptScreen; +}); diff --git a/pos_kiosk/static/src/js/Screens/ProductScreen/KioskProductCategory.js b/pos_kiosk/static/src/js/Screens/ProductScreen/KioskProductCategory.js new file mode 100644 index 0000000000..a12a0f5de3 --- /dev/null +++ b/pos_kiosk/static/src/js/Screens/ProductScreen/KioskProductCategory.js @@ -0,0 +1,44 @@ +odoo.define("pos_kiosk.KioskProductCategory", function (require) { + "use strict"; + + const PosComponent = require("point_of_sale.PosComponent"); + const Registries = require("point_of_sale.Registries"); + + class KioskProductCategory extends PosComponent { + constructor() { + super(...arguments); + this.categoryID = false; + this.categoriesList = Object.values(this.env.pos.db.category_by_id).filter( + (category) => category.parent_id === false + ); + this.selectedCategoryId = this.env.pos.get("selectedCategoryId"); + } + + mounted() { + this.env.pos.on("change:selectedCategoryId", this.render, this); + } + + willUnmount() { + this.env.pos.off("change:selectedCategoryId", null, this); + } + + set selectedCategoryId(categoryID) { + this.categoryID = categoryID; + } + + get selectedCategoryId() { + return this.env.pos.get("selectedCategoryId"); + } + + getCategoryImage(categoryID) { + const {id, write_date} = this.env.pos.db.get_category_by_id(categoryID); + return `/web/image?model=pos.category&field=image_128&id=${id}&write_date=${write_date}&unique=1`; + } + } + + KioskProductCategory.template = "KioskProductCategory"; + + Registries.Component.add(KioskProductCategory); + + return KioskProductCategory; +}); diff --git a/pos_kiosk/static/src/js/Screens/ProductScreen/KioskProductScreen.js b/pos_kiosk/static/src/js/Screens/ProductScreen/KioskProductScreen.js new file mode 100644 index 0000000000..0dd0396793 --- /dev/null +++ b/pos_kiosk/static/src/js/Screens/ProductScreen/KioskProductScreen.js @@ -0,0 +1,167 @@ +odoo.define("pos_kiosk.KioskProductScreen", function (require) { + "use strict"; + + const PosComponent = require("point_of_sale.PosComponent"); + const Registries = require("point_of_sale.Registries"); + const {useListener} = require("web.custom_hooks"); + + class KioskProductScreen extends PosComponent { + constructor() { + super(...arguments); + useListener("switch-category", this.setCategory); + this.env.pos.set("selectedCategoryId", false); + this.categoriesList = Object.values(this.env.pos.db.category_by_id).filter( + (category) => category.parent_id === false + ); + this.mainProductList = Object.values(this.env.pos.db.product_by_id); + this.subCategoriesList = []; + } + + setCategory(event) { + const categoryID = event.detail; + this.env.pos.set("selectedCategoryId", categoryID); + + const category = this.env.pos.db.get_category_by_id(categoryID); + if (!category) return; + + const subCategories = Object.values(this.env.pos.db.category_by_id).filter( + (element) => { + return element.parent_id && element.parent_id[0] === categoryID; + } + ); + + this.subCategoriesList = subCategories.map((element) => { + const productList = this.mainProductList.filter( + (product) => product.pos_categ_id[0] === element.id + ); + return { + id: element.id, + name: element.name, + productList: productList, + }; + }); + + this.render(); + } + + getCategoryName() { + const categoryID = this.env.pos.get("selectedCategoryId"); + if (!categoryID) return; + const category = this.env.pos.db.get_category_by_id(categoryID); + return category.name; + } + + getProductName(product_id) { + const product = this.subCategoriesList.find((element) => + element.productList.some((product) => product.id === product_id) + ); + + return product + ? product.productList.find((product) => product.id === product_id) + .display_name + : null; + } + + getProductPrice(product_id) { + const product = this.subCategoriesList.find((element) => + element.productList.some((product) => product.id === product_id) + ); + + if (product) { + const targetProduct = product.productList.find( + (product) => product.id === product_id + ); + return this.env.pos.format_currency(targetProduct.lst_price); + } + + return null; + } + + productImageURL(product_id) { + const {id, write_date} = this.env.pos.db.product_by_id[product_id]; + return `/web/image?model=product.product&field=image_1920&id=${id}&write_date=${write_date}&unique=1`; + } + + async addProduct(product_id) { + const product = this.env.pos.db.product_by_id[product_id]; + const options = await this.getOptions(product); + + if (!options.confirm) return; + + this.env.pos.get_order().add_product(product, options.payload); + this.render(); + } + + async getOptions(product) { + let confirm = false; + let payload = {}; + + if ( + this.env.pos.config.product_configurator && + this.hasAttributes(product) + ) { + const { + confirmed, + payload: attrPayload, + } = await this.showConfigurableProductPopup(product); + confirm = confirmed; + payload = attrPayload; + } else { + const { + confirmed, + payload: prodPayload, + } = await this.showRegularProductPopup(product); + confirm = confirmed; + payload = prodPayload; + } + + if (!confirm) return {confirm}; + + const options = { + quantity: payload.productQuantity || 1, + price_extra: payload.priceExtra || 0.0, + description: payload.selectedAttributes + ? payload.selectedAttributes.join(", ") + : "", + }; + + return {confirm, payload: options}; + } + + hasAttributes(product) { + return product.attribute_line_ids.some( + (id) => id in this.env.pos.attributes_by_ptal_id + ); + } + + async showConfigurableProductPopup(product) { + const attributes = this.getAttributeList(product); + return this.showPopup("InsertProductConfigurableModal", { + product, + attributes, + }); + } + + async showRegularProductPopup(product) { + return this.showPopup("InsertProductModal", { + product, + }); + } + + getAttributeList(product) { + return product.attribute_line_ids + .map((id) => this.env.pos.attributes_by_ptal_id[id]) + .filter((attr) => attr !== undefined); + } + + haveProduct() { + return true ? this.env.pos.get_order().orderlines.length > 0 : false; + } + } + + KioskProductScreen.template = "KioskProductScreen"; + + Registries.Component.add(KioskProductScreen); + + return KioskProductScreen; +}); diff --git a/pos_kiosk/static/src/js/Screens/WelcomeScreen.js b/pos_kiosk/static/src/js/Screens/WelcomeScreen.js new file mode 100644 index 0000000000..56945b24cb --- /dev/null +++ b/pos_kiosk/static/src/js/Screens/WelcomeScreen.js @@ -0,0 +1,22 @@ +odoo.define("pos_kiosk.WelcomeScreen", function (require) { + "use strict"; + const PosComponent = require("point_of_sale.PosComponent"); + const Registries = require("point_of_sale.Registries"); + + class WelcomeScreen extends PosComponent { + navigateToProductScreen() { + this.showScreen("KioskProductScreen"); + } + + get bannerURL() { + const {id, write_date} = this.env.pos.config; + return `/web/image?model=pos.config&field=banner_image&id=${id}&write_date=${write_date}&unique=1`; + } + } + + WelcomeScreen.template = "WelcomeScreen"; + + Registries.Component.add(WelcomeScreen); + + return WelcomeScreen; +}); diff --git a/pos_kiosk/static/src/js/main.js b/pos_kiosk/static/src/js/main.js new file mode 100644 index 0000000000..705505924b --- /dev/null +++ b/pos_kiosk/static/src/js/main.js @@ -0,0 +1,49 @@ +odoo.define("web.web_client", function (require) { + "use strict"; + + const AbstractService = require("web.AbstractService"); + const env = require("web.env"); + const WebClient = require("web.AbstractWebClient"); + const ChromeKiosk = require("pos_kiosk.ChromeKiosk"); + const Registries = require("point_of_sale.Registries"); + const {configureGui} = require("point_of_sale.Gui"); + + owl.config.mode = env.isDebug() ? "dev" : "prod"; + owl.Component.env = env; + + Registries.Component.add(owl.misc.Portal); + + function setupResponsivePlugin(env) { + const isMobile = () => window.innerWidth <= 768; + env.isMobile = isMobile(); + const updateEnv = owl.utils.debounce(() => { + if (env.isMobile !== isMobile()) { + env.isMobile = !env.isMobile; + env.qweb.forceUpdate(); + } + }, 15); + window.addEventListener("resize", updateEnv); + } + + setupResponsivePlugin(owl.Component.env); + + async function startPosApp(webClient) { + Registries.Component.freeze(); + await env.session.is_bound; + env.qweb.addTemplates(env.session.owlTemplates); + env.bus = new owl.core.EventBus(); + await owl.utils.whenReady(); + await webClient.setElement(document.body); + await webClient.start(); + webClient.isStarted = true; + const chrome = new (Registries.Component.get(ChromeKiosk))(null, {webClient}); + await chrome.mount(document.querySelector(".o_action_manager")); + configureGui({component: chrome}); + await chrome.start(); + } + + AbstractService.prototype.deployServices(env); + const webClient = new WebClient(); + startPosApp(webClient); + return webClient; +}); diff --git a/pos_kiosk/static/src/js/models.js b/pos_kiosk/static/src/js/models.js new file mode 100644 index 0000000000..ccc5b72c88 --- /dev/null +++ b/pos_kiosk/static/src/js/models.js @@ -0,0 +1,27 @@ +odoo.define("pos_kiosk.models", function (require) { + "use strict"; + + const models = require("point_of_sale.models"); + + const _super_order = models.Order.prototype; + models.Order = models.Order.extend({ + initialize: function (attributes, options) { + _super_order.initialize.apply(this, arguments); + this.client_name = options.client_name || ""; + this.vat = options.vat || ""; + }, + + init_from_JSON: function (json) { + _super_order.init_from_JSON.apply(this, arguments); + this.client_name = json.client_name; + this.vat = json.vat; + }, + + export_as_JSON: function () { + const json = _super_order.export_as_JSON.apply(this, arguments); + json.client_name = this.client_name; + json.vat = this.vat; + return json; + }, + }); +}); diff --git a/pos_kiosk/static/src/xml/ChromeKiosk.xml b/pos_kiosk/static/src/xml/ChromeKiosk.xml new file mode 100644 index 0000000000..454f77af6e --- /dev/null +++ b/pos_kiosk/static/src/xml/ChromeKiosk.xml @@ -0,0 +1,55 @@ + + + + +
+
+ + + + + + +
+
+

+ +

+
+
+
+ +
+
+ +
+ +
+
+
+ + diff --git a/pos_kiosk/static/src/xml/Components/KioskCartFooter.xml b/pos_kiosk/static/src/xml/Components/KioskCartFooter.xml new file mode 100644 index 0000000000..7d0636876f --- /dev/null +++ b/pos_kiosk/static/src/xml/Components/KioskCartFooter.xml @@ -0,0 +1,41 @@ + + + + +
+
+
+ +
+
Items
+
See Cart Shop
+
+
+
+
+
+
+
Finalize Order
+ +
+
+
+
+ +
diff --git a/pos_kiosk/static/src/xml/Components/KioskHeader.xml b/pos_kiosk/static/src/xml/Components/KioskHeader.xml new file mode 100644 index 0000000000..95fbf80b4a --- /dev/null +++ b/pos_kiosk/static/src/xml/Components/KioskHeader.xml @@ -0,0 +1,67 @@ + + + + +
+ +
+
+ Logo +
+ +
+
+
+ + +
+ Sync orders +
+
+ +
+ Update +
+
+
+ +
+ +
+ +
+ Close +
+
+
+
+
+ +
+ + Banner Logo +
+
+ + + diff --git a/pos_kiosk/static/src/xml/Modals/CartModal.xml b/pos_kiosk/static/src/xml/Modals/CartModal.xml new file mode 100644 index 0000000000..6833f2a2bc --- /dev/null +++ b/pos_kiosk/static/src/xml/Modals/CartModal.xml @@ -0,0 +1,121 @@ + + + + +
+
+
+
+ Your shop cart +
+
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
Items
+
+
+
+
Finalize Order
+ +
+
+
+
+
+ + + diff --git a/pos_kiosk/static/src/xml/Modals/ErrorModal.xml b/pos_kiosk/static/src/xml/Modals/ErrorModal.xml new file mode 100644 index 0000000000..b00e91b499 --- /dev/null +++ b/pos_kiosk/static/src/xml/Modals/ErrorModal.xml @@ -0,0 +1,43 @@ + + + + +
+ +
+ +
+

+ +

+

+ +

+
+
+
+
+
+
+
+
+
+ + + diff --git a/pos_kiosk/static/src/xml/Modals/InsertProductConfigurableModal.xml b/pos_kiosk/static/src/xml/Modals/InsertProductConfigurableModal.xml new file mode 100644 index 0000000000..70a4d9cfc1 --- /dev/null +++ b/pos_kiosk/static/src/xml/Modals/InsertProductConfigurableModal.xml @@ -0,0 +1,152 @@ + + + + +
+ +
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ Choose from the following options +
+
+
+ +
+ +
+ +
+
+
+ + +
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ Add Product +
+
+ +
+
+
+
+
+ + + diff --git a/pos_kiosk/static/src/xml/Modals/InsertProductModal.xml b/pos_kiosk/static/src/xml/Modals/InsertProductModal.xml new file mode 100644 index 0000000000..48d19a7495 --- /dev/null +++ b/pos_kiosk/static/src/xml/Modals/InsertProductModal.xml @@ -0,0 +1,96 @@ + + + + +
+ +
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ Add Product +
+ +
+
+
+
+
+
+ + + diff --git a/pos_kiosk/static/src/xml/Screens/KioskClientScreen.xml b/pos_kiosk/static/src/xml/Screens/KioskClientScreen.xml new file mode 100644 index 0000000000..49be3cd150 --- /dev/null +++ b/pos_kiosk/static/src/xml/Screens/KioskClientScreen.xml @@ -0,0 +1,91 @@ + + + + +
+ + +
+
+
+
+ Who is the order for? +
+
+ Enter your name +
+
+
+ +
+
+
Your name
+ +
+ +
+
+
VAT (optional)
+ +
+
+
+ +
+
+
+ Back +
+
+
+
+
+ Continue +
+
+
+
+
+ +
+
+ +
diff --git a/pos_kiosk/static/src/xml/Screens/KioskPaymentScreen.xml b/pos_kiosk/static/src/xml/Screens/KioskPaymentScreen.xml new file mode 100644 index 0000000000..0bc6996f72 --- /dev/null +++ b/pos_kiosk/static/src/xml/Screens/KioskPaymentScreen.xml @@ -0,0 +1,117 @@ + + + + +
+ + +
+
+
+ End Order +
+
+ Select a payment method +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+ Back +
+
+ +
+
+
+
+ +
+
+ + Items +
+
+ See Cart +
+
+
+
+
+
+ +
+
+
+ End Order +
+ +
+
+
+
+
+
+
+
+ +
diff --git a/pos_kiosk/static/src/xml/Screens/KioskReceiptScreen.xml b/pos_kiosk/static/src/xml/Screens/KioskReceiptScreen.xml new file mode 100644 index 0000000000..11772a8a45 --- /dev/null +++ b/pos_kiosk/static/src/xml/Screens/KioskReceiptScreen.xml @@ -0,0 +1,76 @@ + + + + +
+
+ + + +
+
+
+ Thanks for your order! +
+
+ Come back soon! +
+
+
+
+
+
+
+ Make another order +
+
+
+
+
+ Print receipt +
+
+
+
+ Ended session +
+
+
+
+
+
+ + +
+
+ +
diff --git a/pos_kiosk/static/src/xml/Screens/ProductScreen/KioskProductCategory.xml b/pos_kiosk/static/src/xml/Screens/ProductScreen/KioskProductCategory.xml new file mode 100644 index 0000000000..7070bbdda6 --- /dev/null +++ b/pos_kiosk/static/src/xml/Screens/ProductScreen/KioskProductCategory.xml @@ -0,0 +1,28 @@ + + + + +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
diff --git a/pos_kiosk/static/src/xml/Screens/ProductScreen/KioskProductScreen.xml b/pos_kiosk/static/src/xml/Screens/ProductScreen/KioskProductScreen.xml new file mode 100644 index 0000000000..d7edaa2614 --- /dev/null +++ b/pos_kiosk/static/src/xml/Screens/ProductScreen/KioskProductScreen.xml @@ -0,0 +1,90 @@ + + + + +
+ + +
+ + + + +
+ +
+
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+ + + + +
+
+ +
diff --git a/pos_kiosk/static/src/xml/Screens/WelcomeScreen.xml b/pos_kiosk/static/src/xml/Screens/WelcomeScreen.xml new file mode 100644 index 0000000000..753c2f59eb --- /dev/null +++ b/pos_kiosk/static/src/xml/Screens/WelcomeScreen.xml @@ -0,0 +1,24 @@ + + + + +
+
+ Banner Image +
+
+
+ Start +
+
+
+
+ +
diff --git a/pos_kiosk/views/pos_assets_kiosk_common.xml b/pos_kiosk/views/pos_assets_kiosk_common.xml new file mode 100644 index 0000000000..b2ea3a6ee4 --- /dev/null +++ b/pos_kiosk/views/pos_assets_kiosk_common.xml @@ -0,0 +1,167 @@ + + + +