From a0258e566d8ee1257a8b800d61580b2ed7566104 Mon Sep 17 00:00:00 2001 From: gaikaz Date: Fri, 13 Sep 2024 21:37:46 +0300 Subject: [PATCH 01/15] [CLA] Via laurea: Remove Mantux11 closes odoo/odoo#180235 Signed-off-by: Martin Trigaux (mat) --- doc/cla/corporate/via_laurea.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/cla/corporate/via_laurea.md b/doc/cla/corporate/via_laurea.md index 0adc58d1a8215..602243b5542d1 100644 --- a/doc/cla/corporate/via_laurea.md +++ b/doc/cla/corporate/via_laurea.md @@ -15,5 +15,5 @@ List of contributors: Gailius Kazlauskas gailius.kaz@vialaurea.lt https://github.com/gaikaz Donatas Valiulis donatas@vialaurea.lt https://github.com/DonatasV Domantas Girdžiūnas domantas@vialaurea.lt https://github.com/Du-ma -Mantas Šniukas mantas@vialaurea.lt https://github.com/Mantux11 +Mantas Šniukas mantas@vialaurea.lt (up to 2024-09-13) Saulius Zilys saulius@vialaurea.lt (up to 2017-04-24) From 6b7d5e8169ce54a552f998f034f1b7968fc0f038 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Fri, 13 Sep 2024 16:31:10 +0200 Subject: [PATCH 02/15] [FIX] sale: only create discount product if allowed The issue was recently fixed with a sudo, but it allowed standard salesman/users to create discount products and update the company, which ideally shouldn't happen. Since a single use of the wizard by the admin will properly create the discount product, we prefer to encourage the user to request the admin to do it first instead of bypassing the standard access rights. opw-4048403 closes odoo/odoo#180221 Signed-off-by: Victor Feyens (vfe) --- addons/sale/i18n/sale.pot | 13 ++++++++++++- addons/sale/wizard/sale_order_discount.py | 18 +++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/addons/sale/i18n/sale.pot b/addons/sale/i18n/sale.pot index 8a2e2014a0234..60b980e2aacc2 100644 --- a/addons/sale/i18n/sale.pot +++ b/addons/sale/i18n/sale.pot @@ -844,7 +844,8 @@ msgid "" "e.g. this option can be useful to share Product description files.\n" "Confirmed order: the document will be sent to and accessible by customers.\n" "e.g. this option can be useful to share User Manual or digital content bought on ecommerce. \n" -"Inside quote: The document will be included in the pdf of the quotation between the header pages and the quote table. " +"Inside quote: The document will be included in the pdf of the quotation \n" +"and sale order between the header pages and the quote table. " msgstr "" #. module: sale @@ -4545,6 +4546,16 @@ msgstr "" msgid "There are existing" msgstr "" +#. module: sale +#. odoo-python +#: code:addons/sale/wizard/sale_order_discount.py:0 +#, python-format +msgid "" +"There does not seem to be any discount product configured for this company " +"yet. You can either use a per-line discount, or ask an administrator to " +"grant the discount the first time." +msgstr "" + #. module: sale #: model_terms:ir.ui.view,arch_db:sale.res_config_settings_view_form msgid "" diff --git a/addons/sale/wizard/sale_order_discount.py b/addons/sale/wizard/sale_order_discount.py index 873776ca3bf77..d6213251c256a 100644 --- a/addons/sale/wizard/sale_order_discount.py +++ b/addons/sale/wizard/sale_order_discount.py @@ -68,9 +68,21 @@ def _get_discount_product(self): self.ensure_one() discount_product = self.company_id.sale_discount_product_id if not discount_product: - self.company_id.sudo().sale_discount_product_id = self.env['product.product'].sudo().create( - self._prepare_discount_product_values() - ) + if ( + self.env['product.product'].check_access_rights('create', raise_exception=False) + and self.company_id.check_access_rights('write', raise_exception=False) + and self.company_id._filter_access_rules_python('write') + and self.company_id.check_field_access_rights('write', ['sale_discount_product_id']) + ): + self.company_id.sale_discount_product_id = self.env['product.product'].create( + self._prepare_discount_product_values() + ) + else: + raise ValidationError(_( + "There does not seem to be any discount product configured for this company yet." + " You can either use a per-line discount, or ask an administrator to grant the" + " discount the first time." + )) discount_product = self.company_id.sale_discount_product_id return discount_product From 8f32952daa13b95e2360e9f509630ebb74eb746a Mon Sep 17 00:00:00 2001 From: "Alessandro Caldonazzi (alca)" Date: Tue, 10 Sep 2024 11:29:55 +0000 Subject: [PATCH 03/15] [FIX] website_payment: skip donation test when demo data is unavailable The donation test relies on a complete payment flow using demo data. To avoid failures in environments without demo data, the test is now skipped when demo data is not loaded. This adjustment ensures accurate test results and prevents unnecessary failures in such environments. runbot-76571 closes odoo/odoo#179892 X-original-commit: 8c9098d36c8731fec51e130283b13199ced20b59 Signed-off-by: Antoine Vandevenne (anv) Signed-off-by: Alessandro Caldonazzi (alca) --- addons/website_payment/tests/test_snippets.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/addons/website_payment/tests/test_snippets.py b/addons/website_payment/tests/test_snippets.py index 901d08aa0ea64..b1bf8500c175e 100644 --- a/addons/website_payment/tests/test_snippets.py +++ b/addons/website_payment/tests/test_snippets.py @@ -1,12 +1,22 @@ import odoo import odoo.tests +import logging + +_logger = logging.getLogger(__name__) @odoo.tests.common.tagged('post_install', '-at_install') class TestSnippets(odoo.tests.HttpCase): def test_01_donation(self): + payment_demo = self.env['ir.module.module']._get('payment_demo') + if payment_demo.state != 'installed': + self.skipTest("payment_demo module is not installed") + demo_provider = self.env['payment.provider'].search([('code', '=', "demo")]) demo_provider.write({'state': 'test'}) + if not odoo.tests.loaded_demo_data(self.env): + _logger.warning("This test relies on demo data. To be rewritten independently of demo data for accurate and reliable results.") + return self.start_tour("/?enable_editor=1", "donation_snippet_edition", login='admin') From 9a9063a2d1f385b02f8453b766ec225def531d7e Mon Sep 17 00:00:00 2001 From: "Xavier Bol (xbo)" Date: Fri, 13 Sep 2024 17:18:08 +0200 Subject: [PATCH 04/15] [FIX] project: makes sure the view project is shown in email for user Before this commit, due to 715d6be, the `View Project` button in the email sent even if the receiver is an internal user, which is not really expected. This commit makes sure the button is only hidden for the customer portal when the project is private. closes odoo/odoo#180267 X-original-commit: 715d6be Signed-off-by: Xavier Bol (xbo) --- addons/project/models/project_project.py | 2 +- addons/project/tests/test_project_flow.py | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/addons/project/models/project_project.py b/addons/project/models/project_project.py index 6ef3f3d80bba4..3b753da1b4265 100644 --- a/addons/project/models/project_project.py +++ b/addons/project/models/project_project.py @@ -644,7 +644,7 @@ def _notify_get_recipients_groups(self, message, model_description, msg_vals=Non self.ensure_one() portal_privacy = self.privacy_visibility == 'portal' for group_name, _group_method, group_data in groups: - if group_name in ('customer', 'user') or group_name == 'portal_customer' and not portal_privacy: + if group_name in ['portal', 'portal_customer'] and not portal_privacy: group_data['has_button_access'] = False return groups diff --git a/addons/project/tests/test_project_flow.py b/addons/project/tests/test_project_flow.py index ef44c36815aff..b7b2fa1a365dc 100644 --- a/addons/project/tests/test_project_flow.py +++ b/addons/project/tests/test_project_flow.py @@ -458,13 +458,17 @@ def test_project_notify_get_recipients_groups(self): ]) for project in projects: groups = project._notify_get_recipients_groups(self.env['mail.message'], False) - portal_customer_group = next((g for g in groups if g[0] == 'portal_customer'), False) - if portal_customer_group: - self.assertEqual( - portal_customer_group[2]['has_button_access'], - project.name == 'public project', - "Only the public project should have its name clickable in the email sent to the customer when an email is sent via a email template set in the project stage for instance." - ) + groups_per_key = {g[0]: g for g in groups} + for key, group in groups_per_key.items(): + has_button_access = group[2]['has_button_access'] + if key in ['portal', 'portal_customer']: + self.assertEqual( + has_button_access, + project.name == 'public project', + "Only the public project should have its name clickable in the email sent to the customer when an email is sent via a email template set in the project stage for instance." + ) + elif key == 'user': + self.assertTrue(has_button_access) def test_private_task_search_tag(self): task = self.env['project.task'].create({ From 5f748c9d5731fe2e7e519ee9625da25e2bb219bc Mon Sep 17 00:00:00 2001 From: Louis Travaux Date: Fri, 13 Sep 2024 14:29:19 +0200 Subject: [PATCH 05/15] [FIX] hw_drivers: open chromium without signing in It appears that `chromium-browser` sometimes asks for user to sign in, displaying an unwanted popup instead of the IoT Box homepage. We fix this by adding `--bwsi` argument to the command line. closes odoo/odoo#180190 Signed-off-by: Yaroslav Soroko (yaso) --- addons/hw_drivers/iot_handlers/drivers/DisplayDriver_L.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/hw_drivers/iot_handlers/drivers/DisplayDriver_L.py b/addons/hw_drivers/iot_handlers/drivers/DisplayDriver_L.py index 5606ecec9c960..57c50fea46494 100644 --- a/addons/hw_drivers/iot_handlers/drivers/DisplayDriver_L.py +++ b/addons/hw_drivers/iot_handlers/drivers/DisplayDriver_L.py @@ -83,8 +83,9 @@ def update_url(self, url=None): # Kill browser instance (can't `instance.pkill()` as we can't keep the instance after Odoo service restarts) # We need to terminate it because Odoo will create a new instance each time it is restarted. subprocess.run(['pkill', self.browser.split('-')[0]], check=False) - # --log-level=3 to avoid useless log messages - subprocess.Popen([self.browser, self.url, '--start-fullscreen', '--log-level=3'], env=browser_env) + # --log-level=3 to avoid useless log messages, --bwsi to use chromium without signing in + browser_args = ['--start-fullscreen', '--log-level=3', '--bwsi'] + subprocess.Popen([self.browser, self.url, *browser_args], env=browser_env) # To remove when everyone is on version >= 24.08: chromium has '--start-fullscreen' option if self.browser == 'firefox': From f44a2dd26bfb4dd1e97f376d26e0710bb6f9e40d Mon Sep 17 00:00:00 2001 From: David Monnom Date: Thu, 22 Aug 2024 17:17:21 +0200 Subject: [PATCH 06/15] [REF] hw_posbox_homepage: refactor the homepage to use owl Before this commit, the IOT was using an old system to render pages on the IOT box. This commit refactors the homepage to use the new owl system. Taskid: 4134163 Part-of: odoo/odoo#177895 Signed-off-by: Yaroslav Soroko (yaso) --- addons/hw_posbox_homepage/__manifest__.py | 2 +- .../controllers/__init__.py | 1 + .../controllers/homepage.py | 467 +++++++++++++++++ addons/hw_posbox_homepage/controllers/main.py | 473 +----------------- .../static/src/app/Homepage.js | 144 ++++++ .../src/app/components/FooterButtons.js | 31 ++ .../static/src/app/components/IconButton.js | 22 + .../src/app/components/LoadingFullScreen.js | 41 ++ .../static/src/app/components/SingleData.js | 48 ++ .../app/components/dialog/BootstrapDialog.js | 58 +++ .../app/components/dialog/CredentialDialog.js | 93 ++++ .../src/app/components/dialog/DeviceDialog.js | 52 ++ .../app/components/dialog/HandlerDialog.js | 191 +++++++ .../components/dialog/RemoteDebugDialog.js | 111 ++++ .../src/app/components/dialog/ServerDialog.js | 101 ++++ .../src/app/components/dialog/SixDialog.js | 85 ++++ .../src/app/components/dialog/UpdateDialog.js | 96 ++++ .../src/app/components/dialog/WifiDialog.js | 139 +++++ .../static/src/app/css/override.css | 34 ++ .../static/src/app/hooks/useStore.js | 8 + .../hw_posbox_homepage/static/src/app/main.js | 16 + .../static/src/app/store.js | 33 ++ .../views/configure_wizard.html | 179 ------- .../views/handler_list.html | 128 ----- addons/hw_posbox_homepage/views/homepage.html | 263 ---------- addons/hw_posbox_homepage/views/index.html | 35 ++ addons/hw_posbox_homepage/views/layout.html | 146 ------ .../views/list_credential.html | 68 --- addons/hw_posbox_homepage/views/loading.html | 14 - addons/hw_posbox_homepage/views/logs.html | 24 + .../views/remote_connect.html | 56 --- .../views/server_config.html | 79 --- .../views/six_payment_terminal.html | 61 --- .../views/upgrade_page.html | 95 ---- .../hw_posbox_homepage/views/wifi_config.html | 71 --- 35 files changed, 1833 insertions(+), 1632 deletions(-) create mode 100644 addons/hw_posbox_homepage/controllers/homepage.py create mode 100644 addons/hw_posbox_homepage/static/src/app/Homepage.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/FooterButtons.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/IconButton.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/LoadingFullScreen.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/SingleData.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/dialog/BootstrapDialog.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/dialog/CredentialDialog.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/dialog/DeviceDialog.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/dialog/HandlerDialog.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/dialog/RemoteDebugDialog.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/dialog/ServerDialog.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/dialog/SixDialog.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/dialog/UpdateDialog.js create mode 100644 addons/hw_posbox_homepage/static/src/app/components/dialog/WifiDialog.js create mode 100644 addons/hw_posbox_homepage/static/src/app/css/override.css create mode 100644 addons/hw_posbox_homepage/static/src/app/hooks/useStore.js create mode 100644 addons/hw_posbox_homepage/static/src/app/main.js create mode 100644 addons/hw_posbox_homepage/static/src/app/store.js delete mode 100644 addons/hw_posbox_homepage/views/configure_wizard.html delete mode 100644 addons/hw_posbox_homepage/views/handler_list.html delete mode 100644 addons/hw_posbox_homepage/views/homepage.html create mode 100644 addons/hw_posbox_homepage/views/index.html delete mode 100644 addons/hw_posbox_homepage/views/layout.html delete mode 100644 addons/hw_posbox_homepage/views/list_credential.html delete mode 100644 addons/hw_posbox_homepage/views/loading.html create mode 100644 addons/hw_posbox_homepage/views/logs.html delete mode 100644 addons/hw_posbox_homepage/views/remote_connect.html delete mode 100644 addons/hw_posbox_homepage/views/server_config.html delete mode 100644 addons/hw_posbox_homepage/views/six_payment_terminal.html delete mode 100644 addons/hw_posbox_homepage/views/upgrade_page.html delete mode 100644 addons/hw_posbox_homepage/views/wifi_config.html diff --git a/addons/hw_posbox_homepage/__manifest__.py b/addons/hw_posbox_homepage/__manifest__.py index 5bcef68601504..80840079f9797 100644 --- a/addons/hw_posbox_homepage/__manifest__.py +++ b/addons/hw_posbox_homepage/__manifest__.py @@ -21,7 +21,7 @@ """, 'assets': { 'web.assets_backend': [ - 'hw_posbox_homepage/static/img/*', + 'hw_posbox_homepage/static/*/**', ], }, 'installable': False, diff --git a/addons/hw_posbox_homepage/controllers/__init__.py b/addons/hw_posbox_homepage/controllers/__init__.py index 5d4b25db9c001..e47f2420e7e3b 100644 --- a/addons/hw_posbox_homepage/controllers/__init__.py +++ b/addons/hw_posbox_homepage/controllers/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import main +from . import homepage diff --git a/addons/hw_posbox_homepage/controllers/homepage.py b/addons/hw_posbox_homepage/controllers/homepage.py new file mode 100644 index 0000000000000..fd336d5348ded --- /dev/null +++ b/addons/hw_posbox_homepage/controllers/homepage.py @@ -0,0 +1,467 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json +import subprocess +import threading +import logging +import platform +import jinja2 +import os +import sys + +from pathlib import Path +from odoo import http, tools +from odoo.addons.hw_drivers.tools import helpers +from odoo.addons.hw_drivers.main import iot_devices +from odoo.addons.web.controllers.home import Home +from odoo.addons.hw_drivers.connection_manager import connection_manager +from odoo.tools.misc import file_path +from odoo.addons.hw_drivers.server_logger import ( + check_and_update_odoo_config_log_to_server_option, + get_odoo_config_log_to_server_option, + close_server_log_sender_handler, +) + +_logger = logging.getLogger(__name__) + +IOT_LOGGING_PREFIX = 'iot-logging-' +INTERFACE_PREFIX = 'interface-' +DRIVER_PREFIX = 'driver-' +AVAILABLE_LOG_LEVELS = ('debug', 'info', 'warning', 'error') +AVAILABLE_LOG_LEVELS_WITH_PARENT = AVAILABLE_LOG_LEVELS + ('parent',) + +if hasattr(sys, 'frozen'): + # When running on compiled windows binary, we don't have access to package loader. + path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'views')) + loader = jinja2.FileSystemLoader(path) +else: + loader = jinja2.PackageLoader('odoo.addons.hw_posbox_homepage', "views") + +jinja_env = jinja2.Environment(loader=loader, autoescape=True) +jinja_env.filters["json"] = json.dumps + +index_template = jinja_env.get_template('index.html') +logs_template = jinja_env.get_template('logs.html') + + +class IotBoxOwlHomePage(Home): + def __init__(self): + super().__init__() + self.updating = threading.Lock() + + @http.route() + def index(self): + return index_template.render() + + @http.route("/logs") + def logs_page(self): + return logs_template.render() + + # ---------------------------------------------------------- # + # GET methods # + # -> Always use json.dumps() to return a JSON response # + # ---------------------------------------------------------- # + @http.route('/hw_posbox_homepage/restart_odoo_service', auth='none', type='http', cors='*') + def odoo_service_restart(self): + helpers.odoo_restart(0) + return json.dumps({ + 'status': 'success', + 'message': 'Odoo service restarted', + }) + + @http.route('/hw_posbox_homepage/restart_iotbox', auth='none', type='http', cors='*') + def iotbox_restart(self): + subprocess.call(['sudo', 'reboot']) + return json.dumps({ + 'status': 'success', + 'message': 'IoT Box is restarting', + }) + + @http.route('/hw_posbox_homepage/iot_logs', auth='none', type='http', cors='*') + def get_iot_logs(self): + with open("/var/log/odoo/odoo-server.log", encoding="utf-8") as file: + return json.dumps({ + 'status': 'success', + 'logs': file.read(), + }) + + @http.route('/hw_posbox_homepage/six_payment_terminal_clear', auth='none', type='http', cors='*') + def clear_six_terminal(self): + helpers.update_conf({'six_payment_terminal': ''}) + return json.dumps({ + 'status': 'success', + 'message': 'Successfully cleared Six Payment Terminal', + }) + + @http.route('/hw_posbox_homepage/clear_credential', auth='none', type='http', cors='*') + def clear_credential(self): + helpers.update_conf({ + 'db_uuid': '', + 'enterprise_code': '', + }) + helpers.odoo_restart(0) + return json.dumps({ + 'status': 'success', + 'message': 'Successfully cleared credentials', + }) + + @http.route('/hw_posbox_homepage/wifi_clear', auth='none', type='http', cors='*') + def clear_wifi_configuration(self): + helpers.unlink_file('wifi_network.txt') + return json.dumps({ + 'status': 'success', + 'message': 'Successfully disconnected from wifi', + }) + + @http.route('/hw_posbox_homepage/server_clear', auth='none', type='http', cors='*') + def clear_server_configuration(self): + helpers.disconnect_from_server() + close_server_log_sender_handler() + return json.dumps({ + 'status': 'success', + 'message': 'Successfully disconnected from server', + }) + + @http.route('/hw_posbox_homepage/ping', auth='none', type='http', cors='*') + def ping(self): + return json.dumps({ + 'status': 'success', + 'message': 'pong', + }) + + @http.route('/hw_posbox_homepage/data', auth="none", type="http", cors='*') + def get_homepage_data(self): + if platform.system() == 'Linux': + ssid = helpers.get_ssid() + wired = helpers.read_file_first_line('/sys/class/net/eth0/operstate') + else: + wired = 'up' + if wired == 'up': + network = 'Ethernet' + elif ssid: + if helpers.access_point(): + network = 'Wifi access point' + else: + network = 'Wifi : ' + ssid + else: + network = 'Not Connected' + + is_certificate_ok, certificate_details = helpers.get_certificate_status() + + iot_device = [] + for device in iot_devices: + iot_device.append({ + 'name': iot_devices[device].device_name + ' : ' + str(iot_devices[device].data['value']), + 'type': iot_devices[device].device_type.replace('_', ' '), + 'identifier': iot_devices[device].device_identifier, + }) + + terminal_id = helpers.get_conf('six_payment_terminal') + six_terminal = terminal_id or 'Not Configured' + + return json.dumps({ + 'db_uuid': helpers.get_conf('db_uuid'), + 'enterprise_code': helpers.get_conf('enterprise_code'), + 'hostname': helpers.get_hostname(), + 'ip': helpers.get_ip(), + 'mac': helpers.get_mac_address(), + 'iot_device_status': iot_device, + 'server_status': helpers.get_odoo_server_url() or 'Not Configured', + 'pairing_code': connection_manager.pairing_code, + 'six_terminal': six_terminal, + 'network_status': network, + 'version': helpers.get_version(), + 'system': platform.system(), + 'is_certificate_ok': is_certificate_ok, + 'certificate_details': certificate_details, + }) + + @http.route('/hw_posbox_homepage/wifi', auth="none", type="http", cors='*') + def get_available_wifi(self): + return json.dumps(helpers.get_wifi_essid()) + + @http.route('/hw_posbox_homepage/generate_password', auth="none", type="http", cors='*') + def generate_password(self): + return json.dumps({ + 'password': helpers.generate_password(), + }) + + @http.route('/hw_posbox_homepage/upgrade', auth="none", type="http", cors='*') + def upgrade_iotbox(self): + commit = subprocess.check_output( + ["git", "--work-tree=/home/pi/odoo/", "--git-dir=/home/pi/odoo/.git", "log", "-1"]).decode('utf-8').replace("\n", "
") + flashToVersion = helpers.check_image() + actualVersion = helpers.get_version() + + if flashToVersion: + flashToVersion = '%s.%s' % (flashToVersion.get( + 'major', ''), flashToVersion.get('minor', '')) + + return json.dumps({ + 'title': "Odoo's IoTBox - Software Upgrade", + 'breadcrumb': 'IoT Box Software Upgrade', + 'loading_message': 'Updating IoT box', + 'commit': commit, + 'flashToVersion': flashToVersion, + 'actualVersion': actualVersion, + }) + + @http.route('/hw_posbox_homepage/log_levels', auth="none", type="http", cors='*') + def log_levels(self): + drivers_list = helpers.list_file_by_os( + file_path('hw_drivers/iot_handlers/drivers')) + interfaces_list = helpers.list_file_by_os( + file_path('hw_drivers/iot_handlers/interfaces')) + return json.dumps({ + 'title': "Odoo's IoT Box - Handlers list", + 'breadcrumb': 'Handlers list', + 'drivers_list': drivers_list, + 'interfaces_list': interfaces_list, + 'server': helpers.get_odoo_server_url(), + 'is_log_to_server_activated': get_odoo_config_log_to_server_option(), + 'root_logger_log_level': self._get_logger_effective_level_str(logging.getLogger()), + 'odoo_current_log_level': self._get_logger_effective_level_str(logging.getLogger('odoo')), + 'recommended_log_level': 'warning', + 'available_log_levels': AVAILABLE_LOG_LEVELS, + 'drivers_logger_info': self._get_iot_handlers_logger(drivers_list, 'drivers'), + 'interfaces_logger_info': self._get_iot_handlers_logger(interfaces_list, 'interfaces'), + }) + + @http.route('/hw_posbox_homepage/load_iot_handlers', auth="none", type="http", cors='*') + def load_iot_log_level(self): + helpers.download_iot_handlers(False) + helpers.odoo_restart(0) + return json.dumps({ + 'status': 'success', + 'message': 'IoT Handlers loaded successfully', + }) + + @http.route('/hw_posbox_homepage/clear_iot_handlers', auth="none", type="http", cors='*') + def clear_iot_handlers(self): + for directory in ['drivers', 'interfaces']: + for file in list(Path(file_path(f'hw_drivers/iot_handlers/{directory}')).glob('*')): + if file.name != '__pycache__': + helpers.unlink_file(str(file.relative_to(*file.parts[:3]))) + + return json.dumps({ + 'status': 'success', + 'message': 'IoT Handlers cleared successfully', + }) + + # ---------------------------------------------------------- # + # POST methods # + # -> Never use json.dumps() it will be done automatically # + # ---------------------------------------------------------- # + @http.route('/hw_posbox_homepage/six_payment_terminal_add', auth="none", type="json", methods=['POST'], cors='*') + def add_six_terminal(self, terminal_id): + if terminal_id.isdigit(): + helpers.update_conf({'six_payment_terminal': terminal_id}) + else: + _logger.warning('Ignoring invalid Six TID: "%s". Only digits are allowed', terminal_id) + return self.clear_six_terminal() + return { + 'status': 'success', + 'message': 'Successfully saved Six Payment Terminal', + } + + @http.route('/hw_posbox_homepage/save_credential', auth="none", type="json", methods=['POST'], cors='*') + def save_credential(self, db_uuid, enterprise_code): + helpers.update_conf({ + 'db_uuid': db_uuid, + 'enterprise_code': enterprise_code, + }) + helpers.odoo_restart(0) + return { + 'status': 'success', + 'message': 'Successfully saved credentials', + } + + @http.route('/hw_posbox_homepage/update_wifi', auth="none", type="json", methods=['POST'], cors='*') + def update_wifi(self, essid, password, persistent=False): + persistent = "1" if persistent else "" + subprocess.check_call([file_path( + 'point_of_sale/tools/posbox/configuration/connect_to_wifi.sh'), essid, password, persistent]) + server = helpers.get_odoo_server_url() + + res_payload = { + 'status': 'success', + 'message': 'Connecting to ' + essid, + 'server': { + 'url': server or 'http://' + helpers.get_ip() + ':8069', + 'message': 'Redirect to Odoo Server' if server else 'Redirect to IoT Box' + } + } + + return res_payload + + @http.route('/hw_posbox_homepage/enable_ngrok', auth="none", type="json", methods=['POST'], cors='*') + def enable_remote_connection(self, auth_token): + if subprocess.call(['pgrep', 'ngrok']) == 1: + subprocess.Popen(['ngrok', 'tcp', '--authtoken', auth_token, '--log', '/tmp/ngrok.log', '22']) + + return { + 'status': 'success', + 'auth_token': auth_token, + 'message': 'Ngrok tunnel is now enabled', + } + + @http.route('/hw_posbox_homepage/connect_to_server', auth="none", type="json", methods=['POST'], cors='*') + def connect_to_odoo_server(self, token=False, iotname=False): + if token: + credential = token.split('|') + url = credential[0] + token = credential[1] + db_uuid = credential[2] + enterprise_code = credential[3] + try: + helpers.save_conf_server(url, token, db_uuid, enterprise_code) + except (subprocess.CalledProcessError, OSError, Exception): + return 'Failed to write server configuration files on IoT. Please try again.' + + if iotname and platform.system() == 'Linux' and iotname != helpers.get_hostname(): + subprocess.run([file_path( + 'point_of_sale/tools/posbox/configuration/rename_iot.sh'), iotname], check=False) + + # 1 sec delay for IO operations (save_conf_server) + helpers.odoo_restart(1) + return { + 'status': 'success', + 'message': 'Successfully connected to db, IoT will restart to update the configuration.', + } + + @http.route('/hw_posbox_homepage/log_levels_update', auth="none", type="json", methods=['POST'], cors='*') + def update_log_level(self, name, value): + if not name.startswith(IOT_LOGGING_PREFIX) and name != 'log-to-server': + return { + 'status': 'error', + 'message': 'Invalid logger name', + } + + need_config_save = False + if name == 'log-to-server': + need_config_save |= check_and_update_odoo_config_log_to_server_option( + value + ) + + name = name[len(IOT_LOGGING_PREFIX):] + if name == 'root': + need_config_save |= self._update_logger_level( + '', value, AVAILABLE_LOG_LEVELS) + elif name == 'odoo': + need_config_save |= self._update_logger_level( + 'odoo', value, AVAILABLE_LOG_LEVELS) + need_config_save |= self._update_logger_level( + 'werkzeug', value if value != 'debug' else 'info', AVAILABLE_LOG_LEVELS) + elif name.startswith(INTERFACE_PREFIX): + logger_name = name[len(INTERFACE_PREFIX):] + need_config_save |= self._update_logger_level( + logger_name, value, AVAILABLE_LOG_LEVELS_WITH_PARENT, 'interfaces') + elif name.startswith(DRIVER_PREFIX): + logger_name = name[len(DRIVER_PREFIX):] + need_config_save |= self._update_logger_level( + logger_name, value, AVAILABLE_LOG_LEVELS_WITH_PARENT, 'drivers') + else: + _logger.warning('Unhandled iot logger: %s', name) + + if need_config_save: + with helpers.writable(): + tools.config.save() + + return { + 'status': 'success', + 'message': 'Logger level updated', + } + + # ---------------------------------------------------------- # + # Utils # + # ---------------------------------------------------------- # + def _get_iot_handlers_logger(self, handlers_name, iot_handler_folder_name): + handlers_loggers_level = dict() + for handler_name in handlers_name: + handler_logger = self._get_iot_handler_logger(handler_name, iot_handler_folder_name) + if not handler_logger: + # Might happen if the file didn't define a logger (or not init yet) + handlers_loggers_level[handler_name] = False + _logger.debug('Unable to find logger for handler %s', handler_name) + continue + logger_parent = handler_logger.parent + handlers_loggers_level[handler_name] = { + 'level': self._get_logger_effective_level_str(handler_logger), + 'is_using_parent_level': handler_logger.level == logging.NOTSET, + 'parent_name': logger_parent.name, + 'parent_level': self._get_logger_effective_level_str(logger_parent), + } + return handlers_loggers_level + + def _update_logger_level(self, logger_name, new_level, available_log_levels, handler_folder=False): + """ + Update (if necessary) Odoo's configuration and logger to the given logger_name to the given level. + The responsibility of saving the config file is not managed here. + :param logger_name: name of the logging logger to change level + :param new_level: new log level to set for this logger + :param available_log_levels: iterable of logs levels allowed (for initial check) + :param handler_folder: optional string of the IoT handler folder name ('interfaces' or 'drivers') + :return: wherever some changes were performed or not on the config + """ + if new_level not in available_log_levels: + _logger.warning('Unknown level to set on logger %s: %s', logger_name, new_level) + return False + + if handler_folder: + logger = self._get_iot_handler_logger(logger_name, handler_folder) + if not logger: + _logger.warning('Unable to change log level for logger %s as logger missing', logger_name) + return False + logger_name = logger.name + + ODOO_TOOL_CONFIG_HANDLER_NAME = 'log_handler' + LOG_HANDLERS = tools.config[ODOO_TOOL_CONFIG_HANDLER_NAME] + LOGGER_PREFIX = logger_name + ':' + IS_NEW_LEVEL_PARENT = new_level == 'parent' + + if not IS_NEW_LEVEL_PARENT: + intended_to_find = LOGGER_PREFIX + new_level.upper() + if intended_to_find in LOG_HANDLERS: + # There is nothing to do, the entry is already inside + return False + + # We remove every occurrence for the given logger + log_handlers_without_logger = [ + log_handler for log_handler in LOG_HANDLERS if not log_handler.startswith(LOGGER_PREFIX) + ] + + if IS_NEW_LEVEL_PARENT: + # We must check that there is no existing entries using this logger (whatever the level) + if len(log_handlers_without_logger) == len(LOG_HANDLERS): + return False + + # We add if necessary new logger entry + # If it is "parent" it means we want it to inherit from the parent logger. + # In order to do this we have to make sure that no entries for the logger exists in the + # `log_handler` (which is the case at this point as long as we don't re-add an entry) + tools.config[ODOO_TOOL_CONFIG_HANDLER_NAME] = log_handlers_without_logger + new_level_upper_case = new_level.upper() + if not IS_NEW_LEVEL_PARENT: + new_entry = [LOGGER_PREFIX + new_level_upper_case] + tools.config[ODOO_TOOL_CONFIG_HANDLER_NAME] += new_entry + _logger.debug('Adding to odoo config log_handler: %s', new_entry) + + # Update the logger dynamically + real_new_level = logging.NOTSET if IS_NEW_LEVEL_PARENT else new_level_upper_case + _logger.debug('Change logger %s level to %s', logger_name, real_new_level) + logging.getLogger(logger_name).setLevel(real_new_level) + return True + + def _get_logger_effective_level_str(self, logger): + return logging.getLevelName(logger.getEffectiveLevel()).lower() + + def _get_iot_handler_logger(self, handler_name, handler_folder_name): + """ + Get Odoo Iot logger given an IoT handler name + :param handler_name: name of the IoT handler + :param handler_folder_name: IoT handler folder name (interfaces or drivers) + :return: logger if any, False otherwise + """ + odoo_addon_handler_path = helpers.compute_iot_handlers_addon_name(handler_folder_name, handler_name) + return odoo_addon_handler_path in logging.Logger.manager.loggerDict and \ + logging.getLogger(odoo_addon_handler_path) diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py index 3208c29ecdb95..745e8855810c7 100644 --- a/addons/hw_posbox_homepage/controllers/main.py +++ b/addons/hw_posbox_homepage/controllers/main.py @@ -1,57 +1,18 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import json -import jinja2 -import platform import logging import os -from pathlib import Path import subprocess -import sys import threading -from odoo import http, tools -from odoo.http import Response, request -from odoo.addons.hw_drivers.connection_manager import connection_manager -from odoo.addons.hw_drivers.main import iot_devices +from odoo import http +from odoo.http import Response from odoo.addons.hw_drivers.tools import helpers -from odoo.addons.hw_drivers.server_logger import ( - check_and_update_odoo_config_log_to_server_option, - close_server_log_sender_handler, - get_odoo_config_log_to_server_option, -) from odoo.addons.web.controllers.home import Home -from odoo.tools.misc import file_path _logger = logging.getLogger(__name__) -#---------------------------------------------------------- -# Controllers -#---------------------------------------------------------- - -if hasattr(sys, 'frozen'): - # When running on compiled windows binary, we don't have access to package loader. - path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'views')) - loader = jinja2.FileSystemLoader(path) -else: - loader = jinja2.PackageLoader('odoo.addons.hw_posbox_homepage', "views") - -jinja_env = jinja2.Environment(loader=loader, autoescape=True) -jinja_env.filters["json"] = json.dumps - -homepage_template = jinja_env.get_template('homepage.html') -server_config_template = jinja_env.get_template('server_config.html') -wifi_config_template = jinja_env.get_template('wifi_config.html') -handler_list_template = jinja_env.get_template('handler_list.html') -remote_connect_template = jinja_env.get_template('remote_connect.html') -configure_wizard_template = jinja_env.get_template('configure_wizard.html') -six_payment_terminal_template = jinja_env.get_template('six_payment_terminal.html') -list_credential_template = jinja_env.get_template('list_credential.html') -upgrade_page_template = jinja_env.get_template('upgrade_page.html') - - class IoTboxHomepage(Home): def __init__(self): super(IoTboxHomepage,self).__init__() @@ -60,319 +21,6 @@ def __init__(self): def clean_partition(self): subprocess.check_call(['sudo', 'bash', '-c', '. /home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/upgrade.sh; cleanup']) - def get_six_terminal(self): - terminal_id = helpers.get_conf('six_payment_terminal') - return terminal_id or 'Not Configured' - - def get_homepage_data(self): - if platform.system() == 'Linux': - ssid = helpers.get_ssid() - wired = helpers.read_file_first_line('/sys/class/net/eth0/operstate') - else: - wired = 'up' - if wired == 'up': - network = 'Ethernet' - elif ssid: - if helpers.access_point(): - network = 'Wifi access point' - else: - network = 'Wifi : ' + ssid - else: - network = 'Not Connected' - - is_certificate_ok, certificate_details = helpers.get_certificate_status() - - iot_device = [] - for device in iot_devices: - iot_device.append({ - 'name': iot_devices[device].device_name + ' : ' + str(iot_devices[device].data['value']), - 'type': iot_devices[device].device_type.replace('_', ' '), - 'identifier': iot_devices[device].device_identifier, - }) - - return { - 'hostname': helpers.get_hostname(), - 'ip': helpers.get_ip(), - 'mac': helpers.get_mac_address(), - 'iot_device_status': iot_device, - 'server_status': helpers.get_odoo_server_url() or 'Not Configured', - 'pairing_code': connection_manager.pairing_code, - 'six_terminal': self.get_six_terminal(), - 'network_status': network, - 'version': helpers.get_version(), - 'system': platform.system(), - 'is_certificate_ok': is_certificate_ok, - 'certificate_details': certificate_details, - } - - @http.route() - def index(self): - wifi = helpers.get_conf('wifi_ssid') - remote_server = helpers.get_odoo_server_url() - if (not wifi or not remote_server) and helpers.access_point(): - return "" - else: - return homepage_template.render(self.get_homepage_data()) - - @http.route('/list_handlers', type='http', auth='none', website=True, csrf=False, save_session=False) - def list_handlers(self, **post): - AVAILABLE_LOG_LEVELS = ('debug', 'info', 'warning', 'error') - if request.httprequest.method == 'POST': - need_config_save = False # If the config file needed to be saved at the end - - # Check and update "send logs to server" - need_config_save |= check_and_update_odoo_config_log_to_server_option( - post.get('log-to-server') == 'on' # we use .get() as if the HTML checkbox is unchecked, no value is given in the POST request - ) - - # Check and update logging levels - IOT_LOGGING_PREFIX = 'iot-logging-' - INTERFACE_PREFIX = 'interface-' - DRIVER_PREFIX = 'driver-' - AVAILABLE_LOG_LEVELS_WITH_PARENT = AVAILABLE_LOG_LEVELS + ('parent',) - for post_request_key, log_level_or_parent in post.items(): - if not post_request_key.startswith(IOT_LOGGING_PREFIX): - # probably a new post request payload argument not related to logging - continue - post_request_key = post_request_key[len(IOT_LOGGING_PREFIX):] - - if post_request_key == 'root': - need_config_save |= self._update_logger_level('', log_level_or_parent, AVAILABLE_LOG_LEVELS) - elif post_request_key == 'odoo': - need_config_save |= self._update_logger_level('odoo', log_level_or_parent, AVAILABLE_LOG_LEVELS) - need_config_save |= self._update_logger_level('werkzeug', log_level_or_parent if log_level_or_parent != 'debug' else 'info', AVAILABLE_LOG_LEVELS) - elif post_request_key.startswith(INTERFACE_PREFIX): - logger_name = post_request_key[len(INTERFACE_PREFIX):] - need_config_save |= self._update_logger_level(logger_name, log_level_or_parent, AVAILABLE_LOG_LEVELS_WITH_PARENT, 'interfaces') - elif post_request_key.startswith(DRIVER_PREFIX): - logger_name = post_request_key[len(DRIVER_PREFIX):] - need_config_save |= self._update_logger_level(logger_name, log_level_or_parent, AVAILABLE_LOG_LEVELS_WITH_PARENT, 'drivers') - else: - _logger.warning('Unhandled iot logger: %s', post_request_key) - - # Update and save the config file (in case of IoT box reset) - if need_config_save: - with helpers.writable(): - tools.config.save() - drivers_list = helpers.list_file_by_os(file_path('hw_drivers/iot_handlers/drivers')) - interfaces_list = helpers.list_file_by_os(file_path('hw_drivers/iot_handlers/interfaces')) - return handler_list_template.render({ - 'title': "Odoo's IoT Box - Handlers list", - 'breadcrumb': 'Handlers list', - 'drivers_list': drivers_list, - 'interfaces_list': interfaces_list, - 'server': helpers.get_odoo_server_url(), - 'is_log_to_server_activated': get_odoo_config_log_to_server_option(), - 'root_logger_log_level': self._get_logger_effective_level_str(logging.getLogger()), - 'odoo_current_log_level': self._get_logger_effective_level_str(logging.getLogger('odoo')), - 'recommended_log_level': 'warning', - 'available_log_levels': AVAILABLE_LOG_LEVELS, - 'drivers_logger_info': self._get_iot_handlers_logger(drivers_list, 'drivers'), - 'interfaces_logger_info': self._get_iot_handlers_logger(interfaces_list, 'interfaces'), - }) - - @http.route('/load_iot_handlers', type='http', auth='none', website=True) - def load_iot_handlers(self): - helpers.download_iot_handlers(False) - helpers.odoo_restart(0) - return "" - - @http.route('/list_credential', type='http', auth='none', website=True) - def list_credential(self): - return list_credential_template.render({ - 'title': "Odoo's IoT Box - List credential", - 'breadcrumb': 'List credential', - 'db_uuid': helpers.get_conf('db_uuid'), - 'enterprise_code': helpers.get_conf('enterprise_code'), - }) - - @http.route('/save_credential', type='http', auth='none', cors='*', csrf=False) - def save_credential(self, db_uuid, enterprise_code): - helpers.update_conf({ - 'db_uuid': db_uuid, - 'enterprise_code': enterprise_code, - }) - helpers.odoo_restart(0) - return "" - - @http.route('/clear_credential', type='http', auth='none', cors='*', csrf=False) - def clear_credential(self): - helpers.update_conf({ - 'db_uuid': '', - 'enterprise_code': '', - }) - helpers.odoo_restart(0) - return "" - - @http.route('/wifi', type='http', auth='none', website=True) - def wifi(self): - return wifi_config_template.render({ - 'title': 'Wifi configuration', - 'breadcrumb': 'Configure Wifi', - 'loading_message': 'Connecting to Wifi', - 'ssid': helpers.get_wifi_essid(), - }) - - @http.route('/wifi_connect', type='http', auth='none', cors='*', csrf=False) - def connect_to_wifi(self, essid, password, persistent=False): - persistent = "1" if persistent else "" - - subprocess.check_call([file_path('point_of_sale/tools/posbox/configuration/connect_to_wifi.sh'), essid, password, persistent]) - server = helpers.get_odoo_server_url() - res_payload = { - 'message': 'Connecting to ' + essid, - } - if server: - res_payload['server'] = { - 'url': server, - 'message': 'Redirect to Odoo Server' - } - else: - res_payload['server'] = { - 'url': 'http://' + helpers.get_ip() + ':8069', - 'message': 'Redirect to IoT Box' - } - - return json.dumps(res_payload) - - @http.route('/wifi_clear', type='http', auth='none', cors='*', csrf=False) - def clear_wifi_configuration(self): - helpers.update_conf({'wifi_ssid': '', 'wifi_password': ''}) - return "" - - @http.route('/server_clear', type='http', auth='none', cors='*', csrf=False) - def clear_server_configuration(self): - helpers.disconnect_from_server() - close_server_log_sender_handler() - return "" - - @http.route('/handlers_clear', type='http', auth='none', cors='*', csrf=False) - def clear_handlers_list(self): - for directory in ['drivers', 'interfaces']: - for file in list(Path(file_path(f'hw_drivers/iot_handlers/{directory}')).glob('*')): - if file.name != '__pycache__': - helpers.unlink_file(str(file.relative_to(*file.parts[:3]))) - return "" - - @http.route('/server_connect', type='http', auth='none', cors='*', csrf=False) - def connect_to_server(self, token, iotname): - if token: - credential = token.split('|') - url = credential[0] - token = credential[1] - db_uuid = credential[2] - enterprise_code = credential[3] - try: - helpers.save_conf_server(url, token, db_uuid, enterprise_code) - except (subprocess.CalledProcessError, OSError, Exception): - return 'Failed to write server configuration files on IoT. Please try again.' - - if iotname and platform.system() == 'Linux' and iotname != helpers.get_hostname(): - subprocess.run([file_path('point_of_sale/tools/posbox/configuration/rename_iot.sh'), iotname], check=False) - - helpers.odoo_restart(1) # 1 sec delay for IO operations (save_conf_server) - return 'Successfully connected to db, IoT will restart to update the configuration.' - - @http.route('/steps', type='http', auth='none', cors='*', csrf=False) - def step_by_step_configure_page(self): - return configure_wizard_template.render({ - 'title': 'Configure IoT Box', - 'breadcrumb': 'Configure IoT Box', - 'loading_message': 'Configuring your IoT Box', - 'ssid': helpers.get_wifi_essid(), - 'server': helpers.get_odoo_server_url() or '', - 'hostname': subprocess.check_output('hostname').decode('utf-8').strip('\n'), - }) - - @http.route('/step_configure', type='http', auth='none', cors='*', csrf=False) - def step_by_step_configure(self, token, iotname, essid, password): - url = '' - if token: - url = token.split('|')[0] - token = token.split('|')[1] - - subprocess.check_call([file_path('point_of_sale/tools/posbox/configuration/connect_to_wifi.sh'), essid, password, "1"]) - self.connect_to_server(token, iotname) - return url - - # Set server address - @http.route('/server', type='http', auth='none', website=True) - def server(self): - return server_config_template.render({ - 'title': 'IoT -> Odoo server configuration', - 'breadcrumb': 'Configure Odoo Server', - 'hostname': subprocess.check_output('hostname').decode('utf-8').strip('\n'), - 'server_status': helpers.get_odoo_server_url() or 'Not configured yet', - 'loading_message': 'Configure Domain Server' - }) - - # Get password - @http.route('/hw_posbox_homepage/password', type='json', auth='none', methods=['POST']) - def view_password(self): - return helpers.generate_password() - - @http.route('/remote_connect', type='http', auth='none', cors='*') - def remote_connect(self): - """ - Establish a link with a customer box trough internet with a ssh tunnel - 1 - take a new auth_token on https://dashboard.ngrok.com/ - 2 - copy past this auth_token on the IoT Box : http://IoT_Box:8069/remote_connect - 3 - check on ngrok the port and url to get access to the box - 4 - you can connect to the box with this command : ssh -p port -v pi@url - """ - return remote_connect_template.render({ - 'title': 'Remote debugging', - 'breadcrumb': 'Remote Debugging', - }) - - @http.route('/enable_ngrok', type='http', auth='none', cors='*', csrf=False) - def enable_ngrok(self, auth_token): - if subprocess.call(['pgrep', 'ngrok']) == 1: - subprocess.Popen(['ngrok', 'tcp', '--authtoken', auth_token, '--log', '/tmp/ngrok.log', '22']) - return 'starting with ' + auth_token - else: - return 'already running' - - @http.route('/six_payment_terminal', type='http', auth='none', cors='*', csrf=False) - def six_payment_terminal(self): - return six_payment_terminal_template.render({ - 'title': 'Six Payment Terminal', - 'breadcrumb': 'Six Payment Terminal', - 'terminalId': self.get_six_terminal(), - }) - - @http.route('/six_payment_terminal_add', type='http', auth='none', cors='*', csrf=False) - def add_six_payment_terminal(self, terminal_id): - if terminal_id.isdigit(): - helpers.update_conf({'six_payment_terminal': terminal_id}) - helpers.odoo_restart(0) - else: - _logger.warning('Ignoring invalid Six TID: "%s". Only digits are allowed', terminal_id) - self.clear_six_payment_terminal() - return 'http://' + helpers.get_ip() + ':8069' - - @http.route('/six_payment_terminal_clear', type='http', auth='none', cors='*', csrf=False) - def clear_six_payment_terminal(self): - helpers.update_conf({'six_payment_terminal': ''}) - helpers.odoo_restart(0) - return "" - - @http.route('/hw_proxy/upgrade', type='http', auth='none', ) - def upgrade(self): - commit = subprocess.check_output(["git", "--work-tree=/home/pi/odoo/", "--git-dir=/home/pi/odoo/.git", "log", "-1"]).decode('utf-8').replace("\n", "
") - flashToVersion = helpers.check_image() - actualVersion = helpers.get_version() - if flashToVersion: - flashToVersion = '%s.%s' % (flashToVersion.get('major', ''), flashToVersion.get('minor', '')) - return upgrade_page_template.render({ - 'title': "Odoo's IoTBox - Software Upgrade", - 'breadcrumb': 'IoT Box Software Upgrade', - 'loading_message': 'Updating IoT box', - 'commit': commit, - 'flashToVersion': flashToVersion, - 'actualVersion': actualVersion, - }) - @http.route('/hw_proxy/perform_upgrade', type='http', auth='none') def perform_upgrade(self): self.updating.acquire() @@ -424,120 +72,3 @@ def perform_flashing_copy_raspios(self): self.clean_partition() _logger.error('A error encountered : %s ' % e) return Response(str(e), status=500) - - @http.route('/iot_restart_odoo_or_reboot', type='json', auth='none', cors='*', csrf=False) - def iot_restart_odoo_or_reboot(self, action): - """ Reboots the IoT Box / restarts Odoo on it depending on chosen 'action' argument""" - try: - if action == 'restart_odoo': - helpers.odoo_restart(3) - else: - subprocess.call(['sudo', 'reboot']) - return 'success' - except Exception as e: - _logger.error('An error encountered : %s ', e) - return str(e) - - def _get_logger_effective_level_str(self, logger): - return logging.getLevelName(logger.getEffectiveLevel()).lower() - - def _get_iot_handler_logger(self, handler_name, handler_folder_name): - """ - Get Odoo Iot logger given an IoT handler name - :param handler_name: name of the IoT handler - :param handler_folder_name: IoT handler folder name (interfaces or drivers) - :return: logger if any, False otherwise - """ - odoo_addon_handler_path = helpers.compute_iot_handlers_addon_name(handler_folder_name, handler_name) - return odoo_addon_handler_path in logging.Logger.manager.loggerDict.keys() and \ - logging.getLogger(odoo_addon_handler_path) - - def _update_logger_level(self, logger_name, new_level, available_log_levels, handler_folder=False): - """ - Update (if necessary) Odoo's configuration and logger to the given logger_name to the given level. - The responsibility of saving the config file is not managed here. - :param logger_name: name of the logging logger to change level - :param new_level: new log level to set for this logger - :param available_log_levels: iterable of logs levels allowed (for initial check) - :param handler_folder: optional string of the IoT handler folder name ('interfaces' or 'drivers') - :return: wherever some changes were performed or not on the config - """ - if new_level not in available_log_levels: - _logger.warning('Unknown level to set on logger %s: %s', logger_name, new_level) - return False - - if handler_folder: - logger = self._get_iot_handler_logger(logger_name, handler_folder) - if not logger: - _logger.warning('Unable to change log level for logger %s as logger missing', logger_name) - return False - logger_name = logger.name - - ODOO_TOOL_CONFIG_HANDLER_NAME = 'log_handler' - LOG_HANDLERS = tools.config[ODOO_TOOL_CONFIG_HANDLER_NAME] - LOGGER_PREFIX = logger_name + ':' - IS_NEW_LEVEL_PARENT = new_level == 'parent' - - if not IS_NEW_LEVEL_PARENT: - intended_to_find = LOGGER_PREFIX + new_level.upper() - if intended_to_find in LOG_HANDLERS: - # There is nothing to do, the entry is already inside - return False - - # We remove every occurrence for the given logger - log_handlers_without_logger = [ - log_handler for log_handler in LOG_HANDLERS if not log_handler.startswith(LOGGER_PREFIX) - ] - - if IS_NEW_LEVEL_PARENT: - # We must check that there is no existing entries using this logger (whatever the level) - if len(log_handlers_without_logger) == len(LOG_HANDLERS): - return False - - # We add if necessary new logger entry - # If it is "parent" it means we want it to inherit from the parent logger. - # In order to do this we have to make sure that no entries for the logger exists in the - # `log_handler` (which is the case at this point as long as we don't re-add an entry) - tools.config[ODOO_TOOL_CONFIG_HANDLER_NAME] = log_handlers_without_logger - new_level_upper_case = new_level.upper() - if not IS_NEW_LEVEL_PARENT: - new_entry = [LOGGER_PREFIX + new_level_upper_case] - tools.config[ODOO_TOOL_CONFIG_HANDLER_NAME] += new_entry - _logger.debug('Adding to odoo config log_handler: %s', new_entry) - - # Update the logger dynamically - real_new_level = logging.NOTSET if IS_NEW_LEVEL_PARENT else new_level_upper_case - _logger.debug('Change logger %s level to %s', logger_name, real_new_level) - logging.getLogger(logger_name).setLevel(real_new_level) - return True - - def _get_iot_handlers_logger(self, handlers_name, iot_handler_folder_name): - """ - :param handlers_name: List of IoT handler string to search the loggers of - :param iot_handler_folder_name: name of the handler folder ('interfaces' or 'drivers') - :return: - { - : { - 'level': , - 'is_using_parent_level': , - 'parent_name': , - }, - ... - } - """ - handlers_loggers_level = dict() - for handler_name in handlers_name: - handler_logger = self._get_iot_handler_logger(handler_name, iot_handler_folder_name) - if not handler_logger: - # Might happen if the file didn't define a logger (or not init yet) - handlers_loggers_level[handler_name] = False - _logger.debug('Unable to find logger for handler %s', handler_name) - continue - logger_parent = handler_logger.parent - handlers_loggers_level[handler_name] = { - 'level': self._get_logger_effective_level_str(handler_logger), - 'is_using_parent_level': handler_logger.level == logging.NOTSET, - 'parent_name': logger_parent.name, - 'parent_level': self._get_logger_effective_level_str(logger_parent), - } - return handlers_loggers_level diff --git a/addons/hw_posbox_homepage/static/src/app/Homepage.js b/addons/hw_posbox_homepage/static/src/app/Homepage.js new file mode 100644 index 0000000000000..c902b3523aa91 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/Homepage.js @@ -0,0 +1,144 @@ +/* global owl */ + +import { SingleData } from "./components/SingleData.js"; +import { FooterButtons } from "./components/FooterButtons.js"; +import { ServerDialog } from "./components/dialog/ServerDialog.js"; +import { WifiDialog } from "./components/dialog/WifiDialog.js"; +import useStore from "./hooks/useStore.js"; +import { UpdateDialog } from "./components/dialog/UpdateDialog.js"; +import { DeviceDialog } from "./components/dialog/DeviceDialog.js"; +import { SixDialog } from "./components/dialog/SixDialog.js"; +import { LoadingFullScreen } from "./components/LoadingFullScreen.js"; +import { IconButton } from "./components/IconButton.js"; + +const { Component, xml, useState, onWillStart } = owl; + +export class Homepage extends Component { + static props = {}; + static components = { + SingleData, + FooterButtons, + ServerDialog, + WifiDialog, + UpdateDialog, + DeviceDialog, + SixDialog, + LoadingFullScreen, + IconButton, + }; + + setup() { + this.store = useStore(); + this.state = useState({ loading: true, waitRestart: false }); + this.data = useState({}); + this.store.advanced = localStorage.getItem("showAdvanced") === "true"; + + onWillStart(async () => { + await this.loadInitialData(); + }); + + setInterval(() => { + this.loadInitialData(); + }, 10000); + } + + async loadInitialData() { + try { + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/data", + }); + this.data = data; + this.store.base = data; + this.state.loading = false; + this.store.update = new Date().getTime(); + } catch { + console.warn("Error while fetching data"); + } + } + + async restartOdooService() { + try { + await this.store.rpc({ + url: "/hw_posbox_homepage/restart_odoo_service", + }); + + this.state.waitRestart = true; + } catch { + console.warn("Error while restarting Odoo Service"); + } + } + + toggleAdvanced() { + this.store.advanced = !this.store.advanced; + localStorage.setItem("showAdvanced", this.store.advanced); + } + + static template = xml` + + + Restarting IoT Box, please wait... + + + +
+
+
+ + +
+
+

IoT Box -

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+ Loading... +
+
+ `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/components/FooterButtons.js b/addons/hw_posbox_homepage/static/src/app/components/FooterButtons.js new file mode 100644 index 0000000000000..6acdd4c880013 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/components/FooterButtons.js @@ -0,0 +1,31 @@ +/* global owl */ + +import useStore from "../hooks/useStore.js"; +import { CredentialDialog } from "./dialog/CredentialDialog.js"; +import { HandlerDialog } from "./dialog/HandlerDialog.js"; +import { RemoteDebugDialog } from "./dialog/RemoteDebugDialog.js"; + +const { Component, xml } = owl; + +export class FooterButtons extends Component { + static props = {}; + static components = { + RemoteDebugDialog, + HandlerDialog, + CredentialDialog, + }; + + setup() { + this.store = useStore(); + } + + static template = xml` + + `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/components/IconButton.js b/addons/hw_posbox_homepage/static/src/app/components/IconButton.js new file mode 100644 index 0000000000000..9c420efc7f8a6 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/components/IconButton.js @@ -0,0 +1,22 @@ +/* global owl */ + +import useStore from "../hooks/useStore.js"; + +const { Component, xml } = owl; + +export class IconButton extends Component { + static props = { + onClick: Function, + icon: String, + }; + + setup() { + this.store = useStore(); + } + + static template = xml` +
+ +
+ `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/components/LoadingFullScreen.js b/addons/hw_posbox_homepage/static/src/app/components/LoadingFullScreen.js new file mode 100644 index 0000000000000..3e652e352e636 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/components/LoadingFullScreen.js @@ -0,0 +1,41 @@ +/* global owl */ + +import useStore from "../hooks/useStore.js"; + +const { Component, xml, onMounted } = owl; + +export class LoadingFullScreen extends Component { + static props = { + slots: Object, + }; + + setup() { + this.store = useStore(); + + // We delay the RPC verification for 10 seconds to be sure that the Odoo service + // was already restarted + onMounted(() => { + setTimeout(() => { + setInterval(async () => { + try { + await this.store.rpc({ + url: "/hw_posbox_homepage/ping", + }); + window.location.reload(); + } catch { + console.warn("Odoo service is probably rebooting."); + } + }, 750); + }, 10000); + }); + } + + static template = xml` +
+
+ Loading... +
+ +
+ `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/components/SingleData.js b/addons/hw_posbox_homepage/static/src/app/components/SingleData.js new file mode 100644 index 0000000000000..fc254dfd4c922 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/components/SingleData.js @@ -0,0 +1,48 @@ +/* global owl */ + +const { Component, xml } = owl; + +export class SingleData extends Component { + static props = { + name: String, + value: String, + icon: { type: String, optional: true }, + style: { type: String, optional: true }, + slots: { type: Object, optional: true }, + btnName: { type: String, optional: true }, + btnAction: { type: Function, optional: true }, + }; + static defaultProps = { + style: "primary", + }; + + get valueIsURL() { + const expression = + /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/; + + const regex = new RegExp(expression); + if (this.props.value.match(regex)) { + return true; + } else { + return false; + } + } + + static template = xml` +
+
+
+
+ + +
+

+ +

+
+
+ +
+ `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/components/dialog/BootstrapDialog.js b/addons/hw_posbox_homepage/static/src/app/components/dialog/BootstrapDialog.js new file mode 100644 index 0000000000000..cf021ebd204e2 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/components/dialog/BootstrapDialog.js @@ -0,0 +1,58 @@ +/* global owl */ + +const { Component, xml, useEffect, useRef } = owl; + +export class BootstrapDialog extends Component { + static props = { + identifier: String, + slots: Object, + btnName: { type: String, optional: true }, + onOpen: { type: Function, optional: true }, + onClose: { type: Function, optional: true }, + }; + + setup() { + this.dialog = useRef("dialog"); + + useEffect( + () => { + if (!this.dialog || !this.dialog.el) { + return; + } + + if (this.props.onOpen) { + this.dialog.el.addEventListener("show.bs.modal", this.props.onOpen); + } + + if (this.props.onClose) { + this.dialog.el.addEventListener("hide.bs.modal", this.props.onClose); + } + + return () => { + this.dialog.el.removeEventListener("show.bs.modal", this.props.onOpen); + this.dialog.el.removeEventListener("hide.bs.modal", this.props.onClose); + }; + }, + () => [this.dialog] + ); + } + + static template = xml` + +
+ + + + + + + + `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/components/dialog/DeviceDialog.js b/addons/hw_posbox_homepage/static/src/app/components/dialog/DeviceDialog.js new file mode 100644 index 0000000000000..fd8c59d323779 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/components/dialog/DeviceDialog.js @@ -0,0 +1,52 @@ +/* global owl */ + +import useStore from "../../hooks/useStore.js"; +import { SingleData } from "../SingleData.js"; +import { BootstrapDialog } from "./BootstrapDialog.js"; + +const { Component, xml, useState } = owl; + +export class DeviceDialog extends Component { + static props = {}; + static components = { BootstrapDialog, SingleData }; + + setup() { + this.store = useStore(); + this.state = useState({ + loading: false, + }); + } + + onClose() { + this.state.initialization = []; + this.state.handlerData = {}; + } + + get devices() { + // Put blackbox first in the list + return this.store.base.iot_device_status.sort((a, b) => + a.type === "fiscal data module" ? -1 : 1 + ); + } + + static template = xml` + + + Devices list + + + +
+ + + +
+
+ + + +
+ `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/components/dialog/HandlerDialog.js b/addons/hw_posbox_homepage/static/src/app/components/dialog/HandlerDialog.js new file mode 100644 index 0000000000000..7d1d501283275 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/components/dialog/HandlerDialog.js @@ -0,0 +1,191 @@ +/* global owl */ + +import useStore from "../../hooks/useStore.js"; +import { BootstrapDialog } from "./BootstrapDialog.js"; +import { LoadingFullScreen } from "../LoadingFullScreen.js"; + +const { Component, xml, useState } = owl; + +export class HandlerDialog extends Component { + static props = {}; + static components = { BootstrapDialog, LoadingFullScreen }; + + setup() { + this.store = useStore(); + this.state = useState({ + initialization: true, + waitRestart: false, + loading: false, + handlerData: {}, + globalLogger: {}, + }); + } + + onClose() { + this.state.initialization = []; + this.state.handlerData = {}; + } + + async getHandlerData() { + try { + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/log_levels", + }); + this.state.handlerData = data; + this.state.globalLogger = { + "iot-logging-root": data.root_logger_log_level, + "iot-logging-odoo": data.odoo_current_log_level, + }; + this.state.initialization = false; + } catch { + console.warn("Error while fetching data"); + } + } + + async onChange(name, value) { + try { + await this.store.rpc({ + url: "/hw_posbox_homepage/log_levels_update", + method: "POST", + params: { + name: name, + value: value, + }, + }); + } catch { + console.warn("Error while saving data"); + } + } + + async loadIotHandlers() { + try { + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/load_iot_handlers", + }); + + if (data.status === "success") { + this.state.waitRestart = true; + } + } catch { + console.warn("Error while saving data"); + } + } + + async clearIotHandlers() { + try { + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/clear_iot_handlers", + }); + + if (data.status === "success") { + this.state.waitRestart = true; + } + } catch { + console.warn("Error while saving data"); + } + } + + static template = xml` + + + Processing your request, please wait... + + + + + + Handler logging + + +
+
+ Loading... +
+

Currently scanning for initialized drivers and interfaces...

+
+ +
+
Global logs level
+
+ + +
+
+
+
+
+
Interfaces logs level
+
+
+
+
+
Drivers logs level
+
+
+
+
+
Debug
+
+ + +
+
+
+
+ + + +
+ `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/components/dialog/RemoteDebugDialog.js b/addons/hw_posbox_homepage/static/src/app/components/dialog/RemoteDebugDialog.js new file mode 100644 index 0000000000000..b0404c99ada01 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/components/dialog/RemoteDebugDialog.js @@ -0,0 +1,111 @@ +/* global owl */ + +import useStore from "../../hooks/useStore.js"; +import { LoadingFullScreen } from "../LoadingFullScreen.js"; +import { BootstrapDialog } from "./BootstrapDialog.js"; + +const { Component, xml, useState } = owl; + +export class RemoteDebugDialog extends Component { + static props = {}; + static components = { BootstrapDialog, LoadingFullScreen }; + + setup() { + this.store = useStore(); + this.state = useState({ + password: "", + loading: false, + ngrok: false, + ngrokToken: "", + }); + } + + async generatePassword() { + try { + this.state.loading = true; + + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/generate_password", + }); + + this.state.password = data.password; + this.state.loading = false; + } catch { + console.warn("Error while fetching data"); + } + } + + async connectToRemoteDebug() { + if (!this.state.ngrokToken) { + return; + } + + try { + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/enable_ngrok", + method: "POST", + params: { + auth_token: this.state.ngrokToken, + }, + }); + + if (data.status === "success") { + this.state.ngrok = true; + } + } catch { + console.warn("Error while enabling remote debugging"); + } + } + + async restartIotBox() { + try { + await this.store.rpc({ + url: "/hw_posbox_homepage/restart_iotbox", + }); + + this.state.waitRestart = true; + } catch { + console.warn("Error while restarting IoT Box"); + } + } + + static template = xml` + + + Processing your request, please wait... + + + + + + Remote Debugging + + + +
+ + +
+ +
+ +
+ +
+ + + + +
+ `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/components/dialog/ServerDialog.js b/addons/hw_posbox_homepage/static/src/app/components/dialog/ServerDialog.js new file mode 100644 index 0000000000000..d90e1d85d4992 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/components/dialog/ServerDialog.js @@ -0,0 +1,101 @@ +/* global owl */ + +import useStore from "../../hooks/useStore.js"; +import { BootstrapDialog } from "./BootstrapDialog.js"; +import { LoadingFullScreen } from "../LoadingFullScreen.js"; + +const { Component, xml, useState, toRaw } = owl; + +export class ServerDialog extends Component { + static props = {}; + static components = { BootstrapDialog, LoadingFullScreen }; + + setup() { + this.store = toRaw(useStore()); + this.state = useState({ waitRestart: false }); + this.form = useState({ + token: "", + iotname: this.store.base.hostname, + }); + } + + async connectToServer() { + try { + if (!this.form.token && !this.form.iotname) { + return; + } + + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/connect_to_server", + method: "POST", + params: this.form, + }); + + if (data.status === "success") { + this.state.waitRestart = true; + } + } catch { + console.warn("Error while fetching data"); + } + } + + async clearConfiguration() { + try { + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/server_clear", + }); + + if (data.status === "success") { + this.state.waitRestart = true; + } + } catch { + console.warn("Error while clearing configuration"); + } + } + + static template = xml` + + + Processing your request please wait... + + + + + + Configure Odoo Server + + + +
+
+ + + Please enter a correct name +
+
+ + + Please enter a server token +
+
+ +
+
+
+ +
+
+

Your current server is:

+
+
+ + +
+
+
+
+ `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/components/dialog/SixDialog.js b/addons/hw_posbox_homepage/static/src/app/components/dialog/SixDialog.js new file mode 100644 index 0000000000000..e411cbb03d93f --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/components/dialog/SixDialog.js @@ -0,0 +1,85 @@ +/* global owl */ + +import useStore from "../../hooks/useStore.js"; +import { BootstrapDialog } from "./BootstrapDialog.js"; +import { LoadingFullScreen } from "../LoadingFullScreen.js"; + +const { Component, xml, useState, toRaw } = owl; + +export class SixDialog extends Component { + static props = {}; + static components = { BootstrapDialog, LoadingFullScreen }; + + setup() { + this.store = toRaw(useStore()); + this.state = useState({ waitRestart: false }); + this.form = useState({ terminal_id: this.store.base.six_terminal }); + } + + async connectToServer() { + try { + if (!this.form.terminal_id) { + return; + } + + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/six_payment_terminal_add", + method: "POST", + params: this.form, + }); + + if (data.status === "success") { + this.state.waitRestart = true; + } + } catch { + console.warn("Error while fetching data"); + } + } + + async clearConfiguration() { + try { + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/six_payment_terminal_clear", + }); + + if (data.status === "success") { + this.state.waitRestart = true; + } + } catch { + console.warn("Error while clearing configuration"); + } + } + + static template = xml` + + + Your IoT Box is currently processing your request. Please wait. + + + + + + Configure Six Terminal + + + +
+
+ + + Please enter a correct terminal ID +
+
+ + +
+
+
+ + + +
+ `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/components/dialog/UpdateDialog.js b/addons/hw_posbox_homepage/static/src/app/components/dialog/UpdateDialog.js new file mode 100644 index 0000000000000..d7af68b0efe6f --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/components/dialog/UpdateDialog.js @@ -0,0 +1,96 @@ +/* global owl */ + +import useStore from "../../hooks/useStore.js"; +import { BootstrapDialog } from "./BootstrapDialog.js"; +import { LoadingFullScreen } from "../LoadingFullScreen.js"; + +const { Component, xml, useState, markup } = owl; + +export class UpdateDialog extends Component { + static props = {}; + static components = { BootstrapDialog, LoadingFullScreen }; + + setup() { + this.store = useStore(); + this.state = useState({ + initialization: true, + commitHtml: "", + loading: false, + waitRestart: false, + upgradeData: [], + }); + } + + onClose() { + this.state.initialization = []; + } + + async getUpgradeData() { + try { + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/upgrade", + }); + + this.state.upgradeData = data; + this.state.commitHtml = markup(data.commit); + this.state.initialization = false; + } catch { + console.warn("Error while fetching data"); + } + } + + async upgradeIotBox() { + console.warn("Not implemented yet"); + } + + static template = xml` + + + Upgrading your device, please wait... + + + + + + Upgrade IoTBox + + +
+
+ Loading... +
+

Currently fetching upgrade data...

+
+ +
+ +
+
Commit details:
+
+                
+ +
+ +
+ + + + + + `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/components/dialog/WifiDialog.js b/addons/hw_posbox_homepage/static/src/app/components/dialog/WifiDialog.js new file mode 100644 index 0000000000000..ccdff40e1bfa9 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/components/dialog/WifiDialog.js @@ -0,0 +1,139 @@ +/* global owl */ + +import useStore from "../../hooks/useStore.js"; +import { BootstrapDialog } from "./BootstrapDialog.js"; +import { LoadingFullScreen } from "../LoadingFullScreen.js"; + +const { Component, xml, useState } = owl; + +export class WifiDialog extends Component { + static props = {}; + static components = { BootstrapDialog, LoadingFullScreen }; + + setup() { + this.store = useStore(); + this.state = useState({ + scanning: true, + loading: false, + waitRestart: false, + availableWifi: [], + }); + this.form = useState({ + essid: "", + password: "", + persistent: false, + }); + } + + onClose() { + this.state.availableWifi = []; + this.state.scanning = true; + this.form.essid = ""; + this.form.password = ""; + this.form.persistent = false; + } + + async getWifiNetworks() { + try { + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/wifi", + }); + + this.state.availableWifi = data; + this.state.scanning = false; + } catch { + console.warn("Error while fetching data"); + } + } + + async connectToWifi() { + if (!this.form.essid || !this.form.password) { + return; + } + + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/update_wifi", + method: "POST", + params: this.form, + }); + + if (data.status === "success") { + this.state.waitRestart = true; + } + } + + async clearConfiguration() { + try { + const data = await this.store.rpc({ + url: "/hw_posbox_homepage/wifi_clear", + }); + + if (data.status === "success") { + this.state.waitRestart = true; + } + } catch { + console.warn("Error while clearing configuration"); + } + } + + static template = xml` + + + Processing your request please wait... + + + + + + Configure WIFI + + +
+
+ Loading... +
+

Currently scanning for available networks...

+
+ + +
+
+ + + Please select a network +
+ +
+ + + Please enter a password +
+ +
+ + +
+ +
+ + +
+
+
+ + + +
+ `; +} diff --git a/addons/hw_posbox_homepage/static/src/app/css/override.css b/addons/hw_posbox_homepage/static/src/app/css/override.css new file mode 100644 index 0000000000000..41378e3ce1c35 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/css/override.css @@ -0,0 +1,34 @@ +.btn.btn-primary { + background-color: #714B67 !important; + border-color: #714B67 !important; +} + +.alert.alert-secondary { + color: #017E84 !important; + background-color: #017E8425 !important; + border-color: #017E8480 !important; +} + +.odoo-bg-primary, .bg-primary, .form-check-input:checked, .nav-pills .nav-link.active { + background-color: #714B67 !important; + border-color: #714B67 !important; +} + +.odoo-bg-secondary, .bg-secondary { + background-color: #017E84 !important; +} + +.cursor-pointer { + cursor: pointer; +} + +.one-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.link-primary { + color: #714B67 !important; + font-size: 12px; +} diff --git a/addons/hw_posbox_homepage/static/src/app/hooks/useStore.js b/addons/hw_posbox_homepage/static/src/app/hooks/useStore.js new file mode 100644 index 0000000000000..2e7ff17b2dba3 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/hooks/useStore.js @@ -0,0 +1,8 @@ +/* global owl */ + +const { useState, useEnv } = owl; + +export default function useStore() { + const env = useEnv(); + return useState(env.store); +} diff --git a/addons/hw_posbox_homepage/static/src/app/main.js b/addons/hw_posbox_homepage/static/src/app/main.js new file mode 100644 index 0000000000000..f297ca3372f27 --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/main.js @@ -0,0 +1,16 @@ +/* global owl */ + +import { Homepage } from "./Homepage.js"; +import Store from "./store.js"; + +const { mount, reactive } = owl; + +function createStore() { + return reactive(new Store()); +} + +mount(Homepage, document.body, { + env: { + store: createStore(), + }, +}); diff --git a/addons/hw_posbox_homepage/static/src/app/store.js b/addons/hw_posbox_homepage/static/src/app/store.js new file mode 100644 index 0000000000000..02c97732c43ca --- /dev/null +++ b/addons/hw_posbox_homepage/static/src/app/store.js @@ -0,0 +1,33 @@ +export default class Store { + constructor() { + this.setup(); + } + setup() { + this.url = ""; + this.base = {}; + this.update = 0; + this.advanced = false; + } + + async rpc({ url, method = "GET", params = {} }) { + if (method === "POST") { + const response = await fetch(url, { + method, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + params, + }), + }); + + const data = await response.json(); + return data.result; + } else if (method === "GET") { + const response = await fetch(url); + return await response.json(); + } + + return false; + } +} diff --git a/addons/hw_posbox_homepage/views/configure_wizard.html b/addons/hw_posbox_homepage/views/configure_wizard.html deleted file mode 100644 index 52b07e389ec10..0000000000000 --- a/addons/hw_posbox_homepage/views/configure_wizard.html +++ /dev/null @@ -1,179 +0,0 @@ -{% extends "layout.html" %} -{% from "loading.html" import loading_block_ui %} -{% block head %} - - -{% endblock %} -{% block content %} -

Configure IoT Box

-
    -
  • Connect to Odoo
  • -
  • Connect to Internet
  • -
  • Done
  • -
-
-
-
- - - - - - - - - -
IoT Box Name
Server token
-
- Server token is not mandatory for the community version. -
- -
-
- - - - - - - - - -
Wifi Network - -
Password
- -
-
-

✔ Nice! Your configuration is done.

-

-

-
- {{ loading_block_ui(loading_message) }} -
-{% endblock %} diff --git a/addons/hw_posbox_homepage/views/handler_list.html b/addons/hw_posbox_homepage/views/handler_list.html deleted file mode 100644 index c758df01218e3..0000000000000 --- a/addons/hw_posbox_homepage/views/handler_list.html +++ /dev/null @@ -1,128 +0,0 @@ -{% extends "layout.html" %} -{% from "loading.html" import loading_block_ui %} -{% block head %} - -{% endblock %} -{% block content %} -

Logging

-
- - -
- - - - - -
- -

Interfaces list

- - - - - - {% for interface in interfaces_list -%} - - - - - {%- endfor %} -
NameLog Level
{{ interface }} - {% set interface_logger_info = interfaces_logger_info[interface] %} - {% if interface_logger_info != False %} - - {% else %} - Logger uninitialised - {% endif %} -
- -

Drivers list

- - - - - - {% for driver in drivers_list -%} - - - - - {%- endfor %} -
NameLog Level
{{ driver }} - {% set driver_logger_info = drivers_logger_info[driver] %} - {% if driver_logger_info != False %} - - {% else %} - Logger uninitialised - {% endif %} -
- -
- {% if server %} - Load handlers - {% endif %} - -
-
- {% if server %} -
- You can clear the handlers configuration -
- -
-
- {% endif %} - {{ loading_block_ui('Loading Handlers') }} -{% endblock %} diff --git a/addons/hw_posbox_homepage/views/homepage.html b/addons/hw_posbox_homepage/views/homepage.html deleted file mode 100644 index c3b97242b3523..0000000000000 --- a/addons/hw_posbox_homepage/views/homepage.html +++ /dev/null @@ -1,263 +0,0 @@ -{% extends "layout.html" %} -{% from "loading.html" import loading_block_ui %} -{% block head %} - - -{% endblock %} -{% block content %} -
-
-
Restart
-
-
- {% if system == "Linux" %} - - {% endif %} - -
-
-
-
-

Your IoT Box is up and running

- - - - - - - - - - - - - - - - - - - - - - - - - - {% if server_status != "Not Configured" %} - - - - - - - - - {% endif %} - {% if pairing_code %} - - - - - {% endif %} - - - - -
Name {{ hostname }} {% if system == "Linux" %}configure{% endif %}
Version {{ version }} {% if system == "Linux" %}update{% endif %}
IP Address{{ ip }}
Mac Address {{ mac }}
Network{{ network_status }} {% if system == "Linux" %}configure wifi{% endif %}
Server{% if server_status != "Not Configured" %}{% endif %}{{ server_status }}configure
HTTPS certificate - {% if is_certificate_ok %} -
- OK - {{ certificate_details }} -
- {% else %} - Error code: - {% set error_code = certificate_details.split(' ') | first | replace("_", "-") | lower %} - {% set doc_url = 'https://www.odoo.com/documentation/17.0/applications/productivity/iot/config/https_certificate_iot.html#' ~ error_code %} - help -
- {{ certificate_details }} - {% endif %} -
Six payment terminal{{ six_terminal }} configure
Pairing code{{ pairing_code }}
IOT Device -
- {% if iot_device_status|length == 0 %} - No Device Found - {% endif %} - {% for iot_devices in iot_device_status|groupby('type') %} -
-
{{ iot_devices.grouper|capitalize }}s
-
- {% for device in iot_devices.list %} -
- {{ device['name'] }} -
{{ device['identifier'] }}
-
- {% endfor %} -
-
- {% endfor %} -
-
handlers list
-
-
- POS Display - {% if system == "Linux" %} - Remote Debug - Printers server - {% endif %} - {% if server_status != "Not Configured" %} - Credential - {% endif %} -
- {{ loading_block_ui(loading_message) }} -{% endblock %} diff --git a/addons/hw_posbox_homepage/views/index.html b/addons/hw_posbox_homepage/views/index.html new file mode 100644 index 0000000000000..41276211327d5 --- /dev/null +++ b/addons/hw_posbox_homepage/views/index.html @@ -0,0 +1,35 @@ + + + + + + + Odoo's IoT Box + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/hw_posbox_homepage/views/layout.html b/addons/hw_posbox_homepage/views/layout.html deleted file mode 100644 index 490e89d802ef6..0000000000000 --- a/addons/hw_posbox_homepage/views/layout.html +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - {{ title or "Odoo's IoT Box" }} - - - {% block head %}{% endblock %} - - - {%if breadcrumb %} - - {% endif %} -
- {% block content %}{% endblock %} -

-

- - - diff --git a/addons/hw_posbox_homepage/views/list_credential.html b/addons/hw_posbox_homepage/views/list_credential.html deleted file mode 100644 index 87a8f09f22f1f..0000000000000 --- a/addons/hw_posbox_homepage/views/list_credential.html +++ /dev/null @@ -1,68 +0,0 @@ -{% extends "layout.html" %} -{% from "loading.html" import loading_block_ui %} -{% block head %} - -{% endblock %} -{% block content %} -

List Credential

-

- Set the DB UUID and your Contract Number you want to use. -

-
- - - - - - - - - - - - -
DB uuid
Contract Number
-
- {{ loading_block_ui(loading_message) }} -
- {% if db_uuid or enterprise_code %} -

- Current DB uuid: {{ db_uuid }} -

-

- Current Contract Number: {{ enterprise_code }} -

-
- You can clear the credential configuration -
- -
-
- {% endif %} -{% endblock %} diff --git a/addons/hw_posbox_homepage/views/loading.html b/addons/hw_posbox_homepage/views/loading.html deleted file mode 100644 index 7d651464cc72a..0000000000000 --- a/addons/hw_posbox_homepage/views/loading.html +++ /dev/null @@ -1,14 +0,0 @@ -{% macro loading_block_ui(message) %} -
-
-
- Loading... -
-
-
- Please wait..
- {{ message }} -
-
-
-{% endmacro %} diff --git a/addons/hw_posbox_homepage/views/logs.html b/addons/hw_posbox_homepage/views/logs.html new file mode 100644 index 0000000000000..3a4c5722ae9fb --- /dev/null +++ b/addons/hw_posbox_homepage/views/logs.html @@ -0,0 +1,24 @@ + + + + + + + Odoo's IoT Box Logs + + + + +

+    
+
diff --git a/addons/hw_posbox_homepage/views/remote_connect.html b/addons/hw_posbox_homepage/views/remote_connect.html
deleted file mode 100644
index 1967918d8c1e8..0000000000000
--- a/addons/hw_posbox_homepage/views/remote_connect.html
+++ /dev/null
@@ -1,56 +0,0 @@
-{% extends "layout.html" %}
-{% block head %}
-    
-{% endblock %}
-{% block content %}
-    

Remote Debugging

-

- This allows someone who give a ngrok authtoken to gain remote access to your IoT Box, and - thus your entire local network. Only enable this for someone - you trust. -

- -{% endblock %} diff --git a/addons/hw_posbox_homepage/views/server_config.html b/addons/hw_posbox_homepage/views/server_config.html deleted file mode 100644 index b2c0ec0bafcf8..0000000000000 --- a/addons/hw_posbox_homepage/views/server_config.html +++ /dev/null @@ -1,79 +0,0 @@ -{% extends "layout.html" %} -{% from "loading.html" import loading_block_ui %} -{% block head %} - -{% endblock %} -{% block content %} -

Configure Odoo Server

-

- Paste the token from the Connect wizard in your Odoo instance in the Server Token field. If you change the IoT Box Name, - your IoT Box will need a reboot. -

-
- - - - - - - - - - - - -
IoT Box Name
Server Token
-
-

- Your current server {{ server_status }} -

- - {{ loading_block_ui(loading_message) }} -
-
- You can clear the server configuration -
- -
-
-{% endblock %} diff --git a/addons/hw_posbox_homepage/views/six_payment_terminal.html b/addons/hw_posbox_homepage/views/six_payment_terminal.html deleted file mode 100644 index 89a2e694f6c36..0000000000000 --- a/addons/hw_posbox_homepage/views/six_payment_terminal.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends "layout.html" %} -{% from "loading.html" import loading_block_ui %} -{% block head %} - -{% endblock %} -{% block content %} -

Six Payment Terminal

-

- Set the Terminal ID (TID) of the terminal you want to use. -

-
- - - - - - - - -
Terminal ID (digits only)
-
- {{ loading_block_ui(loading_message) }} -
- {% if terminalId %} -

- Current Terminal Id: {{ terminalId }} -

-
- You can clear the terminal configuration -
- -
-
- {% endif %} -{% endblock %} diff --git a/addons/hw_posbox_homepage/views/upgrade_page.html b/addons/hw_posbox_homepage/views/upgrade_page.html deleted file mode 100644 index 0c5d7a93960e9..0000000000000 --- a/addons/hw_posbox_homepage/views/upgrade_page.html +++ /dev/null @@ -1,95 +0,0 @@ -{% extends "layout.html" %} -{% from "loading.html" import loading_block_ui %} -{% block head %} - - - -{% endblock %} -{% block content %} -

IoT Box Software Upgrade

-

- This tool will help you perform an upgrade of the IoTBox's software over the internet. - However the preferred method to upgrade the IoTBox is to flash the sd-card with - the latest image. The upgrade - procedure is explained into to the - IoTBox manual -

-

- To upgrade the IoTBox, click on the upgrade button. The upgrade will take a few minutes. Do not reboot the IoTBox during the upgrade. -

-
-
- Latest patch: -
-
{{ commit|safe }}
-
-
- {% if flashToVersion %} - Upgrade to {{ flashToVersion }} - {% else %} - Upgrade - {% endif %} -
- {{ loading_block_ui(loading_message) }} -{% endblock %} diff --git a/addons/hw_posbox_homepage/views/wifi_config.html b/addons/hw_posbox_homepage/views/wifi_config.html deleted file mode 100644 index 94e0579ba9c60..0000000000000 --- a/addons/hw_posbox_homepage/views/wifi_config.html +++ /dev/null @@ -1,71 +0,0 @@ -{% extends "layout.html" %} -{% from "loading.html" import loading_block_ui %} -{% block head %} - -{% endblock %} -{% block content %} -

Configure Wifi

-

- Here you can configure how the iotbox should connect to wireless networks. - Currently only Open and WPA networks are supported. When enabling the persistent checkbox, - the chosen network will be saved and the iotbox will attempt to connect to it every time it boots. -

-
- - - - - - - - - - - - - - - - -
ESSID - -
Password
Persistent
-
-
-
- You can clear the persistent configuration -
- -
-
- {{ loading_block_ui(loading_message) }} -{% endblock %} From afc82880ddf210307d3992b0b7c969e41ebcf347 Mon Sep 17 00:00:00 2001 From: "David Monnom (moda)" Date: Mon, 16 Sep 2024 10:01:54 +0200 Subject: [PATCH 07/15] [IMP] hw_posbox_homepage: Handle windows environment Remove functionnalities that are not supported on windows environment closes odoo/odoo#177895 Signed-off-by: Yaroslav Soroko (yaso) --- addons/hw_posbox_homepage/static/src/app/Homepage.js | 6 +++--- .../static/src/app/components/FooterButtons.js | 6 +++--- .../static/src/app/components/dialog/ServerDialog.js | 4 ++-- addons/hw_posbox_homepage/static/src/app/store.js | 6 ++++++ 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/addons/hw_posbox_homepage/static/src/app/Homepage.js b/addons/hw_posbox_homepage/static/src/app/Homepage.js index c902b3523aa91..95a1a0b17550e 100644 --- a/addons/hw_posbox_homepage/static/src/app/Homepage.js +++ b/addons/hw_posbox_homepage/static/src/app/Homepage.js @@ -95,17 +95,17 @@ export class Homepage extends Component { - + - + - + diff --git a/addons/hw_posbox_homepage/static/src/app/components/FooterButtons.js b/addons/hw_posbox_homepage/static/src/app/components/FooterButtons.js index 6acdd4c880013..e0764e4bcbb96 100644 --- a/addons/hw_posbox_homepage/static/src/app/components/FooterButtons.js +++ b/addons/hw_posbox_homepage/static/src/app/components/FooterButtons.js @@ -21,9 +21,9 @@ export class FooterButtons extends Component { static template = xml` diff --git a/addons/hw_posbox_homepage/static/src/app/components/dialog/ServerDialog.js b/addons/hw_posbox_homepage/static/src/app/components/dialog/ServerDialog.js index d90e1d85d4992..ecb9da0e5e65a 100644 --- a/addons/hw_posbox_homepage/static/src/app/components/dialog/ServerDialog.js +++ b/addons/hw_posbox_homepage/static/src/app/components/dialog/ServerDialog.js @@ -21,7 +21,7 @@ export class ServerDialog extends Component { async connectToServer() { try { - if (!this.form.token && !this.form.iotname) { + if (!this.form.token) { return; } @@ -70,7 +70,7 @@ export class ServerDialog extends Component { If you change the IoT Box Name, your IoT Box will need a reboot.
-
+
Please enter a correct name diff --git a/addons/hw_posbox_homepage/static/src/app/store.js b/addons/hw_posbox_homepage/static/src/app/store.js index 02c97732c43ca..a3f8abe88c2b7 100644 --- a/addons/hw_posbox_homepage/static/src/app/store.js +++ b/addons/hw_posbox_homepage/static/src/app/store.js @@ -6,6 +6,7 @@ export default class Store { this.url = ""; this.base = {}; this.update = 0; + this.isLinux = false; this.advanced = false; } @@ -22,6 +23,11 @@ export default class Store { }); const data = await response.json(); + + if (data.result.system === "Linux") { + this.isLinux = true; + } + return data.result; } else if (method === "GET") { const response = await fetch(url); From fea4546234832f82738504d5bac19694c795d3d0 Mon Sep 17 00:00:00 2001 From: "Maximilien (malb)" Date: Wed, 3 Jul 2024 07:23:11 +0000 Subject: [PATCH 08/15] [FIX] account: remove readonly constraint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit will change the readonly constraint on the journal when an account move has been posted. It will readonly only when the sequence number is different from False, an empty string or a slash and if it was posted before l10n_ar have specific naming depending on the journal, when changing the readonly some test were failing due to an inverse that had the wrong journal. To avoid changing the behavior of the localisation in particular we added a xpath that will change the readonly back to what it was. closes odoo/odoo#180266 Task: 4028973 X-original-commit: a69fd77780e064fff22d650fc284d5612b172f5d Signed-off-by: William André (wan) Signed-off-by: Maximilien La Barre (malb) --- addons/account/views/account_move_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 287ef2d67b7da..306cd5609d64a 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -975,7 +975,7 @@ invisible="context.get('default_journal_id') and context.get('move_type', 'entry') != 'entry'"> + readonly="posted_before and name not in (False, '', '/')"/> in From 72873056b3f989342246e6b16368e536cf67bd0c Mon Sep 17 00:00:00 2001 From: "Shubham Agarwal [SHAG]" Date: Fri, 5 Jul 2024 15:08:03 +0200 Subject: [PATCH 09/15] [FIX] pos_restaurant: unique table number and duplicate table Steps: Open POS Restaurant Edit floor & click on a table Rapidly click add table multiple times Multiple tables with the same number are created Duplicate tables also overlap the original table completely Issue: Duplicate tables should not be created. Cause: The "Add" button that creates the table calls an async function. Rapid clicks make new calls even before the promises are successful. FIX: Disable the button and creation until the previous table is created so that rapid clicks are handled. task-3956310 closes odoo/odoo#169937 Signed-off-by: Joseph Caburnay (jcb) --- .../static/src/app/utils/hooks.js | 59 ++++++++++++++++++- .../static/src/app/floor_screen/edit_bar.js | 2 + .../static/src/app/floor_screen/edit_bar.xml | 9 ++- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/addons/point_of_sale/static/src/app/utils/hooks.js b/addons/point_of_sale/static/src/app/utils/hooks.js index 42323d05f3fd5..0b4fc42b9e321 100644 --- a/addons/point_of_sale/static/src/app/utils/hooks.js +++ b/addons/point_of_sale/static/src/app/utils/hooks.js @@ -5,7 +5,7 @@ import { OfflineErrorPopup } from "@point_of_sale/app/errors/popups/offline_erro import { ConfirmPopup } from "@point_of_sale/app/utils/confirm_popup/confirm_popup"; import { ErrorTracebackPopup } from "@point_of_sale/app/errors/popups/error_traceback_popup"; import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; -import { useEnv, onMounted, onPatched, useComponent, useRef } from "@odoo/owl"; +import { useState, useEnv, onMounted, onPatched, useComponent, useRef } from "@odoo/owl"; /** * Introduce error handlers in the component. @@ -87,3 +87,60 @@ export function useAsyncLockedMethod(method) { } }; } + +/** + * Wrapper for an async function that exposes the status of the function call. + * + * Sample use case: + * ```js + * { + * // inside in a component + * this.doPrint = useTrackedAsync(() => this.printReceipt()) + * this.doPrint.status === 'idle' + * this.doPrint.call() // triggers the given async function + * this.doPrint.status === 'loading' + * ['success', 'error].includes(this.doPrint.status) && this.doPrint.result + * } + * ``` + * @param {(...args: any[]) => Promise} asyncFn + */ +export function useTrackedAsync(asyncFn) { + /** + * @type {{ + * status: 'idle' | 'loading' | 'error' | 'success', + * result: any, + * lastArgs: any[] + * }} + */ + const state = useState({ + status: "idle", + result: null, + lastArgs: null, + }); + + const lockedCall = useAsyncLockedMethod(async (...args) => { + state.status = "loading"; + state.result = null; + state.lastArgs = args; + try { + const result = await asyncFn(...args); + state.status = "success"; + state.result = result; + } catch (error) { + state.status = "error"; + state.result = error; + } + }); + return { + get status() { + return state.status; + }, + get result() { + return state.result; + }, + get lastArgs() { + return state.lastArgs; + }, + call: lockedCall, + }; +} diff --git a/addons/pos_restaurant/static/src/app/floor_screen/edit_bar.js b/addons/pos_restaurant/static/src/app/floor_screen/edit_bar.js index 57501b7bf20d7..db76066b2b8fb 100644 --- a/addons/pos_restaurant/static/src/app/floor_screen/edit_bar.js +++ b/addons/pos_restaurant/static/src/app/floor_screen/edit_bar.js @@ -2,6 +2,7 @@ import { Component, useExternalListener, useState } from "@odoo/owl"; import { useService } from "@web/core/utils/hooks"; +import { useTrackedAsync } from "@point_of_sale/app/utils/hooks"; export class EditBar extends Component { static template = "pos_restaurant.EditBar"; @@ -26,6 +27,7 @@ export class EditBar extends Component { setup() { this.ui = useState(useService("ui")); useExternalListener(window, "click", this.onOutsideClick); + this.doCreateTable = useTrackedAsync(this.props.createTable); } onOutsideClick() { diff --git a/addons/pos_restaurant/static/src/app/floor_screen/edit_bar.xml b/addons/pos_restaurant/static/src/app/floor_screen/edit_bar.xml index 45df25e6477f3..1f835dc958d20 100644 --- a/addons/pos_restaurant/static/src/app/floor_screen/edit_bar.xml +++ b/addons/pos_restaurant/static/src/app/floor_screen/edit_bar.xml @@ -4,8 +4,13 @@
- - + + + + + + + Table From 2c68dae229f99c4fd7cc4fbfc093bd94eed1341c Mon Sep 17 00:00:00 2001 From: Louis Travaux Date: Fri, 13 Sep 2024 10:49:54 +0200 Subject: [PATCH 10/15] [FIX] hw_drivers: get server url error in access point mode If the IoT Box is in access point mode, it should mean that it has no internet connection. Thus, we avoid returning the linked db url to avoid fetching it. We used to check the access point mode with a `subprocess` call to `hostapd` service. We now check the IP, as it is set to `10.11.12.1` when in AP mode. closes odoo/odoo#180159 Task: 4182947 Signed-off-by: Yaroslav Soroko (yaso) --- addons/hw_drivers/tools/helpers.py | 12 +++++++----- .../tools/posbox/configuration/wireless_ap.sh | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/addons/hw_drivers/tools/helpers.py b/addons/hw_drivers/tools/helpers.py index 573921e6c2466..16ce70c64181c 100644 --- a/addons/hw_drivers/tools/helpers.py +++ b/addons/hw_drivers/tools/helpers.py @@ -288,12 +288,14 @@ def get_ssid(): @cache def get_odoo_server_url(): - if platform.system() == 'Linux': - ap = subprocess.call(['systemctl', 'is-active', '--quiet', 'hostapd']) # if service is active return 0 else inactive - if not ap: - return False + """Get the URL of the linked Odoo database. + If the IoT Box is in access point mode, it will return ``None`` to avoid + connecting to the server. - return get_conf('remote_server') + :return: The URL of the linked Odoo database. + :rtype: str or None + """ + return None if access_point() else get_conf('remote_server') def get_token(): diff --git a/addons/point_of_sale/tools/posbox/configuration/wireless_ap.sh b/addons/point_of_sale/tools/posbox/configuration/wireless_ap.sh index 9af984d35521f..f108a1888d4e2 100755 --- a/addons/point_of_sale/tools/posbox/configuration/wireless_ap.sh +++ b/addons/point_of_sale/tools/posbox/configuration/wireless_ap.sh @@ -77,5 +77,6 @@ else killall nginx service nginx restart service dnsmasq stop + ip addr del 10.11.12.1/24 dev wlan0 # remove the static ip service odoo restart # As this file is executed on boot, this line is responsible for restarting odoo service on reboot fi From ea0334f9a544cae03bb2432c66daaf4e7b9a5c55 Mon Sep 17 00:00:00 2001 From: guva-odoo Date: Mon, 16 Sep 2024 08:39:04 +0000 Subject: [PATCH 11/15] [I18N] account_edi_ubl_cii_tax_extension: add new stable module Add `account_edi_ubl_cii_tax_extension` module to transifex. Pot file already added in original PR that adds in new module into stable. Orig PR: odoo/odoo#176221 opw-4061329 closes odoo/odoo#180325 X-original-commit: b0cf5b6f55b19eb7e5c67b1ed3152ba2221e89f5 Signed-off-by: Guillaume Vanleynseele (guva) --- .tx/config | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.tx/config b/.tx/config index 3deacd1ba43d6..674affff688e2 100644 --- a/.tx/config +++ b/.tx/config @@ -64,6 +64,15 @@ resource_name = account_edi_ubl_cii replace_edited_strings = false keep_translations = false +[o:odoo:p:odoo-17:r:account_edi_ubl_cii_tax_extension] +file_filter = addons/account_edi_ubl_cii_tax_extension/i18n/.po +source_file = addons/account_edi_ubl_cii_tax_extension/i18n/account_edi_ubl_cii_tax_extension.pot +type = PO +minimum_perc = 0 +resource_name = account_edi_ubl_cii_tax_extension +replace_edited_strings = false +keep_translations = false + [o:odoo:p:odoo-17:r:account_fleet] file_filter = addons/account_fleet/i18n/.po source_file = addons/account_fleet/i18n/account_fleet.pot From 6825830064a0a87e822dbc74c8481b5c2ce32ac4 Mon Sep 17 00:00:00 2001 From: Harsh Modi Date: Mon, 16 Sep 2024 17:25:53 +0530 Subject: [PATCH 12/15] [FIX] l10n_in*: correct the hsn code for demo data closes odoo/odoo#180316 Signed-off-by: Josse Colpaert (jco) --- addons/l10n_in/demo/product_demo.xml | 10 +++++----- addons/l10n_in_pos/data/product_demo.xml | 16 ++++++++-------- addons/l10n_in_sale/data/product_demo.xml | 2 +- addons/l10n_in_stock/data/product_demo.xml | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/addons/l10n_in/demo/product_demo.xml b/addons/l10n_in/demo/product_demo.xml index f68157b4b63b3..f33600087fecd 100644 --- a/addons/l10n_in/demo/product_demo.xml +++ b/addons/l10n_in/demo/product_demo.xml @@ -91,12 +91,12 @@ - 9963.31 + 996331 Services provided by Restaurants, Cafes and similar eating facilities including takeaway services, Room services and door delivery of food. - 9963.32 + 996332 Services provided by Hotels, INN, Guest House, Club etc including Room services, takeaway services and door delivery of food. @@ -110,15 +110,15 @@ Lamps and lighting fittings including searchlights and spotlights and parts thereof, not elsewhere specified or included; illuminated signs, illuminated name-plates and the like, having a permanently fixed light source, and parts thereof not elsewhere specified or included - 4911.99.10 + 49119910 Hard copy (printed) of computer software - 9401.61.00 + 94016100 Seats (other than those of heading 9402), whether or not convertible into beds, and parts thereof - 9403.89.00 + 94038900 Other Furniture diff --git a/addons/l10n_in_pos/data/product_demo.xml b/addons/l10n_in_pos/data/product_demo.xml index 38652a1fc3258..412c710db9ff1 100644 --- a/addons/l10n_in_pos/data/product_demo.xml +++ b/addons/l10n_in_pos/data/product_demo.xml @@ -9,19 +9,19 @@ Other furniture and parts thereof. - 8539.50.00 + 85395000 Light-emitting diode (LED) lamps - 4819.60.00 + 48196000 Box files, letter trays, storage boxes and similar articles, of a kind used in offices, shops or the like - 3921.90.99 + 39219099 Other plates, sheets film , foil and strip, of plastics - 8443.32.90 + 84433290 Other, capable of connecting to an automatic data processing machine or to a network @@ -29,15 +29,15 @@ Other furniture and parts thereof. - 9403.10.90 + 94031090 Metal furniture of a kind used in offices - 9403.10.90 + 94031090 Metal furniture of a kind used in offices - 8209.00.90 + 82090090 Plates, sticks, tips and the like for tools, unmounted, of cermets. @@ -45,7 +45,7 @@ Metal furniture of a kind used in offices - 3926.10.99 + 39261099 Office supplies of a kind classified as stationary other than pins,clips, and writing instruments diff --git a/addons/l10n_in_sale/data/product_demo.xml b/addons/l10n_in_sale/data/product_demo.xml index ad3d590e9686f..5e592461c5c77 100644 --- a/addons/l10n_in_sale/data/product_demo.xml +++ b/addons/l10n_in_sale/data/product_demo.xml @@ -1,7 +1,7 @@ - 8303.00.00 + 83030000 Armoured or reinforced safes, strong boxes and doors and safe deposit lockers for strong rooms, cash or deed boxes and the like, of base metal diff --git a/addons/l10n_in_stock/data/product_demo.xml b/addons/l10n_in_stock/data/product_demo.xml index 52d4c17afe320..1989b10d9776b 100644 --- a/addons/l10n_in_stock/data/product_demo.xml +++ b/addons/l10n_in_stock/data/product_demo.xml @@ -1,7 +1,7 @@ - 4819.60.00 + 48196000 Box files, letter trays, storage boxes and similar articles, of a kind used in offices, shops or the like From da8333ea1ec93e4219ff55dbec8433a03d199883 Mon Sep 17 00:00:00 2001 From: Hesham Date: Tue, 9 Jul 2024 16:18:00 +0300 Subject: [PATCH 13/15] [FIX] l10n_in: Made invoice_label field more concise This commit simplifies the labels on invoices in the Indian accounting localization package. task-4035245 closes odoo/odoo#172469 Signed-off-by: Julien Van Roy (juvr) --- .../l10n_in/data/template/account.tax-in.csv | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/addons/l10n_in/data/template/account.tax-in.csv b/addons/l10n_in/data/template/account.tax-in.csv index 950926a163966..9eaedc35f282f 100644 --- a/addons/l10n_in/data/template/account.tax-in.csv +++ b/addons/l10n_in/data/template/account.tax-in.csv @@ -1,101 +1,101 @@ "id","name","description","invoice_label","type_tax_use","amount_type","amount","tax_scope","active","tax_group_id","is_base_affected","include_base_amount","children_tax_ids","python_compute","sequence","l10n_in_reverse_charge","repartition_line_ids/factor_percent","repartition_line_ids/repartition_type","repartition_line_ids/document_type","repartition_line_ids/account_id","repartition_line_ids/tag_ids" -"tcs_1_us_206c_1_alfhc","1% A","TCS @1% u/s 206C(1): Alcoholic Liquor for human consumption","TCS @1% u/s 206C(1): Alcoholic Liquor for human consumption","sale","percent","1.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_1_us_206c_1_alfhc","1% A","TCS @1% u/s 206C(1): Alcoholic Liquor for human consumption","1% TCS 206C(1)","sale","percent","1.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) Alcoholic Liquor" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) Alcoholic Liquor" -"tcs_5_us_206c_1_alfhc","5% A","TCS @5% u/s 206C(1): Alcoholic Liquor for human consumption","TCS @5% u/s 206C(1): Alcoholic Liquor for human consumption","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1_alfhc","5% A","TCS @5% u/s 206C(1): Alcoholic Liquor for human consumption","5% TCS 206C(1)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) Alcoholic Liquor" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) Alcoholic Liquor" -"tcs_5_us_206c_1_tl","5% T L","TCS @5% u/s 206C(1): Tendu leaves","TCS @5% u/s 206C(1): Tendu leaves","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1_tl","5% T L","TCS @5% u/s 206C(1): Tendu leaves","5% TCS 206C(1)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) Tendu leaves" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) Tendu leaves" -"tcs_2_5_us_206c_1_touafl","2.5% Tim","TCS @2.5% u/s 206C(1): Timber obtained under a forest lease","TCS @2.5% u/s 206C(1): Timber obtained under a forest lease","sale","percent","2.5","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_2_5_us_206c_1_touafl","2.5% Tim","TCS @2.5% u/s 206C(1): Timber obtained under a forest lease","2.5% TCS 206C(1)","sale","percent","2.5","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) Timber (forest lease)" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) Timber (forest lease)" -"tcs_5_us_206c_1_touafl","5% Tim","TCS @5% u/s 206C(1): Timber obtained under a forest lease","TCS @5% u/s 206C(1): Timber obtained under a forest lease","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1_touafl","5% Tim","TCS @5% u/s 206C(1): Timber obtained under a forest lease","5% TCS 206C(1)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) Timber (forest lease)" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) Timber (forest lease)" -"tcs_2_5_us_206c_1_tobamotuafl","2.5% Tim O","TCS @2.5% u/s 206C(1): Timber obtained by any mode other than under a forest lease","TCS @2.5% u/s 206C(1): Timber obtained by any mode other than under a forest lease","sale","percent","2.5","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_2_5_us_206c_1_tobamotuafl","2.5% Tim O","TCS @2.5% u/s 206C(1): Timber obtained by any mode other than under a forest lease","2.5% TCS 206C(1)","sale","percent","2.5","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) Timber (other than under a forest lease)" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) Timber (other than under a forest lease)" -"tcs_5_us_206c_1_tobamotuafl","5% Tim O","TCS @5% u/s 206C(1): Timber obtained by any mode other than under a forest lease","TCS @5% u/s 206C(1): Timber obtained by any mode other than under a forest lease","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1_tobamotuafl","5% Tim O","TCS @5% u/s 206C(1): Timber obtained by any mode other than under a forest lease","5% TCS 206C(1)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) Timber (other than under a forest lease)" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) Timber (other than under a forest lease)" -"tcs_2_5_us_206c_1_aofpnbtotl","2.5% F O","TCS @2.5% u/s 206C(1): Any other forest produce not being timber or tendu leaves","TCS @2.5% u/s 206C(1): Any other forest produce not being timber or tendu leaves","sale","percent","2.5","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_2_5_us_206c_1_aofpnbtotl","2.5% F O","TCS @2.5% u/s 206C(1): Any other forest produce not being timber or tendu leaves","2.5% TCS 206C(1)","sale","percent","2.5","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) other forest produce" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) other forest produce" -"tcs_5_us_206c_1_aofpnbtotl","5% F O","TCS @5% u/s 206C(1): Any other forest produce not being timber or tendu leaves","TCS @5% u/s 206C(1): Any other forest produce not being timber or tendu leaves","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1_aofpnbtotl","5% F O","TCS @5% u/s 206C(1): Any other forest produce not being timber or tendu leaves","5% TCS 206C(1)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) other forest produce" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) other forest produce" -"tcs_1_us_206c_1_s","1% Sc","TCS @1% u/s 206C(1): Scrap","TCS @1% u/s 206C(1): Scrap","sale","percent","1.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_1_us_206c_1_s","1% Sc","TCS @1% u/s 206C(1): Scrap","1% TCS 206C(1)","sale","percent","1.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) Scrap" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) Scrap" -"tcs_5_us_206c_1_s","5% Sc","TCS @5% u/s 206C(1): Scrap","TCS @5% u/s 206C(1): Scrap","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1_s","5% Sc","TCS @5% u/s 206C(1): Scrap","5% TCS 206C(1)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) Scrap" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) Scrap" -"tcs_1_us_206c_1_mbcoloio","1% Min","TCS @1% u/s 206C(1): Minrals, being coal or lignite or iron ore","TCS @1% u/s 206C(1): Minrals, being coal or lignite or iron ore","sale","percent","1.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_1_us_206c_1_mbcoloio","1% Min","TCS @1% u/s 206C(1): Minrals, being coal or lignite or iron ore","1% TCS 206C(1)","sale","percent","1.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) Minrals" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) Minrals" -"tcs_5_us_206c_1_mbcoloio","5% Min","TCS @5% u/s 206C(1): Minrals, being coal or lignite or iron ore","TCS @5% u/s 206C(1): Minrals, being coal or lignite or iron ore","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1_mbcoloio","5% Min","TCS @5% u/s 206C(1): Minrals, being coal or lignite or iron ore","5% TCS 206C(1)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1) Minrals" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1) Minrals" -"tcs_2_us_206c_1c_pl","2% P","TCS @2% u/s 206C(1C): Parking lot","TCS @2% u/s 206C(1C): Parking lot","sale","percent","2.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_2_us_206c_1c_pl","2% P","TCS @2% u/s 206C(1C): Parking lot","2% TCS 206C(1C)","sale","percent","2.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1C) Parking lot" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1C) Parking lot" -"tcs_5_us_206c_1c_pl","5% P","TCS @5% u/s 206C(1C): Parking lot","TCS @5% u/s 206C(1C): Parking lot","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1c_pl","5% P","TCS @5% u/s 206C(1C): Parking lot","5% TCS 206C(1C)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1C) Parking lot" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1C) Parking lot" -"tcs_2_us_206c_1c_tp","2% T","TCS @2% u/s 206C(1C): Toll plaza","TCS @2% u/s 206C(1C): Toll plaza","sale","percent","2.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_2_us_206c_1c_tp","2% T","TCS @2% u/s 206C(1C): Toll plaza","2% TCS 206C(1C)","sale","percent","2.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1C) Toll plaza" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1C) Toll plaza" -"tcs_5_us_206c_1c_tp","5% T","TCS @5% u/s 206C(1C): Toll plaza","TCS @5% u/s 206C(1C): Toll plaza","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1c_tp","5% T","TCS @5% u/s 206C(1C): Toll plaza","5% TCS 206C(1C)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1C) Toll plaza" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1C) Toll plaza" -"tcs_2_us_206c_1c_maq","2% M Q","TCS @2% u/s 206C(1C): Mining and quarrying","TCS @2% u/s 206C(1C): Mining and quarrying","sale","percent","2.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_2_us_206c_1c_maq","2% M Q","TCS @2% u/s 206C(1C): Mining and quarrying","2% TCS 206C(1C)","sale","percent","2.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1C) Mining and quarrying" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1C) Mining and quarrying" -"tcs_5_us_206c_1c_maq","5% M Q","TCS @5% u/s 206C(1C): Mining and quarrying","TCS @5% u/s 206C(1C): Mining and quarrying","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1c_maq","5% M Q","TCS @5% u/s 206C(1C): Mining and quarrying","5% TCS 206C(1C)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1C) Mining and quarrying" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1C) Mining and quarrying" -"tcs_1_us_206c_1f_mv","1% M V","TCS @1% u/s 206C(1F): Motor Vehicle","TCS @1% u/s 206C(1F): Motor Vehicle","sale","percent","1.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_1_us_206c_1f_mv","1% M V","TCS @1% u/s 206C(1F): Motor Vehicle","1% TCS 206C(1F)","sale","percent","1.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1F)" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1F)" -"tcs_5_us_206c_1f_mv","5% M V","TCS @5% u/s 206C(1F): Motor Vehicle","TCS @5% u/s 206C(1F): Motor Vehicle","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1f_mv","5% M V","TCS @5% u/s 206C(1F): Motor Vehicle","5% TCS 206C(1F)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1F)" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1F)" -"tcs_5_us_206c_1g_som","5% R 206C(1G)","TCS @5% u/s 206C(1G): Sum of money (above 7 lakhs) for remittance out of India","TCS @5% u/s 206C(1G): Sum of money (above 7 lakhs) for remittance out of India","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1g_som","5% R 206C(1G)","TCS @5% u/s 206C(1G): Sum of money (above 7 lakhs) for remittance out of India","5% TCS 206C(1G)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1G) remittance out of India" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1G) remittance out of India" -"tcs_5_us_206c_1g_soaotpp","5% O T 206C(1G)","TCS @5% u/s 206C(1G): Seller of an overseas tour program package","TCS @5% u/s 206C(1G): Seller of an overseas tour program package","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_5_us_206c_1g_soaotpp","5% O T 206C(1G)","TCS @5% u/s 206C(1G): Seller of an overseas tour program package","5% TCS 206C(1G)","sale","percent","5.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1G) overseas tour program" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1G) overseas tour program" -"tcs_0_1_us_206c_1h_sog","0.1% G 206C(1H)","TCS @0.1% u/s 206C(1H): Sale of Goods","TCS @0.1% u/s 206C(1H): Sale of Goods","sale","percent","0.1","consu","","tcs_group","","","","","","","100","base","invoice","","" +"tcs_0_1_us_206c_1h_sog","0.1% G 206C(1H)","TCS @0.1% u/s 206C(1H): Sale of Goods","0.1% TCS 206C(1H)","sale","percent","0.1","consu","","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1H)" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1H)" -"tcs_1_us_206c_1h_sog","1% G 206C(1H)","TCS @1% u/s 206C(1H): Sale of Goods","TCS @1% u/s 206C(1H): Sale of Goods","sale","percent","1.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" +"tcs_1_us_206c_1h_sog","1% G 206C(1H)","TCS @1% u/s 206C(1H): Sale of Goods","1% TCS 206C(1H)","sale","percent","1.0","consu","False","tcs_group","","","","","","","100","base","invoice","","" "","","","","","","","","","","","","","","","","100","tax","invoice","p11245","+206C(1H)" "","","","","","","","","","","","","","","","","100","base","refund","","" "","","","","","","","","","","","","","","","","100","tax","refund","p11245","-206C(1H)" From 3b11221fdbb734a721da76eb2681a47e08fffb1f Mon Sep 17 00:00:00 2001 From: kmdi-odoo Date: Mon, 2 Sep 2024 14:27:50 +0530 Subject: [PATCH 14/15] [FIX] crm: contact created without name Steps to reproduce =================== 1. Create a new lead in CRM with only an email address. 2. Send a message to the email through the chatter. -> The contact is created but it has no name. Issue ================= The issue was in _get_customer_information() for CRM. The function uses parse_contact_from_email which returns an empty name for a simple email. After this commit ================= Email will serve as a name for the contact created through the chatter in CRM. Task-4129454 closes odoo/odoo#178797 Signed-off-by: Thibault Delavallee (tde) --- addons/crm/models/crm_lead.py | 2 +- addons/crm/tests/test_crm_lead_notification.py | 3 ++- addons/mail/tests/test_res_partner.py | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 3b7500783dc85..e3c84d7670604 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -1875,7 +1875,7 @@ def _get_customer_information(self): for record in self.filtered('email_normalized'): values = email_normalized_to_values.setdefault(record.email_normalized, {}) - contact_name = record.contact_name or record.partner_name or parse_contact_from_email(record.email_from)[0] + contact_name = record.contact_name or record.partner_name or parse_contact_from_email(record.email_from)[0] or record.email_from # Note that we don't attempt to create the parent company even if partner name is set values.update(record._prepare_customer_values(contact_name, is_company=False)) values['company_name'] = record.partner_name diff --git a/addons/crm/tests/test_crm_lead_notification.py b/addons/crm/tests/test_crm_lead_notification.py index 9ec95098fc96f..c07630b4b923d 100644 --- a/addons/crm/tests/test_crm_lead_notification.py +++ b/addons/crm/tests/test_crm_lead_notification.py @@ -174,6 +174,7 @@ def test_lead_message_get_suggested_recipients_values_for_create(self): (False, 'Test', 'test_default_create@example.com'), ('Delivery Boy company', 'Test With Company', 'default_create_with_partner@example.com'), ('Delivery Boy company', '', 'default_create_with_partner_no_name@example.com'), + ('', '', 'lenny.bar@gmail.com'), ]: formatted_email = formataddr((name, email)) if name else formataddr((partner_name, email)) with self.subTest(partner_name=partner_name): @@ -197,7 +198,7 @@ def test_lead_message_get_suggested_recipients_values_for_create(self): self.assertEqual(create_vals, lead1._get_customer_information().get(email, {})) for field, value in lead_details_for_contact.items(): self.assertEqual(create_vals.get(field), value) - expected_name = partner_name if partner_name and not name else name + expected_name = name or partner_name or email self.assertEqual(create_vals['name'], expected_name) self.assertEqual(create_vals['comment'], description) # description -> comment # Parent company not created even if partner_name is set diff --git a/addons/mail/tests/test_res_partner.py b/addons/mail/tests/test_res_partner.py index ba70f86961d33..fd2ed85a5cae0 100644 --- a/addons/mail/tests/test_res_partner.py +++ b/addons/mail/tests/test_res_partner.py @@ -485,6 +485,8 @@ def test_name_create_corner_cases(self): 'False', # (simili) void values '', ' ', False, None, + # email only + 'lenny.bar@gmail.com', ] expected = [ ('Raoul', 'raoul@grosbedon.fr'), @@ -493,6 +495,8 @@ def test_name_create_corner_cases(self): ('False', False), # (simili) void values: always False ('', False), ('', False), ('', False), ('', False), + # email only: email used as both name and email + ('lenny.bar@gmail.com', 'lenny.bar@gmail.com') ] for (expected_name, expected_email), sample in zip(expected, samples): with self.subTest(sample=sample): From 176bfcb48bcee64ecfd9137f18b8e8b9c128e85a Mon Sep 17 00:00:00 2001 From: Biraj Khatiwada Date: Tue, 27 Aug 2024 22:30:41 +0000 Subject: [PATCH 15/15] [FIX] website_sale_stock_wishlist: "Temporarily out of stock" When the product has the "continue selling" box when "out-of-stock" checked, and you add the product to the wishlist from ecommerce, "Temporarily out of stock" message/warning will appear in the wishlist. To Reproduce on Runbot: 1. Make sure ecommerce module is installed 2. Go to storable product (for example: Cable Management Box) 3. In the sales tab, make sure the option "Continue Selling" for Out-of-Stock field is checked 4. Go to ecommerce, search for the product (here, let's search for Cable Management Box), click on it 5. Click on add to wishlist 6. Go to wishlist. 7. We'll see "Temporarily out of stock" message. But, we don't want this because we want to continue selling even if it's out of stock, and don't want to customer to get confused with the message. So, since the message/warning doesn't align with the concept of continue selling when out of stock, we want to get rid of the message/warning if the product has "continue selling" box checked. opw-4121929 closes odoo/odoo#178834 X-original-commit: f96458301f951369f55d427049287a1c0567de2e Signed-off-by: Antoine Vandevenne (anv) Signed-off-by: Biraj Khatiwada (bikh) --- .../views/website_sale_stock_wishlist_templates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_sale_stock_wishlist/views/website_sale_stock_wishlist_templates.xml b/addons/website_sale_stock_wishlist/views/website_sale_stock_wishlist_templates.xml index d6767fc2ca687..6a7a1f93c1dab 100644 --- a/addons/website_sale_stock_wishlist/views/website_sale_stock_wishlist_templates.xml +++ b/addons/website_sale_stock_wishlist/views/website_sale_stock_wishlist_templates.xml @@ -2,7 +2,7 @@