commit 9ae41c29a2ec4b8eea3876c916859e3174e84a1d Author: Данил Воробьев Date: Fri May 3 12:03:07 2024 +0000 initial commit diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9ab5f17 --- /dev/null +++ b/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from . import models +from . import controllers +from . import websocket diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..d29765f --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,40 @@ +{ + 'name' : 'IM Bus', + 'version': '1.0', + 'category': 'Hidden', + 'description': "Instant Messaging Bus allow you to send messages to users, in live.", + 'depends': ['base', 'web'], + 'data': [ + 'security/ir.model.access.csv', + ], + 'installable': True, + 'auto_install': True, + 'assets': { + 'web.assets_backend': [ + 'bus/static/src/*.js', + 'bus/static/src/services/**/*.js', + 'bus/static/src/workers/websocket_worker.js', + 'bus/static/src/workers/websocket_worker_utils.js', + ], + 'web.assets_frontend': [ + 'bus/static/src/*.js', + 'bus/static/src/services/**/*.js', + ('remove', 'bus/static/src/services/assets_watchdog_service.js'), + ('remove', 'bus/static/src/simple_notification_service.js'), + 'bus/static/src/workers/websocket_worker.js', + 'bus/static/src/workers/websocket_worker_utils.js', + ], + 'web.tests_assets': [ + 'bus/static/tests/helpers/**/*', + ], + 'web.qunit_suite_tests': [ + 'bus/static/tests/**/*.js', + ('remove', 'bus/static/tests/helpers/**/*'), + ], + 'bus.websocket_worker_assets': [ + 'web/static/src/module_loader.js', + 'bus/static/src/workers/*', + ], + }, + 'license': 'LGPL-3', +} diff --git a/__pycache__/__init__.cpython-311.pyc b/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..6759313 Binary files /dev/null and b/__pycache__/__init__.cpython-311.pyc differ diff --git a/__pycache__/websocket.cpython-311.pyc b/__pycache__/websocket.cpython-311.pyc new file mode 100644 index 0000000..7f22b6c Binary files /dev/null and b/__pycache__/websocket.cpython-311.pyc differ diff --git a/controllers/__init__.py b/controllers/__init__.py new file mode 100644 index 0000000..33497e4 --- /dev/null +++ b/controllers/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import home +from . import main +from . import websocket diff --git a/controllers/__pycache__/__init__.cpython-311.pyc b/controllers/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..862a994 Binary files /dev/null and b/controllers/__pycache__/__init__.cpython-311.pyc differ diff --git a/controllers/__pycache__/home.cpython-311.pyc b/controllers/__pycache__/home.cpython-311.pyc new file mode 100644 index 0000000..aaeef25 Binary files /dev/null and b/controllers/__pycache__/home.cpython-311.pyc differ diff --git a/controllers/__pycache__/main.cpython-311.pyc b/controllers/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..8164db9 Binary files /dev/null and b/controllers/__pycache__/main.cpython-311.pyc differ diff --git a/controllers/__pycache__/websocket.cpython-311.pyc b/controllers/__pycache__/websocket.cpython-311.pyc new file mode 100644 index 0000000..257ccb1 Binary files /dev/null and b/controllers/__pycache__/websocket.cpython-311.pyc differ diff --git a/controllers/home.py b/controllers/home.py new file mode 100644 index 0000000..c3cb0d6 --- /dev/null +++ b/controllers/home.py @@ -0,0 +1,33 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import ipaddress + +from odoo import _, SUPERUSER_ID +from odoo.http import request +from odoo.addons.web.controllers.home import Home as WebHome + + +def _admin_password_warn(uid): + if request.params['password'] != 'admin': + return + if ipaddress.ip_address(request.httprequest.remote_addr).is_private: + return + env = request.env(user=SUPERUSER_ID, su=True) + admin = env.ref('base.partner_admin') + if uid not in admin.user_ids.ids: + return + has_demo = bool(env['ir.module.module'].search_count([('demo', '=', True)])) + if has_demo: + return + user = request.env(user=uid)['res.users'] + env(context=user.context_get())['bus.bus']._sendone(admin, 'simple_notification', { + 'type': 'danger', + 'message': _("Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!"), + 'sticky': True, + }) + + +class Home(WebHome): + def _login_redirect(self, uid, redirect=None): + if request.params.get('login_success'): + _admin_password_warn(uid) + return super()._login_redirect(uid, redirect) diff --git a/controllers/main.py b/controllers/main.py new file mode 100644 index 0000000..230acaa --- /dev/null +++ b/controllers/main.py @@ -0,0 +1,13 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json + +from odoo.http import Controller, request, route + + +class BusController(Controller): + @route('/bus/get_model_definitions', methods=['POST'], type='http', auth='user') + def get_model_definitions(self, model_names_to_fetch, **kwargs): + return request.make_response(json.dumps( + request.env['ir.model']._get_model_definitions(json.loads(model_names_to_fetch)), + )) diff --git a/controllers/websocket.py b/controllers/websocket.py new file mode 100644 index 0000000..2056b37 --- /dev/null +++ b/controllers/websocket.py @@ -0,0 +1,64 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json + +from odoo.http import Controller, request, route, SessionExpiredException +from odoo.addons.base.models.assetsbundle import AssetsBundle +from ..models.bus import channel_with_db +from ..websocket import WebsocketConnectionHandler + + +class WebsocketController(Controller): + @route('/websocket', type="http", auth="public", cors='*', websocket=True) + def websocket(self): + """ + Handle the websocket handshake, upgrade the connection if + successfull. + """ + return WebsocketConnectionHandler.open_connection(request) + + @route('/websocket/health', type='http', auth='none', save_session=False) + def health(self): + data = json.dumps({ + 'status': 'pass', + }) + headers = [('Content-Type', 'application/json'), + ('Cache-Control', 'no-store')] + return request.make_response(data, headers) + + @route('/websocket/peek_notifications', type='json', auth='public', cors='*') + def peek_notifications(self, channels, last, is_first_poll=False): + if not all(isinstance(c, str) for c in channels): + raise ValueError("bus.Bus only string channels are allowed.") + if is_first_poll: + # Used to detect when the current session is expired. + request.session['is_websocket_session'] = True + elif 'is_websocket_session' not in request.session: + raise SessionExpiredException() + channels = list(set( + channel_with_db(request.db, c) + for c in request.env['ir.websocket']._build_bus_channel_list(channels) + )) + last_known_notification_id = request.env['bus.bus'].sudo().search([], limit=1, order='id desc').id or 0 + if last > last_known_notification_id: + last = 0 + notifications = request.env['bus.bus']._poll(channels, last) + return {'channels': channels, 'notifications': notifications} + + @route('/websocket/update_bus_presence', type='json', auth='public', cors='*') + def update_bus_presence(self, inactivity_period, im_status_ids_by_model): + if 'is_websocket_session' not in request.session: + raise SessionExpiredException() + request.env['ir.websocket']._update_bus_presence(int(inactivity_period), im_status_ids_by_model) + return {} + + @route('/bus/websocket_worker_bundle', type='http', auth='public', cors='*') + def get_websocket_worker_bundle(self, v=None): # pylint: disable=unused-argument + """ + :param str v: Version of the worker, frontend only argument used to + prevent new worker versions to be loaded from the browser cache. + """ + bundle_name = 'bus.websocket_worker_assets' + bundle = request.env["ir.qweb"]._get_asset_bundle(bundle_name, debug_assets="assets" in request.session.debug) + stream = request.env['ir.binary']._get_stream_from(bundle.js()) + return stream.get_response() diff --git a/i18n/af.po b/i18n/af.po new file mode 100644 index 0000000..084dc17 --- /dev/null +++ b/i18n/af.po @@ -0,0 +1,150 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux, 2022 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0beta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2022-09-22 05:45+0000\n" +"Last-Translator: Martin Trigaux, 2022\n" +"Language-Team: Afrikaans (https://app.transifex.com/odoo/teams/41243/af/)\n" +"Language: af\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontak" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Geskep deur" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Geskep op" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Vertoningsnaam" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Laas Opgedateer deur" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Laas Opgedateer op" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Boodskap" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Gebruiker" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Gebruikers" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/am.po b/i18n/am.po new file mode 100644 index 0000000..a680c8c --- /dev/null +++ b/i18n/am.po @@ -0,0 +1,146 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0beta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2022-09-22 05:45+0000\n" +"Language-Team: Amharic (https://app.transifex.com/odoo/teams/41243/am/)\n" +"Language: am\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/ar.po b/i18n/ar.po new file mode 100644 index 0000000..f21d038 --- /dev/null +++ b/i18n/ar.po @@ -0,0 +1,157 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ar\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "بعيد" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "القناة" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "ناقل الاتصالات" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "جهة الاتصال" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "أنشئ بواسطة" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "أنشئ في" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "اسم العرض " + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "المُعرف" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "حالة المحادثات الفورية" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "آخر استطلاع" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "آخر حضور" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "آخر تحديث بواسطة" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "آخر تحديث في" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "الرسالة" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "النماذج" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "غير متصل بالإنترنت " + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "عبر الإنترنت " + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "تحديث " + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "يبدو أن هذه الصفحة قديمة. " + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "المستخدم" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "وجود المستخدم" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "المستخدمون" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"كلمة المرور الخاصة بك هي كلمة المرور الافتراضية (admin)! إذا كان هذا النظام " +"معرضااً للمستخدمين غير الموثوقين، من المهم تغييرها فورا لأسباب أمنية. سأظل " +"ألح عليك حول هذا الموضوع! " + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "التعامل مع رسائل websocket " diff --git a/i18n/az.po b/i18n/az.po new file mode 100644 index 0000000..13cca66 --- /dev/null +++ b/i18n/az.po @@ -0,0 +1,151 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# erpgo translator , 2022 +# Jumshud Sultanov , 2022 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0beta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2022-09-22 05:45+0000\n" +"Last-Translator: Jumshud Sultanov , 2022\n" +"Language-Team: Azerbaijani (https://app.transifex.com/odoo/teams/41243/az/)\n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Uzaq" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Kommunikasiya Şini" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Tərəfindən yaradılıb" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Tarixdə yaradıldı" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Ekran Adı" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Son Sorğu- sual" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Son İştirak" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Son Yeniləyən" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Son Yenilənmə tarixi" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Mesaj" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modellər" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Oflayn" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Onlayn" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "İstifadəçi" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "İstifadəçi İştirakı" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "İstifadəçilər" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/bg.po b/i18n/bg.po new file mode 100644 index 0000000..8ff14ce --- /dev/null +++ b/i18n/bg.po @@ -0,0 +1,157 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# KeyVillage, 2023 +# Maria Boyadjieva , 2023 +# aleksandar ivanov, 2023 +# Albena Mincheva , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Albena Mincheva , 2023\n" +"Language-Team: Bulgarian (https://app.transifex.com/odoo/teams/41243/bg/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: bg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Извън" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Канал" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Контакт" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Създадено от" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Създадено на" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Име за Показване" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Статус IM " + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Последна анкета" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Последно присъствие" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Последно актуализирано от" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Последно актуализирано на" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Съобщение" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Модели" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Офлайн" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Онлайн" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Потребител" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Потребителско присъствие" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Потребители" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/bs.po b/i18n/bs.po new file mode 100644 index 0000000..e34b95a --- /dev/null +++ b/i18n/bs.po @@ -0,0 +1,151 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux, 2018 +# Boško Stojaković , 2018 +# Bole , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server saas~11.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2018-09-21 13:17+0000\n" +"Last-Translator: Bole , 2018\n" +"Language-Team: Bosnian (https://www.transifex.com/odoo/teams/41243/bs/)\n" +"Language: bs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Poruka" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Van mreže" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Na mreži" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Korisnici" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/bus.pot b/i18n/bus.pot new file mode 100644 index 0000000..de0896f --- /dev/null +++ b/i18n/bus.pot @@ -0,0 +1,150 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 21:55+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/ca.po b/i18n/ca.po new file mode 100644 index 0000000..a256dd9 --- /dev/null +++ b/i18n/ca.po @@ -0,0 +1,165 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Arnau Ros, 2023 +# Cristian Cruz, 2023 +# Sandra Franch , 2023 +# Óscar Fonseca , 2023 +# marcescu, 2023 +# Martin Trigaux, 2023 +# RGB Consulting , 2023 +# Jonatan Gk, 2023 +# Quim - eccit , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Quim - eccit , 2023\n" +"Language-Team: Catalan (https://app.transifex.com/odoo/teams/41243/ca/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ca\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Absent" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Canal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Bus de comunicació " + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Contacte" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creat per" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Creat el" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nom mostrat" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Estat de la conversa" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Última conversa" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Última presencia" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Última actualització per" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Última actualització el" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Missatge" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Models" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Fora de línia" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "En línia" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Refresca" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Sembla que la pàgina està desactualitzada." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Usuari" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Presència del usuari" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Usuaris" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"La vostra contrasenya és per omissió (admin)! Si aquest sistema està exposat" +" als usuaris que no són de confiança és important canviar-lo immediatament " +"per motius de seguretat. Et continuaré molestant!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "gestió de missatges websocket" diff --git a/i18n/cs.po b/i18n/cs.po new file mode 100644 index 0000000..ce17eab --- /dev/null +++ b/i18n/cs.po @@ -0,0 +1,160 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Jakub Smolka, 2023 +# Ivana Bartonkova, 2023 +# Wil Odoo, 2023 +# karolína schusterová , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: karolína schusterová , 2023\n" +"Language-Team: Czech (https://app.transifex.com/odoo/teams/41243/cs/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: cs\n" +"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Pryč" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanál" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Komunikační sběrnice" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Vytvořeno uživatelem" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Vytvořeno dne" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Zobrazovací název" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM Status" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Poslední průzkum" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Poslední přítomnost" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Naposledy upraveno uživatelem" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Naposledy upraveno dne" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Zpráva" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modely" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Nepřipojeno" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Obnovit" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Stránka se zdá být zastaralá." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Uživatel" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Přítomnost uživatele" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Uživatelé" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Vaše heslo je výchozí (admin)! Pokud je tento systém vystaven nedůvěryhodným" +" uživatelům, je důležité jej z bezpečnostních důvodů okamžitě změnit. Budu " +"vás o tom pořád otravovat!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "zpráva zpracování websocketu" diff --git a/i18n/da.po b/i18n/da.po new file mode 100644 index 0000000..dd10e9a --- /dev/null +++ b/i18n/da.po @@ -0,0 +1,157 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Martin Trigaux, 2023\n" +"Language-Team: Danish (https://app.transifex.com/odoo/teams/41243/da/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: da\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Væk" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Kommunikations Bus" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Oprettet af" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Oprettet den" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Vis navn" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM status" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Seneste meningsmåling" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Sidste tilstedeværelse" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Sidst opdateret af" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Sidst opdateret den" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Besked" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modeller" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Offline" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Genopfrisk" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Siden ser ud til at være forældet." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Bruger" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Bruger tilstedeværelse" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Brugere" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Dit kodeord er sat til standard (admin)! Det er vigtigt, at du ændre det med" +" det samme, af sikkerhedsmæssige årsager, hvis dette system skulle udsættes " +"for tvivlsomme brugere. Jeg bliver ved med at irritere dig om det!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/de.po b/i18n/de.po new file mode 100644 index 0000000..9dbf0f9 --- /dev/null +++ b/i18n/de.po @@ -0,0 +1,158 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: German (https://app.transifex.com/odoo/teams/41243/de/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Abwesend" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Kommunikationsbus" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Erstellt von" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Erstellt am" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Anzeigename" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Status der Sofortnachricht" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Letzte Befragung" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Letzte Anwesenheit" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Zuletzt aktualisiert von" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Zuletzt aktualisiert am" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Nachricht" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modelle" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Offline" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Aktualisieren" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Die Seite scheint veraltet zu sein." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Benutzer" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Benutzer-Anwesenheit" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Benutzer" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Ihr Passwort ist das Standardpasswort (admin)! Wenn dieses System für nicht " +"vertrauenswürdige Benutzer zugänglich ist, müssen Sie es aus " +"Sicherheitsgründen sofort ändern. Ich werde Sie immer wieder darauf " +"hinweisen!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "Websocket-Nachrichtbearbeitung" diff --git a/i18n/el.po b/i18n/el.po new file mode 100644 index 0000000..46ecdc0 --- /dev/null +++ b/i18n/el.po @@ -0,0 +1,151 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux, 2018 +# Kostas Goutoudis , 2018 +# George Tarasidis , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server saas~11.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2018-09-21 13:17+0000\n" +"Last-Translator: George Tarasidis , 2018\n" +"Language-Team: Greek (https://www.transifex.com/odoo/teams/41243/el/)\n" +"Language: el\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Εκτός" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Κανάλι" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Επαφή" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Δημιουργήθηκε από" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Δημιουργήθηκε στις" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Εμφάνιση Ονόματος" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "Κωδικός" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Κατάσταση Άμεσης Συνομιλίας" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Τελευταία Δημοσκόπηση" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Τελευταία Παρουσία" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Τελευταία Ενημέρωση από" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Τελευταία Ενημέρωση στις" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Μήνυμα" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Offline" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Παρουσία Χρήστη" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Χρήστες" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/en_AU.po b/i18n/en_AU.po new file mode 100644 index 0000000..4983f65 --- /dev/null +++ b/i18n/en_AU.po @@ -0,0 +1,148 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Odoo 9.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2015-09-07 16:42+0000\n" +"Last-Translator: Martin Trigaux\n" +"Language-Team: English (Australia) (http://www.transifex.com/odoo/odoo-9/language/en_AU/)\n" +"Language: en_AU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Created by" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Display Name" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Last Updated by" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Last Updated on" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/en_GB.po b/i18n/en_GB.po new file mode 100644 index 0000000..49a3de6 --- /dev/null +++ b/i18n/en_GB.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: English (United Kingdom) (https://www.transifex.com/odoo/teams/41243/en_GB/)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Created by" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Display Name" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Last Updated by" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Last Updated on" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/es.po b/i18n/es.po new file mode 100644 index 0000000..5dbec74 --- /dev/null +++ b/i18n/es.po @@ -0,0 +1,158 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# Larissa Manderfeld, 2024 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Larissa Manderfeld, 2024\n" +"Language-Team: Spanish (https://app.transifex.com/odoo/teams/41243/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Ausente" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Canal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Bus de comunicación" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Contacto" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Estado de mensajería instantanea" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Última encuesta" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Última conexión" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Mensaje" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modelos" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Desconectado" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "En línea" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Actualizar" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Parece que la página no está actualizada." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Usuario" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Usuario conectado" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Usuarios" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"¡Tu contraseña es la predeterminada (admin)! Si este sistema está expuesto a" +" usuarios no confiables, es importante cambiarlo de inmediato por razones de" +" seguridad. ¡Te seguiré molestando al respecto!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "gestión de mensajes de WebSocket" diff --git a/i18n/es_419.po b/i18n/es_419.po new file mode 100644 index 0000000..1e02a10 --- /dev/null +++ b/i18n/es_419.po @@ -0,0 +1,158 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# Fernanda Alvarez, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Fernanda Alvarez, 2023\n" +"Language-Team: Spanish (Latin America) (https://app.transifex.com/odoo/teams/41243/es_419/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_419\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Ausente" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Canal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Bus de comunicación" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Contacto" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nombre en pantalla" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Estado de mensajería instantanea" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Última encuesta" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Última conexión" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Mensaje" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modelos" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Desconectado" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "En línea" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Actualizar" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Parece que la página no está actualizada." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Usuario" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Conexión del usuario" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Usuarios" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Está usando la contraseña predeterminada (admin). Si se expone este sistema " +"a usuarios no confiables, es importante que la cambie inmediatamente por " +"motivos de seguridad. Seguiré advirtiéndole al respecto." + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "gestión de mensajes de WebSocket" diff --git a/i18n/es_BO.po b/i18n/es_BO.po new file mode 100644 index 0000000..34554c8 --- /dev/null +++ b/i18n/es_BO.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Spanish (Bolivia) (https://www.transifex.com/odoo/teams/41243/es_BO/)\n" +"Language: es_BO\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Última actualización de" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/es_CL.po b/i18n/es_CL.po new file mode 100644 index 0000000..872890a --- /dev/null +++ b/i18n/es_CL.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Spanish (Chile) (https://www.transifex.com/odoo/teams/41243/es_CL/)\n" +"Language: es_CL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Última actualización de" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/es_CO.po b/i18n/es_CO.po new file mode 100644 index 0000000..e715508 --- /dev/null +++ b/i18n/es_CO.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Spanish (Colombia) (https://www.transifex.com/odoo/teams/41243/es_CO/)\n" +"Language: es_CO\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nombre Público" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Actualizado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Actualizado" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/es_CR.po b/i18n/es_CR.po new file mode 100644 index 0000000..3c839dd --- /dev/null +++ b/i18n/es_CR.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Spanish (Costa Rica) (https://www.transifex.com/odoo/teams/41243/es_CR/)\n" +"Language: es_CR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/es_DO.po b/i18n/es_DO.po new file mode 100644 index 0000000..aad6d68 --- /dev/null +++ b/i18n/es_DO.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Spanish (Dominican Republic) (https://www.transifex.com/odoo/teams/41243/es_DO/)\n" +"Language: es_DO\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Última actualización de" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/es_EC.po b/i18n/es_EC.po new file mode 100644 index 0000000..bf991ad --- /dev/null +++ b/i18n/es_EC.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Spanish (Ecuador) (https://www.transifex.com/odoo/teams/41243/es_EC/)\n" +"Language: es_EC\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creado por:" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nombre a Mostrar" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Ultima Actualización por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Actualizado en" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/es_PA.po b/i18n/es_PA.po new file mode 100644 index 0000000..7127dc1 --- /dev/null +++ b/i18n/es_PA.po @@ -0,0 +1,148 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Odoo 9.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2015-09-07 16:42+0000\n" +"Last-Translator: Martin Trigaux\n" +"Language-Team: Spanish (Panama) (http://www.transifex.com/odoo/odoo-9/language/es_PA/)\n" +"Language: es_PA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Última actualización de" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/es_PE.po b/i18n/es_PE.po new file mode 100644 index 0000000..76a40d1 --- /dev/null +++ b/i18n/es_PE.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Spanish (Peru) (https://www.transifex.com/odoo/teams/41243/es_PE/)\n" +"Language: es_PE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nombre a Mostrar" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Actualizado última vez por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Ultima Actualización" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/es_PY.po b/i18n/es_PY.po new file mode 100644 index 0000000..c35466d --- /dev/null +++ b/i18n/es_PY.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Spanish (Paraguay) (https://www.transifex.com/odoo/teams/41243/es_PY/)\n" +"Language: es_PY\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Ultima actualización por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Ultima actualización en" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/es_VE.po b/i18n/es_VE.po new file mode 100644 index 0000000..eaec0a9 --- /dev/null +++ b/i18n/es_VE.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Spanish (Venezuela) (https://www.transifex.com/odoo/teams/41243/es_VE/)\n" +"Language: es_VE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Mostrar nombre" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Última actualización realizada por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Ultima actualizacion en" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/et.po b/i18n/et.po new file mode 100644 index 0000000..332d496 --- /dev/null +++ b/i18n/et.po @@ -0,0 +1,162 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# JanaAvalah, 2023 +# Eneli Õigus , 2023 +# Marek Pontus, 2023 +# Maidu Targama , 2023 +# Arma Gedonsky , 2023 +# Triine Aavik , 2023 +# Anna, 2023 +# Leaanika Randmets, 2023 +# Martin Aavastik , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Martin Aavastik , 2023\n" +"Language-Team: Estonian (https://app.transifex.com/odoo/teams/41243/et/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: et\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Eemal" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Loodud (kelle poolt?)" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Loodud" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Kuvatav nimi" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM Status" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Last Poll" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Last Presence" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Viimati uuendatud" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Viimati uuendatud" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Sõnum" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Mudelid" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Võrguühenduseta" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Uuenda" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Leht on aegunud. " + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Kasutaja" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Kasutaja kohalolek" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Kasutajad" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "websocket sõnumite käsitlus" diff --git a/i18n/eu.po b/i18n/eu.po new file mode 100644 index 0000000..04c218a --- /dev/null +++ b/i18n/eu.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Basque (https://www.transifex.com/odoo/teams/41243/eu/)\n" +"Language: eu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Nork sortua" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Izena erakutsi" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Last Updated by" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Last Updated on" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/fa.po b/i18n/fa.po new file mode 100644 index 0000000..434eb09 --- /dev/null +++ b/i18n/fa.po @@ -0,0 +1,164 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# F Hariri , 2023 +# Yousef Shadmanesh , 2023 +# Hanna Kheradroosta, 2023 +# Hamid Darabi, 2023 +# Mohsen Mohammadi , 2023 +# Hamed Mohammadi , 2023 +# Martin Trigaux, 2023 +# Mohammad Tahmasebi , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Mohammad Tahmasebi , 2023\n" +"Language-Team: Persian (https://app.transifex.com/odoo/teams/41243/fa/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fa\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "دور از کار" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "کانال" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "مخاطب" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "ایجاد شده توسط" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "ایجادشده در" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "نام نمایش داده شده" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "شناسه" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "وضعیت پیام رسانی" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "آخرین رای‌گیری" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "آخرین حضور" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "آخرین بروز رسانی توسط" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "آخرین بروز رسانی در" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "پیام" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "مدل ها" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "آفلاین" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "آنلاین" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "تازه سازی" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "صفحه به نظر می‌رسد قدیمی است." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "کاربر" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "حضور کاربر" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "کاربران" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"پسورد شما هنوز همان پسوورد دیفالت (admin) است! اگر این سیستم در دسترس افراد " +"غیر معتمد است، آن را به سرعت تغییر دهید. من در این مورد دوباره هشدار خواهم " +"داد." + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/fi.po b/i18n/fi.po new file mode 100644 index 0000000..41daa88 --- /dev/null +++ b/i18n/fi.po @@ -0,0 +1,163 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Veikko Väätäjä , 2023 +# Kari Lindgren , 2023 +# Martin Trigaux, 2023 +# Miku Laitinen , 2023 +# Tuomo Aura , 2023 +# Jarmo Kortetjärvi , 2023 +# Ossi Mantylahti , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Ossi Mantylahti , 2023\n" +"Language-Team: Finnish (https://app.transifex.com/odoo/teams/41243/fi/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fi\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Poissa" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanava" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Viestintäväylä" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakti" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Luonut" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Luotu" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Näyttönimi" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Pikaviestimen tila" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Viimeisin kysely" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Viimeksi läsnä" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Viimeksi päivittänyt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Viimeksi päivitetty" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Viesti" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Mallit" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Poissa" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Verkossa" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Päivitä" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Sivu näyttää vanhentuneelta." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Käyttäjä" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Käyttäjän läsnäolo" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Käyttäjät" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Salasanasi on oletussalasana (admin)! Jos tämä järjestelmä on alttiina " +"epäluotettaville käyttäjille, on tärkeää vaihtaa se välittömästi " +"turvallisuussyistä. Jatkan siitä nalkuttamista!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "websocket-viestien käsittely" diff --git a/i18n/fo.po b/i18n/fo.po new file mode 100644 index 0000000..6672f80 --- /dev/null +++ b/i18n/fo.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Faroese (https://www.transifex.com/odoo/teams/41243/fo/)\n" +"Language: fo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Byrjað av" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Vís navn" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Seinast dagført av" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Seinast dagført tann" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po new file mode 100644 index 0000000..0889e6f --- /dev/null +++ b/i18n/fr.po @@ -0,0 +1,157 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: French (https://app.transifex.com/odoo/teams/41243/fr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fr\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Absent" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Chaîne" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Bus de communication" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Contact" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nom d'affichage" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Statut de messagerie instantanée" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Dernier sondage" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Dernière présence en ligne" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Mis à jour par" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Mis à jour le" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Message" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modèles" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Hors ligne" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "En ligne" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Actualiser" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "La page semble obsolète." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Utilisateur" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Présence de l'utilisateur" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Utilisateurs" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Votre mot de passe est celui par défaut (admin)! Si ce système est exposé à " +"des utilisateurs non fiables, il est important de le changer immédiatement " +"pour des raisons de sécurité. Je continuer à vous le rappeler !" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "traitement des messages du websocket " diff --git a/i18n/fr_BE.po b/i18n/fr_BE.po new file mode 100644 index 0000000..fe85000 --- /dev/null +++ b/i18n/fr_BE.po @@ -0,0 +1,148 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Odoo 9.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2015-09-07 16:42+0000\n" +"Last-Translator: Martin Trigaux\n" +"Language-Team: French (Belgium) (http://www.transifex.com/odoo/odoo-9/language/fr_BE/)\n" +"Language: fr_BE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Derniere fois mis à jour par" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Dernière mis à jour le" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Message" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Utilisateurs" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/fr_CA.po b/i18n/fr_CA.po new file mode 100644 index 0000000..2591131 --- /dev/null +++ b/i18n/fr_CA.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: French (Canada) (https://www.transifex.com/odoo/teams/41243/fr_CA/)\n" +"Language: fr_CA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "Identifiant" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/gl.po b/i18n/gl.po new file mode 100644 index 0000000..e6474f1 --- /dev/null +++ b/i18n/gl.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Galician (https://www.transifex.com/odoo/teams/41243/gl/)\n" +"Language: gl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Última actualización de" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/gu.po b/i18n/gu.po new file mode 100644 index 0000000..77d54e5 --- /dev/null +++ b/i18n/gu.po @@ -0,0 +1,150 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Qaidjohar Barbhaya, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0beta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2022-09-22 05:45+0000\n" +"Last-Translator: Qaidjohar Barbhaya, 2023\n" +"Language-Team: Gujarati (https://app.transifex.com/odoo/teams/41243/gu/)\n" +"Language: gu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Contact" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Created by" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Created on" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Display Name" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Last Updated by" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Last Updated on" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "સંદેશ" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "User" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "વપરાશકર્તાઓ" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/he.po b/i18n/he.po new file mode 100644 index 0000000..1f281f7 --- /dev/null +++ b/i18n/he.po @@ -0,0 +1,161 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# MichaelHadar, 2023 +# NoaFarkash, 2023 +# Fishfur A Banter , 2023 +# ZVI BLONDER , 2023 +# Lilach Gilliam , 2023 +# Yihya Hugirat , 2023 +# Martin Trigaux, 2023 +# Ha Ketem , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Ha Ketem , 2023\n" +"Language-Team: Hebrew (https://app.transifex.com/odoo/teams/41243/he/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: he\n" +"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "רחוק" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "ערוץ" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "אפיק תקשורת" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "איש קשר" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "נוצר על-ידי" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "נוצר ב-" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "שם לתצוגה" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "מזהה" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "סטטוס IM" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "סקר אחרון" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "נוכחות אחרונה" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "עודכן לאחרונה על-ידי" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "עדכון אחרון ב" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "הודעה" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "מודלים" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "לא מקוון" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "מקוון" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "רענן" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "נראה שהדף הינו מיושן." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "משתמש" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "נוכחות משתמש" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "משתמשים" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/hr.po b/i18n/hr.po new file mode 100644 index 0000000..1cf735c --- /dev/null +++ b/i18n/hr.po @@ -0,0 +1,153 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Vojislav Opačić , 2022 +# Martin Trigaux, 2022 +# Vladimir Olujić , 2022 +# Bole , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0beta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2022-09-22 05:45+0000\n" +"Last-Translator: Bole , 2023\n" +"Language-Team: Croatian (https://app.transifex.com/odoo/teams/41243/hr/)\n" +"Language: hr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Odsutan" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Kanal komunikacije" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Naziv" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM Status" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Zadnji pokušaj" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Zadnja prijava" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Promijenio" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Vrijeme promjene" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Poruka" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modeli" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Odspojen" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Osvježi" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Stranica se čini zastarjela." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Korisnik" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Prisutnost korisnika" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Korisnici" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "upravljanje porukama websocketa" diff --git a/i18n/hu.po b/i18n/hu.po new file mode 100644 index 0000000..6a8b0cf --- /dev/null +++ b/i18n/hu.po @@ -0,0 +1,159 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Tamás Dombos, 2023 +# Ákos Nagy , 2023 +# Martin Trigaux, 2023 +# Tamás Németh , 2023 +# gezza , 2023 +# krnkris, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: krnkris, 2023\n" +"Language-Team: Hungarian (https://app.transifex.com/odoo/teams/41243/hu/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: hu\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Távol" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Csatorna" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Kommunikációs busz" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kapcsolat" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Létrehozta" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Létrehozva" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Megjelenített név" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "Azonosító" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Üzenetküldési állapot" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Utolsó szavazás" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Utolsó jelenlét" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Frissítette" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Frissítve" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Üzenet" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modellek" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Offline" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Frissítés" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Felhasználó" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Felhasználói jelenlét" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Felhasználók" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/hy.po b/i18n/hy.po new file mode 100644 index 0000000..8be929f --- /dev/null +++ b/i18n/hy.po @@ -0,0 +1,150 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Language-Team: Armenian (https://app.transifex.com/odoo/teams/41243/hy/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: hy\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/id.po b/i18n/id.po new file mode 100644 index 0000000..959bd0c --- /dev/null +++ b/i18n/id.po @@ -0,0 +1,157 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: Indonesian (https://app.transifex.com/odoo/teams/41243/id/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: id\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Menjauh" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Saluran" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Bus Komunikasi" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontak" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Dibuat oleh" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Dibuat pada" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nama Tampilan" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Status IM" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Poll terakhir" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Kehadiran terakhir" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Terakhir Diperbarui oleh" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Terakhir Diperbarui pada" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Pesan" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Model" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Luring" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Daring" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Refresh" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Halaman tersebut tampaknya sudah usang." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Pengguna" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Kehadiran Pengguna" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Pengguna" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Password Anda adalah default (admin)! Bila sistem ini terbuka ke user yang " +"tidak dipercaya penting bagi Anda untuk langsung merubahnya untuk alasan " +"keamanan. Saya akan terus mengingatkan Anda mengenai hal ini!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "penanganan pesan websocket" diff --git a/i18n/is.po b/i18n/is.po new file mode 100644 index 0000000..dde83aa --- /dev/null +++ b/i18n/is.po @@ -0,0 +1,150 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Language-Team: Icelandic (https://app.transifex.com/odoo/teams/41243/is/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: is\n" +"Plural-Forms: nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/it.po b/i18n/it.po new file mode 100644 index 0000000..0ed9665 --- /dev/null +++ b/i18n/it.po @@ -0,0 +1,157 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: Italian (https://app.transifex.com/odoo/teams/41243/it/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: it\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Assente" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Canale" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Bus di comunicazione" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Contatto" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Data creazione" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Stato IM" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Ultimo poll" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Ultima presenza" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Messaggio" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modelli" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Fuori linea" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "In linea" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Ricarica" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "La pagina non sembra essere aggiornata." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Utente" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Presenza utente" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Utenti" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"La password risulta quella predefinita (admin). Se il sistema viene esposto " +"a utenti non fidati, è essenziale cambiarla immediatamente per motivi di " +"sicurezza. Tale avviso verrà riproposto in modo insistente." + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "Gestione dei messaggi websocket" diff --git a/i18n/ja.po b/i18n/ja.po new file mode 100644 index 0000000..fc30898 --- /dev/null +++ b/i18n/ja.po @@ -0,0 +1,156 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: Japanese (https://app.transifex.com/odoo/teams/41243/ja/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ja\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "外出" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "チャネル" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "コミュニケーションバス" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "連絡先" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "作成者" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "作成日" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "表示名" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IMステータス" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "最終返信" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "最終在席" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "最終更新者" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "最終更新日" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "メッセージ" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "モデル" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "オフライン" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "オンライン" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "リフレッシュ" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "ページが古くなっているようです。" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "ユーザ" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "ユーザプレゼンス" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "ユーザ" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"パスワードはデフォルト(管理者の)です! このシステムが信頼できないユーザーに公開されている場合、セキュリティ上の理由からすぐに変更することが重要です。" +" 私はそれについてあなたをしつこくリマインドします!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "WebSocketメッセージ処理" diff --git a/i18n/ka.po b/i18n/ka.po new file mode 100644 index 0000000..e41f5da --- /dev/null +++ b/i18n/ka.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Georgian (https://www.transifex.com/odoo/teams/41243/ka/)\n" +"Language: ka\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "შემქმნელი" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "სახელი" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "იდენტიფიკატორი" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "ბოლოს განაახლა" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "ბოლოს განახლებულია" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/kab.po b/i18n/kab.po new file mode 100644 index 0000000..1528ae3 --- /dev/null +++ b/i18n/kab.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Kabyle (https://www.transifex.com/odoo/teams/41243/kab/)\n" +"Language: kab\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Yerna-t" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "Asulay" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Aleqqem aneggaru sɣuṛ" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Aleqqem aneggaru di" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/km.po b/i18n/km.po new file mode 100644 index 0000000..bba3542 --- /dev/null +++ b/i18n/km.po @@ -0,0 +1,150 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Sengtha Chay , 2018 +# Chan Nath , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server saas~11.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2018-09-21 13:17+0000\n" +"Last-Translator: Chan Nath , 2018\n" +"Language-Team: Khmer (https://www.transifex.com/odoo/teams/41243/km/)\n" +"Language: km\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "មិននៅកន្លែង" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "ឆានែល" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "ទំនាក់ទំនង" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "បង្កើតដោយ" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "បង្កើតនៅ" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "ឈ្មោះសំរាប់បង្ហាញ" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "ផ្លាស់ប្តូរចុងក្រោយ" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "ផ្លាស់ប្តូរចុងក្រោយ" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "អ្នកប្រើ" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/ko.po b/i18n/ko.po new file mode 100644 index 0000000..f77cbce --- /dev/null +++ b/i18n/ko.po @@ -0,0 +1,156 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: Korean (https://app.transifex.com/odoo/teams/41243/ko/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ko\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "자리 비움" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "채널" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "커뮤니케이션 버스" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "연락처" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "작성자" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "작성일자" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "표시명" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "메신저 상태" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "최근 투표" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "최근 출석" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "최근 갱신한 사람" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "최근 갱신 일자" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "메시지" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "모델" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "오프라인" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "온라인" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "새로 고침" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "페이지가 오래된 것 같습니다." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "사용자" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "사용자 출석" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "사용자" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"비밀번호는 기본(admin)입니다! 이 시스템이 신뢰할 수 없는 사용자에게 노출된 경우 보안상의 이유로 즉시 변경해야합니다. 나는 이 " +"문제에 대해 계속 잔소리를 할겁니다!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "웹소켓 메시지 처리" diff --git a/i18n/lb.po b/i18n/lb.po new file mode 100644 index 0000000..2650ed8 --- /dev/null +++ b/i18n/lb.po @@ -0,0 +1,146 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server saas~12.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2019-08-26 09:09+0000\n" +"Language-Team: Luxembourgish (https://www.transifex.com/odoo/teams/41243/lb/)\n" +"Language: lb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/lo.po b/i18n/lo.po new file mode 100644 index 0000000..f89f065 --- /dev/null +++ b/i18n/lo.po @@ -0,0 +1,146 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Language-Team: Lao (https://www.transifex.com/odoo/teams/41243/lo/)\n" +"Language: lo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/lt.po b/i18n/lt.po new file mode 100644 index 0000000..be991e6 --- /dev/null +++ b/i18n/lt.po @@ -0,0 +1,161 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# grupoda2 , 2023 +# Arunas V. , 2023 +# Audrius Palenskis , 2023 +# UAB "Draugiški sprendimai" , 2023 +# Martin Trigaux, 2023 +# Jonas Zinkevicius , 2023 +# Linas Versada , 2023 +# Monika Raciunaite , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Monika Raciunaite , 2023\n" +"Language-Team: Lithuanian (https://app.transifex.com/odoo/teams/41243/lt/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: lt\n" +"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Išėjęs" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanalas" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Komunikacijos magistralė" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontaktas" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Sukūrė" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Sukurta" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Rodomas pavadinimas" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM būsena" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Paskutinė apklausa" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Paskutinį kartą matytas" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Paskutinį kartą atnaujino" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Paskutinį kartą atnaujinta" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Žinutė" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modeliai" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Atsijungęs" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Nuotoliniu Būdu" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Vartotojas" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Vartotojo aktyvumas" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Vartotojai" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/lv.po b/i18n/lv.po new file mode 100644 index 0000000..1ae007d --- /dev/null +++ b/i18n/lv.po @@ -0,0 +1,159 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Arnis Putniņš , 2023 +# Armīns Jeltajevs , 2023 +# ievaputnina , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: ievaputnina , 2023\n" +"Language-Team: Latvian (https://app.transifex.com/odoo/teams/41243/lv/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: lv\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Projām" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanāls" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontaktpersona" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Izveidoja" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Izveidots" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Attēlotais nosaukums" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Pēdējoreiz atjaunināja" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Pēdējoreiz atjaunināts" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Ziņojums" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modeļi" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Bezsaistē" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Tiešsaistē" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Atsvaidzināt" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Lietotājs" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Lietotāji" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Jūsu parole ir noklusējuma (administrators)! Ja šī sistēma ir pakļauta " +"neuzticamiem lietotājiem, drošības apsvērumu dēļ ir svarīgi to nekavējoties " +"nomainīt. Mēs Jums par to turpināsim atgādināt!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/mk.po b/i18n/mk.po new file mode 100644 index 0000000..1c747f9 --- /dev/null +++ b/i18n/mk.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Macedonian (https://www.transifex.com/odoo/teams/41243/mk/)\n" +"Language: mk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Креирано од" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Прикажи име" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Последно ажурирање од" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Последно ажурирање на" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/ml_IN.po b/i18n/ml_IN.po new file mode 100644 index 0000000..9029e5b --- /dev/null +++ b/i18n/ml_IN.po @@ -0,0 +1,148 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Odoo 9.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2016-04-22 12:13+0000\n" +"Last-Translator: Martin Trigaux\n" +"Language-Team: Malayalam (India) (http://www.transifex.com/odoo/odoo-9/language/ml_IN/)\n" +"Language: ml_IN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "രൂപപ്പെടുത്തിയത്" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "അവസാനം അപ്ഡേറ്റ് ചെയ്തത്" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "അവസാനം അപ്ഡേറ്റ് ചെയ്ത ദിവസം" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/mn.po b/i18n/mn.po new file mode 100644 index 0000000..5ec3a08 --- /dev/null +++ b/i18n/mn.po @@ -0,0 +1,152 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Minj P , 2022 +# Martin Trigaux, 2022 +# Батмөнх Ганбат , 2022 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0beta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2022-09-22 05:45+0000\n" +"Last-Translator: Батмөнх Ганбат , 2022\n" +"Language-Team: Mongolian (https://app.transifex.com/odoo/teams/41243/mn/)\n" +"Language: mn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Хол байна" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Суваг" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Харилцааны цуваа" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Харилцах хаяг" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Үүсгэсэн этгээд" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Үүсгэсэн огноо" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Дэлгэрэнгүй нэр" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "ШХ Төлөв" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Сүүлийн Санал" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Сүүлийн Оролцоо" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Сүүлд зассан этгээд" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Сүүлд зассан огноо" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Зурвас" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Модел" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Оффлайн" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Онлайн" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Хэрэглэгч" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Хэрэглэгчийн Оролцоо" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Хэрэглэгчид" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/nb.po b/i18n/nb.po new file mode 100644 index 0000000..db6a46d --- /dev/null +++ b/i18n/nb.po @@ -0,0 +1,151 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Marius Stedjan , 2022 +# Martin Trigaux, 2022 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0beta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2022-09-22 05:45+0000\n" +"Last-Translator: Martin Trigaux, 2022\n" +"Language-Team: Norwegian Bokmål (https://app.transifex.com/odoo/teams/41243/nb/)\n" +"Language: nb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Borte" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Opprettet av" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Opprettet" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Visningsnavn" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Chattestatus" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Sist kontaktet" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Sist tilstede" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Sist oppdatert av" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Sist oppdatert" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Melding" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modeller" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Frakoblet" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "På nett" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Bruker" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Bruker tilstede" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Brukere" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/ne.po b/i18n/ne.po new file mode 100644 index 0000000..ed95afc --- /dev/null +++ b/i18n/ne.po @@ -0,0 +1,146 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Language-Team: Nepali (https://www.transifex.com/odoo/teams/41243/ne/)\n" +"Language: ne\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/nl.po b/i18n/nl.po new file mode 100644 index 0000000..326d04f --- /dev/null +++ b/i18n/nl.po @@ -0,0 +1,157 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: Dutch (https://app.transifex.com/odoo/teams/41243/nl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Afwezig" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanaal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Communicatiebus" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Contact" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Aangemaakt door" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Aangemaakt op" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Schermnaam" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM Status" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Laatste pol" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Laatst aanwezig" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Laatst bijgewerkt door" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Laatst bijgewerkt op" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Bericht" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modellen" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Offline" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Vernieuwen" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "De pagina lijkt verouderd." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Gebruiker" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Gebruiker aanwezig" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Gebruikers" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Je wachtwoord is het standaard (admin)! Als dit systeem wordt blootgesteld " +"aan niet-vertrouwde gebruikers, is het belangrijk om het om " +"veiligheidsredenen onmiddellijk te wijzigen. Ik zal er over blijven zeuren!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "websocket berichtafhandeling" diff --git a/i18n/pl.po b/i18n/pl.po new file mode 100644 index 0000000..3aa9fe7 --- /dev/null +++ b/i18n/pl.po @@ -0,0 +1,157 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: Polish (https://app.transifex.com/odoo/teams/41243/pl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: pl\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Zaraz wracam" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanał" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Magistrala komunikacyjna" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Utworzył(a)" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Data utworzenia" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nazwa wyświetlana" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Status komunikatora" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Ostatnia ankieta" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Ostatnia obecność" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Ostatnio aktualizowane przez" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Data ostatniej aktualizacji" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Wiadomość" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modele" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Niedostępny" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Dostępny" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Odśwież" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Strona wydaje się być nieaktualna." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Użytkownik" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Obecność użytkownika" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Użytkownicy" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Twoje hasło jest domyślne (admin)! Jeśli ten system jest narażony na " +"niezaufanych użytkowników, należy go natychmiast zmienić ze względów " +"bezpieczeństwa. Będę Cię o to nękał!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "obsługa komunikatów websocket" diff --git a/i18n/pt.po b/i18n/pt.po new file mode 100644 index 0000000..6899d7a --- /dev/null +++ b/i18n/pt.po @@ -0,0 +1,154 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: Portuguese (https://app.transifex.com/odoo/teams/41243/pt/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: pt\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Ausente" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Canal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Contacto" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Criado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Criado em" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nome" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Estado IM" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Última Votação" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Última Presença" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Última Atualização por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Última Atualização em" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Mensagem" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modelos" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Desligado" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Utilizador" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Presença de utilizador" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Utilizadores" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/pt_BR.po b/i18n/pt_BR.po new file mode 100644 index 0000000..86acb58 --- /dev/null +++ b/i18n/pt_BR.po @@ -0,0 +1,158 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# Layna Nascimento, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Layna Nascimento, 2023\n" +"Language-Team: Portuguese (Brazil) (https://app.transifex.com/odoo/teams/41243/pt_BR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: pt_BR\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Ausente" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Canal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Barramento de comunicação" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Contato" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Criado por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Criado em" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nome exibido" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Status do mensageiro" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Última enquete" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Última presença" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Última atualização por" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Última atualização em" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Mensagem" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modelos" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Offline" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Atualizar" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "A página parece estar desatualizada." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Usuário" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Presença do usuário" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Usuários" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Sua senha é o padrão (admin). Se este sistema for exposto a usuários não " +"confiáveis, é importante alterá-la imediatamente por motivos de segurança. " +"Eu vou continuar te importunando acerca disso!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "Tratamento de mensagens via websocket" diff --git a/i18n/ro.po b/i18n/ro.po new file mode 100644 index 0000000..0e726d4 --- /dev/null +++ b/i18n/ro.po @@ -0,0 +1,153 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Dorin Hongu , 2022 +# sharkutz , 2022 +# Martin Trigaux, 2022 +# Foldi Robert , 2022 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0beta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2022-09-22 05:45+0000\n" +"Last-Translator: Foldi Robert , 2022\n" +"Language-Team: Romanian (https://app.transifex.com/odoo/teams/41243/ro/)\n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Absent" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Canal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Contact" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Creat de" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Creat în" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Nume afișat" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Status IM" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Ultimul sondaj" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Ultima prezență" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Ultima actualizare făcută de" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Ultima actualizare pe" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Mesaj" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modele" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Deconectat" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Activ" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Actualizare" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Pagina pare să nu fie actualizată." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Operator" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Prezență utilizator" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Utilizatori" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/ru.po b/i18n/ru.po new file mode 100644 index 0000000..c3cb1af --- /dev/null +++ b/i18n/ru.po @@ -0,0 +1,160 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# ILMIR , 2023 +# Alena Vlasova, 2023 +# Martin Trigaux, 2023 +# Wil Odoo, 2024 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2024\n" +"Language-Team: Russian (https://app.transifex.com/odoo/teams/41243/ru/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ru\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Нет на месте" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Канал" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Коммуникационная шина" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Контакты" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Создано" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Создано" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Отображаемое имя" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Статус IM" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Последний опрос" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Последнее присутствие" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Последнее обновление" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Последнее обновление" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Сообщение" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Модели" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Не в сети" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Онлайн" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Обновить" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Похоже, что эта страница устарела." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Пользователь" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Присутствие пользователя" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Пользователи" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Ваш пароль по умолчанию (admin)! Если эта система подвергается воздействию " +"недоверенных пользователей, важно немедленно изменить настройки по " +"соображениям безопасности. Я буду продолжать докучать Вам по этому поводу!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "обработка сообщений в сокетах" diff --git a/i18n/sk.po b/i18n/sk.po new file mode 100644 index 0000000..12a9462 --- /dev/null +++ b/i18n/sk.po @@ -0,0 +1,154 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: Slovak (https://app.transifex.com/odoo/teams/41243/sk/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: sk\n" +"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Neprítomný" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanál" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Komunikačná zbernica" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Vytvoril" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Vytvorené" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Zobrazovaný názov" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM status" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Posledný prieskum" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Posledná prítomnosť" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Naposledy upravoval" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Naposledy upravované" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Správa" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modely" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Offline" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Obnoviť" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Zdá sa, že stránka je zastaraná." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Užívateľ" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Užívateľova prítomnosť" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Užívatelia" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/sl.po b/i18n/sl.po new file mode 100644 index 0000000..f97a29d --- /dev/null +++ b/i18n/sl.po @@ -0,0 +1,157 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# laznikd , 2023 +# Matjaz Mozetic , 2023 +# matjaz k , 2023 +# Martin Trigaux, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Martin Trigaux, 2023\n" +"Language-Team: Slovenian (https://app.transifex.com/odoo/teams/41243/sl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: sl\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Odsoten" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Stik" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Ustvaril" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Ustvarjeno" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM status" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Zadnje glasovanje" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Zadnja prisotnost" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Zadnji posodobil" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Zadnjič posodobljeno" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Sporočilo" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modeli" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Brez povezave" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Spletno" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Uporabnik" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Uporabniki" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/sq.po b/i18n/sq.po new file mode 100644 index 0000000..fbb9c30 --- /dev/null +++ b/i18n/sq.po @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Martin Trigaux , 2017\n" +"Language-Team: Albanian (https://www.transifex.com/odoo/teams/41243/sq/)\n" +"Language: sq\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Krijuar nga" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Emri i paraqitur" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Modifikuar per here te fundit nga" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Modifikuar per here te fundit me" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/sr.po b/i18n/sr.po new file mode 100644 index 0000000..0848b7d --- /dev/null +++ b/i18n/sr.po @@ -0,0 +1,160 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux, 2023 +# Dragan Vukosavljevic , 2023 +# Milan Bojovic , 2023 +# コフスタジオ, 2024 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: コフスタジオ, 2024\n" +"Language-Team: Serbian (https://app.transifex.com/odoo/teams/41243/sr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: sr\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Daleko" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Komunikacioni Bus" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Naziv za prikaz" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM Status" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Poslednje istraživanje." + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Poslednja prisutnost" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Poslednji put ažurirao" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Poslednji put ažurirano" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Poruka" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modeli" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Offline" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Osveži" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Stranica izgleda zastarelo." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Korisnik" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Prisustvo korisnika" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Korisnici" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Vaša lozinka je podrazumevana (admin)! Ako je ovaj sistem izložen " +"nepoverljivim korisnicima, važno je da je odmah promenite iz bezbednosnih " +"razloga. Nastaviću da vas podsećam na to!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "websocket rukovanje porukama" diff --git a/i18n/sr@latin.po b/i18n/sr@latin.po new file mode 100644 index 0000000..00b9faf --- /dev/null +++ b/i18n/sr@latin.po @@ -0,0 +1,152 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Martin Trigaux , 2017 +# Djordje Marjanovic , 2017 +# Ljubisa Jovev , 2017 +# Nemanja Dragovic , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 09:53+0000\n" +"Last-Translator: Nemanja Dragovic , 2017\n" +"Language-Team: Serbian (Latin) (https://www.transifex.com/odoo/teams/41243/sr%40latin/)\n" +"Language: sr@latin\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Odsutan" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Naziv za prikaz" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM status" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Zadnja dojava" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Zadnji put prisutan" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Promenio" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Vreme promene" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Poruka" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Offline" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Prisustvo korisnika" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Korisnici" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/sv.po b/i18n/sv.po new file mode 100644 index 0000000..c14b7a5 --- /dev/null +++ b/i18n/sv.po @@ -0,0 +1,166 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Moa Nicklasson , 2023 +# Simon S, 2023 +# Robin Calvin, 2023 +# Jakob Krabbe , 2023 +# Simon Nilsson, 2023 +# Mikael Carlsson , 2023 +# Kim Asplund , 2023 +# Martin Trigaux, 2023 +# Chrille Hedberg , 2023 +# Anders Wallenquist , 2024 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Anders Wallenquist , 2024\n" +"Language-Team: Swedish (https://app.transifex.com/odoo/teams/41243/sv/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: sv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Borta" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Kommunikationsbuss" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontakt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Skapad av" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Skapad den" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Visningsnamn" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM-status" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Senaste undersökning" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Sågs senast" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Senast uppdaterad av" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Senast uppdaterad den" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Meddelande" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modeller" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Frånvarande" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Uppkopplad" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Ladda om" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Sidan verkar vara föråldrad." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Användare" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Användarens närvaro/status" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Användare" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Ditt lösenord är standard (admin)! Om systemet utsätts för otillförlitliga " +"användare är det av säkerhetsskäl viktigt att omedelbart ändra lösenordet. " +"Jag kommer att fortsätta tjata på dig om det!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "websocket meddelandehantering" diff --git a/i18n/ta.po b/i18n/ta.po new file mode 100644 index 0000000..11fe785 --- /dev/null +++ b/i18n/ta.po @@ -0,0 +1,148 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Odoo 9.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2016-02-11 12:51+0000\n" +"Last-Translator: Martin Trigaux\n" +"Language-Team: Tamil (http://www.transifex.com/odoo/odoo-9/language/ta/)\n" +"Language: ta\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "சேனல்" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "உருவாக்கியவர்" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "காட்சி பெயர்" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "கடைசியாக புதுப்பிக்கப்பட்டது" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "கடைசியாக புதுப்பிக்கப்பட்டது" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "பயனர்கள்" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!" +msgstr "" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/i18n/th.po b/i18n/th.po new file mode 100644 index 0000000..fbe28d3 --- /dev/null +++ b/i18n/th.po @@ -0,0 +1,158 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# Rasareeyar Lappiam, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Rasareeyar Lappiam, 2023\n" +"Language-Team: Thai (https://app.transifex.com/odoo/teams/41243/th/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: th\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "ห่างออกไป" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "ช่อง" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "รถบัสสื่อสาร" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "ติดต่อ" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "สร้างโดย" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "สร้างเมื่อ" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "แสดงชื่อ" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ไอดี" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "สถานะ IM" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "โพลครั้งล่าสุด" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "การแสดงตนครั้งสุดท้าย" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "อัปเดตครั้งล่าสุดโดย" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "อัปเดตครั้งล่าสุดเมื่อ" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "ข้อความ" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "โมเดล" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "ออฟไลน์" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "ออนไลน์" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "รีเฟรช" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "ดูเหมือนเพจจะล้าสมัยแล้ว" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "ผู้ใช้" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "การแสดงตนของผู้ใช้" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "ผู้ใช้" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"รหัสผ่านของคุณเป็นค่าเริ่มต้น (admin+)! " +"หากระบบนี้เปิดเผยต่อผู้ใช้ที่ไม่น่าเชื่อถือสิ่งสำคัญคือต้องเปลี่ยนทันทีด้วยเหตุผลด้านความปลอดภัย" +" ฉันจะจู้จี้คุณเกี่ยวกับเรื่องนี้!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "การจัดการข้อความของ websocket" diff --git a/i18n/tr.po b/i18n/tr.po new file mode 100644 index 0000000..f20d513 --- /dev/null +++ b/i18n/tr.po @@ -0,0 +1,166 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Ramiz Deniz Öner , 2023 +# abc Def , 2023 +# Nadir Gazioglu , 2023 +# Ediz Duman , 2023 +# Ozlem Cikrikci , 2023 +# Tugay Hatıl , 2023 +# Levent Karakaş , 2023 +# Martin Trigaux, 2023 +# Murat Durmuş , 2023 +# Murat Kaplan , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Murat Kaplan , 2023\n" +"Language-Team: Turkish (https://app.transifex.com/odoo/teams/41243/tr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: tr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Dışarıda" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kanal" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "İletişim Veriyolu" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Kontak" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Oluşturan" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Oluşturulma" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Görünüm Adı" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Anlık İleti Durumu" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Son Anket" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Son Durum" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Son Güncelleyen" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Son Güncelleme" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Mesaj" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Modeller" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Çevrimdışı" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Çevrimiçi" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Refresh" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Sayfa güncel değil gibi görünüyor." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Kullanıcı" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Kullanıcı Durumu" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Kullanıcılar" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Şifreniz varsayılan (admin)! Bu sistem güvenilmeyen kullanıcılara maruz " +"kalırsa, güvenlik nedenleriyle derhal değiştirilmesi önemlidir. Bu konuda " +"seni rahatsız etmeye devam edeceğim!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "websocket mesaj işleme" diff --git a/i18n/uk.po b/i18n/uk.po new file mode 100644 index 0000000..b98178a --- /dev/null +++ b/i18n/uk.po @@ -0,0 +1,158 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# Martin Trigaux, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Martin Trigaux, 2023\n" +"Language-Team: Ukrainian (https://app.transifex.com/odoo/teams/41243/uk/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: uk\n" +"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Відійшов" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Канал" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Bus комунікація" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Контакт" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Створив" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Створено" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Назва для відображення" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Статус у миттєвих повідомленнях" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Останнє опитування" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Остання присутність" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Востаннє оновив" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Останнє оновлення" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Повідомлення" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Моделі" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Офлайн" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Онлайн" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Оновити" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Сторінка, здається, застаріла." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Користувач" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Присутність користувача" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Користувачі" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Ваш пароль за замовчуванням (admin)! Якщо ця система наражається на " +"ненадійних користувачів, важливо її негайно змінити з міркувань безпеки. Ми " +"будемо тримати вас в курсі!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "обробка повідомлень websocket" diff --git a/i18n/vi.po b/i18n/vi.po new file mode 100644 index 0000000..2f9e79d --- /dev/null +++ b/i18n/vi.po @@ -0,0 +1,158 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# Thi Huong Nguyen, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Thi Huong Nguyen, 2023\n" +"Language-Team: Vietnamese (https://app.transifex.com/odoo/teams/41243/vi/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: vi\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "Vắng mặt" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "Kênh" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "Bus thông tin trao đổi" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "Liên hệ" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "Được tạo bởi" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "Được tạo vào" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "Tên hiển thị" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "Trạng thái IM" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "Lần thăm dò cuối cùng" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "Hiện diện lần cuối" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "Cập nhật lần cuối bởi" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "Cập nhật lần cuối vào" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "Thông báo" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "Mô hình" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "Offline" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "Online" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "Làm mới" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "Trang này có vẻ đã quá cũ." + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "Người dùng" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "Sự hiện diện của người dùng" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "Người dùng" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "" +"Mật khẩu của bạn đang là mặc định (admin)! Nếu có người dùng không đáng tin " +"cậy sử dụng hệ thống này, thì việc quan trọng là đổi mật khẩu ngay lập tức " +"để bảo mật thông tin. Chúng tôi sẽ tiếp tục nhắc bạn về việc này!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "xử lý thông báo websocket" diff --git a/i18n/zh_CN.po b/i18n/zh_CN.po new file mode 100644 index 0000000..bf77e9d --- /dev/null +++ b/i18n/zh_CN.po @@ -0,0 +1,154 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: Chinese (China) (https://app.transifex.com/odoo/teams/41243/zh_CN/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: zh_CN\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "离开" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "频道" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "通讯总线" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "联系人" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "创建人" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "创建日期" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "显示名称" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "ID" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM的状态" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "最后在线" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "最后登录" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "最后更新人" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "上次更新日期" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "消息" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "模型" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "离线" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "线上" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "刷新" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "‎该页面似乎已过期。‎" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "用户" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "用户上线" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "用户" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "您的密码是默认密码(admin)! 如果此系统暴露给不受信任的用户,则出于安全原因立即更改它很重要。 我会继续唠叨您!" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "websocket消息处理" diff --git a/i18n/zh_TW.po b/i18n/zh_TW.po new file mode 100644 index 0000000..6041e57 --- /dev/null +++ b/i18n/zh_TW.po @@ -0,0 +1,154 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bus +# +# Translators: +# Wil Odoo, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 21:55+0000\n" +"PO-Revision-Date: 2023-10-26 23:09+0000\n" +"Last-Translator: Wil Odoo, 2023\n" +"Language-Team: Chinese (Taiwan) (https://app.transifex.com/odoo/teams/41243/zh_TW/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__away +msgid "Away" +msgstr "離開" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__channel +msgid "Channel" +msgstr "群組" + +#. module: bus +#: model:ir.model,name:bus.model_bus_bus +msgid "Communication Bus" +msgstr "通信匯流排" + +#. module: bus +#: model:ir.model,name:bus.model_res_partner +msgid "Contact" +msgstr "聯絡人" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_uid +msgid "Created by" +msgstr "建立人員" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__create_date +msgid "Created on" +msgstr "建立於" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__display_name +#: model:ir.model.fields,field_description:bus.field_bus_presence__display_name +msgid "Display Name" +msgstr "顯示名稱" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__id +#: model:ir.model.fields,field_description:bus.field_bus_presence__id +msgid "ID" +msgstr "識別號" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__status +#: model:ir.model.fields,field_description:bus.field_res_partner__im_status +#: model:ir.model.fields,field_description:bus.field_res_users__im_status +msgid "IM Status" +msgstr "IM的狀態" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_poll +msgid "Last Poll" +msgstr "最後線上" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__last_presence +msgid "Last Presence" +msgstr "最後出席" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_uid +msgid "Last Updated by" +msgstr "最後更新者" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__write_date +msgid "Last Updated on" +msgstr "最後更新於" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_bus__message +msgid "Message" +msgstr "消息" + +#. module: bus +#: model:ir.model,name:bus.model_ir_model +msgid "Models" +msgstr "型號" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__offline +msgid "Offline" +msgstr "離線" + +#. module: bus +#: model:ir.model.fields.selection,name:bus.selection__bus_presence__status__online +msgid "Online" +msgstr "網上進行" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "Refresh" +msgstr "重新載入" + +#. module: bus +#. odoo-javascript +#: code:addons/bus/static/src/services/assets_watchdog_service.js:0 +#, python-format +msgid "The page appears to be out of date." +msgstr "該頁面似乎已逾時" + +#. module: bus +#: model:ir.model,name:bus.model_res_users +msgid "User" +msgstr "使用者" + +#. module: bus +#: model:ir.model,name:bus.model_bus_presence +msgid "User Presence" +msgstr "使用者出現" + +#. module: bus +#: model:ir.model.fields,field_description:bus.field_bus_presence__user_id +msgid "Users" +msgstr "使用者" + +#. module: bus +#. odoo-python +#: code:addons/bus/controllers/home.py:0 +#, python-format +msgid "" +"Your password is the default (admin)! If this system is exposed to untrusted" +" users it is important to change it immediately for security reasons. I will" +" keep nagging you about it!" +msgstr "您的密碼是預設(admin)!如果此系統向不受信任的使用者公開,出於安全原因,非常重要請立即更改! (將一直提醒!)" + +#. module: bus +#: model:ir.model,name:bus.model_ir_websocket +msgid "websocket message handling" +msgstr "WebSocket 訊息處理" diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..f309e7d --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from . import bus +from . import bus_presence +from . import ir_model +from . import ir_websocket +from . import res_users +from . import res_partner diff --git a/models/__pycache__/__init__.cpython-311.pyc b/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..c820604 Binary files /dev/null and b/models/__pycache__/__init__.cpython-311.pyc differ diff --git a/models/__pycache__/bus.cpython-311.pyc b/models/__pycache__/bus.cpython-311.pyc new file mode 100644 index 0000000..f21bb06 Binary files /dev/null and b/models/__pycache__/bus.cpython-311.pyc differ diff --git a/models/__pycache__/bus_presence.cpython-311.pyc b/models/__pycache__/bus_presence.cpython-311.pyc new file mode 100644 index 0000000..36a2347 Binary files /dev/null and b/models/__pycache__/bus_presence.cpython-311.pyc differ diff --git a/models/__pycache__/ir_model.cpython-311.pyc b/models/__pycache__/ir_model.cpython-311.pyc new file mode 100644 index 0000000..ed61bcc Binary files /dev/null and b/models/__pycache__/ir_model.cpython-311.pyc differ diff --git a/models/__pycache__/ir_websocket.cpython-311.pyc b/models/__pycache__/ir_websocket.cpython-311.pyc new file mode 100644 index 0000000..77e10c8 Binary files /dev/null and b/models/__pycache__/ir_websocket.cpython-311.pyc differ diff --git a/models/__pycache__/res_partner.cpython-311.pyc b/models/__pycache__/res_partner.cpython-311.pyc new file mode 100644 index 0000000..0c0b21b Binary files /dev/null and b/models/__pycache__/res_partner.cpython-311.pyc differ diff --git a/models/__pycache__/res_users.cpython-311.pyc b/models/__pycache__/res_users.cpython-311.pyc new file mode 100644 index 0000000..8b9195b Binary files /dev/null and b/models/__pycache__/res_users.cpython-311.pyc differ diff --git a/models/bus.py b/models/bus.py new file mode 100644 index 0000000..d3a3d8b --- /dev/null +++ b/models/bus.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +import contextlib +import datetime +import json +import logging +import math +import os +import random +import selectors +import threading +import time +from psycopg2 import InterfaceError, sql + +import odoo +from odoo import api, fields, models +from odoo.service.server import CommonServer +from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT +from odoo.tools import date_utils + +_logger = logging.getLogger(__name__) + +# longpolling timeout connection +TIMEOUT = 50 + +# custom function to call instead of default PostgreSQL's `pg_notify` +ODOO_NOTIFY_FUNCTION = os.getenv('ODOO_NOTIFY_FUNCTION', 'pg_notify') + + +def get_notify_payload_max_length(default=8000): + try: + length = int(os.environ.get('ODOO_NOTIFY_PAYLOAD_MAX_LENGTH', default)) + except ValueError: + _logger.warning("ODOO_NOTIFY_PAYLOAD_MAX_LENGTH has to be an integer, " + "defaulting to %d bytes", default) + length = default + return length + + +# max length in bytes for the NOTIFY query payload +NOTIFY_PAYLOAD_MAX_LENGTH = get_notify_payload_max_length() + + +#---------------------------------------------------------- +# Bus +#---------------------------------------------------------- +def json_dump(v): + return json.dumps(v, separators=(',', ':'), default=date_utils.json_default) + +def hashable(key): + if isinstance(key, list): + key = tuple(key) + return key + + +def channel_with_db(dbname, channel): + if isinstance(channel, models.Model): + return (dbname, channel._name, channel.id) + if isinstance(channel, tuple) and len(channel) == 2 and isinstance(channel[0], models.Model): + return (dbname, channel[0]._name, channel[0].id, channel[1]) + if isinstance(channel, str): + return (dbname, channel) + return channel + + +def get_notify_payloads(channels): + """ + Generates the json payloads for the imbus NOTIFY. + Splits recursively payloads that are too large. + + :param list channels: + :return: list of payloads of json dumps + :rtype: list[str] + """ + if not channels: + return [] + payload = json_dump(channels) + if len(channels) == 1 or len(payload.encode()) < NOTIFY_PAYLOAD_MAX_LENGTH: + return [payload] + else: + pivot = math.ceil(len(channels) / 2) + return (get_notify_payloads(channels[:pivot]) + + get_notify_payloads(channels[pivot:])) + + +class ImBus(models.Model): + + _name = 'bus.bus' + _description = 'Communication Bus' + + channel = fields.Char('Channel') + message = fields.Char('Message') + + @api.autovacuum + def _gc_messages(self): + timeout_ago = datetime.datetime.utcnow()-datetime.timedelta(seconds=TIMEOUT*2) + domain = [('create_date', '<', timeout_ago.strftime(DEFAULT_SERVER_DATETIME_FORMAT))] + return self.sudo().search(domain).unlink() + + @api.model + def _sendmany(self, notifications): + channels = set() + values = [] + for target, notification_type, message in notifications: + channel = channel_with_db(self.env.cr.dbname, target) + channels.add(channel) + values.append({ + 'channel': json_dump(channel), + 'message': json_dump({ + 'type': notification_type, + 'payload': message, + }) + }) + self.sudo().create(values) + if channels: + # We have to wait until the notifications are commited in database. + # When calling `NOTIFY imbus`, notifications will be fetched in the + # bus table. If the transaction is not commited yet, there will be + # nothing to fetch, and the websocket will return no notification. + @self.env.cr.postcommit.add + def notify(): + with odoo.sql_db.db_connect('postgres').cursor() as cr: + query = sql.SQL("SELECT {}('imbus', %s)").format(sql.Identifier(ODOO_NOTIFY_FUNCTION)) + payloads = get_notify_payloads(list(channels)) + if len(payloads) > 1: + _logger.info("The imbus notification payload was too large, " + "it's been split into %d payloads.", len(payloads)) + for payload in payloads: + cr.execute(query, (payload,)) + + @api.model + def _sendone(self, channel, notification_type, message): + self._sendmany([[channel, notification_type, message]]) + + @api.model + def _poll(self, channels, last=0): + # first poll return the notification in the 'buffer' + if last == 0: + timeout_ago = datetime.datetime.utcnow()-datetime.timedelta(seconds=TIMEOUT) + domain = [('create_date', '>', timeout_ago.strftime(DEFAULT_SERVER_DATETIME_FORMAT))] + else: # else returns the unread notifications + domain = [('id', '>', last)] + channels = [json_dump(channel_with_db(self.env.cr.dbname, c)) for c in channels] + domain.append(('channel', 'in', channels)) + notifications = self.sudo().search_read(domain) + # list of notification to return + result = [] + for notif in notifications: + result.append({ + 'id': notif['id'], + 'message': json.loads(notif['message']), + }) + return result + + def _bus_last_id(self): + last = self.env['bus.bus'].search([], order='id desc', limit=1) + return last.id if last else 0 + + +#---------------------------------------------------------- +# Dispatcher +#---------------------------------------------------------- + +class BusSubscription: + def __init__(self, channels, last): + self.last_notification_id = last + self.channels = channels + + +class ImDispatch(threading.Thread): + def __init__(self): + super().__init__(daemon=True, name=f'{__name__}.Bus') + self._channels_to_ws = {} + + def subscribe(self, channels, last, db, websocket): + """ + Subcribe to bus notifications. Every notification related to the + given channels will be sent through the websocket. If a subscription + is already present, overwrite it. + """ + channels = {hashable(channel_with_db(db, c)) for c in channels} + for channel in channels: + self._channels_to_ws.setdefault(channel, set()).add(websocket) + outdated_channels = websocket._channels - channels + self._clear_outdated_channels(websocket, outdated_channels) + websocket.subscribe(channels, last) + with contextlib.suppress(RuntimeError): + if not self.is_alive(): + self.start() + + def unsubscribe(self, websocket): + self._clear_outdated_channels(websocket, websocket._channels) + + def _clear_outdated_channels(self, websocket, outdated_channels): + """ Remove channels from channel to websocket map. """ + for channel in outdated_channels: + self._channels_to_ws[channel].remove(websocket) + if not self._channels_to_ws[channel]: + self._channels_to_ws.pop(channel) + + def loop(self): + """ Dispatch postgres notifications to the relevant websockets """ + _logger.info("Bus.loop listen imbus on db postgres") + with odoo.sql_db.db_connect('postgres').cursor() as cr, \ + selectors.DefaultSelector() as sel: + cr.execute("listen imbus") + cr.commit() + conn = cr._cnx + sel.register(conn, selectors.EVENT_READ) + while not stop_event.is_set(): + if sel.select(TIMEOUT): + conn.poll() + channels = [] + while conn.notifies: + channels.extend(json.loads(conn.notifies.pop().payload)) + # relay notifications to websockets that have + # subscribed to the corresponding channels. + websockets = set() + for channel in channels: + websockets.update(self._channels_to_ws.get(hashable(channel), [])) + for websocket in websockets: + websocket.trigger_notification_dispatching() + + def run(self): + while not stop_event.is_set(): + try: + self.loop() + except Exception as exc: + if isinstance(exc, InterfaceError) and stop_event.is_set(): + continue + _logger.exception("Bus.loop error, sleep and retry") + time.sleep(TIMEOUT) + +# Partially undo a2ed3d3d5bdb6025a1ba14ad557a115a86413e65 +# IMDispatch has a lazy start, so we could initialize it anyway +# And this avoids the Bus unavailable error messages +dispatch = ImDispatch() +stop_event = threading.Event() +CommonServer.on_stop(stop_event.set) diff --git a/models/bus_presence.py b/models/bus_presence.py new file mode 100644 index 0000000..f545b19 --- /dev/null +++ b/models/bus_presence.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +import datetime +import time + +from psycopg2 import OperationalError + +from odoo import api, fields, models +from odoo import tools +from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY +from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT + +UPDATE_PRESENCE_DELAY = 60 +DISCONNECTION_TIMER = UPDATE_PRESENCE_DELAY + 5 +AWAY_TIMER = 1800 # 30 minutes + + +class BusPresence(models.Model): + """ User Presence + Its status is 'online', 'away' or 'offline'. This model should be a one2one, but is not + attached to res_users to avoid database concurrence errors. Since the 'update_presence' method is executed + at each poll, if the user have multiple opened tabs, concurrence errors can happend, but are 'muted-logged'. + """ + + _name = 'bus.presence' + _description = 'User Presence' + _log_access = False + + user_id = fields.Many2one('res.users', 'Users', ondelete='cascade') + last_poll = fields.Datetime('Last Poll', default=lambda self: fields.Datetime.now()) + last_presence = fields.Datetime('Last Presence', default=lambda self: fields.Datetime.now()) + status = fields.Selection([('online', 'Online'), ('away', 'Away'), ('offline', 'Offline')], 'IM Status', default='offline') + + def init(self): + self.env.cr.execute("CREATE UNIQUE INDEX IF NOT EXISTS bus_presence_user_unique ON %s (user_id) WHERE user_id IS NOT NULL" % self._table) + + @api.model + def update_presence(self, inactivity_period, identity_field, identity_value): + """ Updates the last_poll and last_presence of the current user + :param inactivity_period: duration in milliseconds + """ + # This method is called in method _poll() and cursor is closed right + # after; see bus/controllers/main.py. + try: + # Hide transaction serialization errors, which can be ignored, the presence update is not essential + # The errors are supposed from presence.write(...) call only + with tools.mute_logger('odoo.sql_db'): + self._update_presence(inactivity_period=inactivity_period, identity_field=identity_field, identity_value=identity_value) + # commit on success + self.env.cr.commit() + except OperationalError as e: + if e.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY: + # ignore concurrency error + return self.env.cr.rollback() + raise + + @api.model + def _update_presence(self, inactivity_period, identity_field, identity_value): + presence = self.search([(identity_field, '=', identity_value)], limit=1) + # compute last_presence timestamp + last_presence = datetime.datetime.now() - datetime.timedelta(milliseconds=inactivity_period) + values = { + 'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + } + # update the presence or a create a new one + if not presence: # create a new presence for the user + values[identity_field] = identity_value + values['last_presence'] = last_presence + self.create(values) + else: # update the last_presence if necessary, and write values + if presence.last_presence < last_presence: + values['last_presence'] = last_presence + presence.write(values) + + @api.autovacuum + def _gc_bus_presence(self): + self.search([('user_id.active', '=', False)]).unlink() diff --git a/models/ir_model.py b/models/ir_model.py new file mode 100644 index 0000000..b673c6b --- /dev/null +++ b/models/ir_model.py @@ -0,0 +1,35 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class IrModel(models.Model): + _inherit = 'ir.model' + + def _get_model_definitions(self, model_names_to_fetch): + fields_by_model_names = {} + for model_name in model_names_to_fetch: + model = self.env[model_name] + # get fields, relational fields are kept only if the related model is in model_names_to_fetch + fields_data_by_fname = { + fname: field_data + for fname, field_data in model.fields_get( + attributes={ + 'name', 'type', 'relation', 'required', 'readonly', 'selection', + 'string', 'definition_record', 'definition_record_field', + }, + ).items() + if not field_data.get('relation') or field_data['relation'] in model_names_to_fetch + } + for fname, field_data in fields_data_by_fname.items(): + if fname in model._fields: + inverse_fields = [ + field for field in model.pool.field_inverses[model._fields[fname]] + if field.model_name in model_names_to_fetch + ] + if inverse_fields: + field_data['inverse_fname_by_model_name'] = {field.model_name: field.name for field in inverse_fields} + if field_data['type'] == 'many2one_reference': + field_data['model_name_ref_fname'] = model._fields[fname].model_field + fields_by_model_names[model_name] = fields_data_by_fname + return fields_by_model_names diff --git a/models/ir_websocket.py b/models/ir_websocket.py new file mode 100644 index 0000000..5e2f8fa --- /dev/null +++ b/models/ir_websocket.py @@ -0,0 +1,63 @@ +from odoo import models +from odoo.http import request, SessionExpiredException +from odoo.service import security +from ..models.bus import dispatch +from ..websocket import wsrequest + + +class IrWebsocket(models.AbstractModel): + _name = 'ir.websocket' + _description = 'websocket message handling' + + def _get_im_status(self, im_status_ids_by_model): + im_status = {} + if 'res.partner' in im_status_ids_by_model: + im_status['Persona'] = [{**p, 'type': "partner"} for p in self.env['res.partner'].with_context(active_test=False).search_read( + [('id', 'in', im_status_ids_by_model['res.partner'])], + ['im_status'] + )] + return im_status + + def _build_bus_channel_list(self, channels): + """ + Return the list of channels to subscribe to. Override this + method to add channels in addition to the ones the client + sent. + + :param channels: The channel list sent by the client. + """ + req = request or wsrequest + channels.append('broadcast') + if req.session.uid: + channels.append(self.env.user.partner_id) + return channels + + def _subscribe(self, data): + if not all(isinstance(c, str) for c in data['channels']): + raise ValueError("bus.Bus only string channels are allowed.") + last_known_notification_id = self.env['bus.bus'].sudo().search([], limit=1, order='id desc').id or 0 + if data['last'] > last_known_notification_id: + data['last'] = 0 + channels = set(self._build_bus_channel_list(data['channels'])) + dispatch.subscribe(channels, data['last'], self.env.registry.db_name, wsrequest.ws) + + def _update_bus_presence(self, inactivity_period, im_status_ids_by_model): + if self.env.user and not self.env.user._is_public(): + self.env['bus.presence'].update_presence( + inactivity_period, + identity_field='user_id', + identity_value=self.env.uid + ) + im_status_notification = self._get_im_status(im_status_ids_by_model) + if im_status_notification: + self.env['bus.bus']._sendone(self.env.user.partner_id, 'mail.record/insert', im_status_notification) + + @classmethod + def _authenticate(cls): + if wsrequest.session.uid is not None: + if not security.check_session(wsrequest.session, wsrequest.env): + wsrequest.session.logout(keep_db=True) + raise SessionExpiredException() + else: + public_user = wsrequest.env.ref('base.public_user') + wsrequest.update_env(user=public_user.id) diff --git a/models/res_partner.py b/models/res_partner.py new file mode 100644 index 0000000..71f2f8f --- /dev/null +++ b/models/res_partner.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models +from odoo.addons.bus.models.bus_presence import AWAY_TIMER +from odoo.addons.bus.models.bus_presence import DISCONNECTION_TIMER + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + im_status = fields.Char('IM Status', compute='_compute_im_status') + + def _compute_im_status(self): + self.env.cr.execute(""" + SELECT + U.partner_id as id, + CASE WHEN max(B.last_poll) IS NULL THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away' + ELSE 'online' + END as status + FROM bus_presence B + RIGHT JOIN res_users U ON B.user_id = U.id + WHERE U.partner_id IN %s AND U.active = 't' + GROUP BY U.partner_id + """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids))) + res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall())) + for partner in self: + partner.im_status = res.get(partner.id, 'im_partner') # if not found, it is a partner, useful to avoid to refresh status in js diff --git a/models/res_users.py b/models/res_users.py new file mode 100644 index 0000000..8e40c1b --- /dev/null +++ b/models/res_users.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models +from odoo.addons.bus.models.bus_presence import AWAY_TIMER +from odoo.addons.bus.models.bus_presence import DISCONNECTION_TIMER + + +class ResUsers(models.Model): + + _inherit = "res.users" + + im_status = fields.Char('IM Status', compute='_compute_im_status') + + def _compute_im_status(self): + """ Compute the im_status of the users """ + self.env.cr.execute(""" + SELECT + user_id as id, + CASE WHEN age(now() AT TIME ZONE 'UTC', last_poll) > interval %s THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', last_presence) > interval %s THEN 'away' + ELSE 'online' + END as status + FROM bus_presence + WHERE user_id IN %s + """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids))) + res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall())) + for user in self: + user.im_status = res.get(user.id, 'offline') diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..4a626a0 --- /dev/null +++ b/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_bus_bus,bus.bus public,model_bus_bus,,0,0,0,0 +access_bus_presence,bus.presence,model_bus_presence,base.group_user,1,1,1,1 +access_bus_presence_portal,bus.presence,model_bus_presence,base.group_portal,1,1,1,1 diff --git a/static/src/bus_parameters_service.js b/static/src/bus_parameters_service.js new file mode 100644 index 0000000..eb803d4 --- /dev/null +++ b/static/src/bus_parameters_service.js @@ -0,0 +1,13 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; + +export const busParametersService = { + start() { + return { + serverURL: window.origin, + }; + }, +}; + +registry.category("services").add("bus.parameters", busParametersService); diff --git a/static/src/im_status_service.js b/static/src/im_status_service.js new file mode 100644 index 0000000..24041f5 --- /dev/null +++ b/static/src/im_status_service.js @@ -0,0 +1,77 @@ +/** @odoo-module **/ + +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; +import { timings } from "@bus/misc"; + +export const UPDATE_BUS_PRESENCE_DELAY = 60000; +/** + * This service updates periodically the user presence in order for the + * im_status to be up to date. + * + * In order to receive bus notifications related to im_status, one must + * register model/ids to monitor to this service. + */ +export const imStatusService = { + dependencies: ["bus_service", "multi_tab", "presence"], + + start(env, { bus_service, multi_tab, presence }) { + const imStatusModelToIds = {}; + let updateBusPresenceTimeout; + const throttledUpdateBusPresence = timings.throttle(function updateBusPresence() { + clearTimeout(updateBusPresenceTimeout); + if (!multi_tab.isOnMainTab()) { + return; + } + const now = new Date().getTime(); + bus_service.send("update_presence", { + inactivity_period: now - presence.getLastPresence(), + im_status_ids_by_model: { ...imStatusModelToIds }, + }); + updateBusPresenceTimeout = browser.setTimeout( + throttledUpdateBusPresence, + UPDATE_BUS_PRESENCE_DELAY + ); + }, UPDATE_BUS_PRESENCE_DELAY); + + bus_service.addEventListener("connect", () => { + // wait for im_status model/ids to be registered before starting. + browser.setTimeout(throttledUpdateBusPresence, 250); + }); + multi_tab.bus.addEventListener("become_main_tab", throttledUpdateBusPresence); + bus_service.addEventListener("reconnect", throttledUpdateBusPresence); + multi_tab.bus.addEventListener("no_longer_main_tab", () => + clearTimeout(updateBusPresenceTimeout) + ); + bus_service.addEventListener("disconnect", () => clearTimeout(updateBusPresenceTimeout)); + + return { + /** + * Register model/ids whose im_status should be monitored. + * Notification related to the im_status are then sent + * through the bus. Overwrite registration if already + * present. + * + * @param {string} model model related to the given ids. + * @param {Number[]} ids ids whose im_status should be + * monitored. + */ + registerToImStatus(model, ids) { + if (!ids.length) { + return this.unregisterFromImStatus(model); + } + imStatusModelToIds[model] = ids; + }, + /** + * Unregister model from im_status notifications. + * + * @param {string} model model to unregister. + */ + unregisterFromImStatus(model) { + delete imStatusModelToIds[model]; + }, + }; + }, +}; + +registry.category("services").add("im_status", imStatusService); diff --git a/static/src/misc.js b/static/src/misc.js new file mode 100644 index 0000000..6b4461e --- /dev/null +++ b/static/src/misc.js @@ -0,0 +1,65 @@ +/** @odoo-module */ + +import { browser } from "@web/core/browser/browser"; + +/** + * Returns a function, that, when invoked, will only be triggered at most once + * during a given window of time. Normally, the throttled function will run + * as much as it can, without ever going more than once per `wait` duration; + * but if you'd like to disable the execution on the leading edge, pass + * `{leading: false}`. To disable execution on the trailing edge, ditto. + * + * credit to `underscore.js` + */ +function throttle(func, wait, options) { + let timeout, context, args, result; + let previous = 0; + if (!options) { + options = {}; + } + + const later = function () { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) { + context = args = null; + } + }; + + const throttled = function () { + const _now = Date.now(); + if (!previous && options.leading === false) { + previous = _now; + } + const remaining = wait - (_now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + browser.clearTimeout(timeout); + timeout = null; + } + previous = _now; + result = func.apply(context, args); + if (!timeout) { + context = args = null; + } + } else if (!timeout && options.trailing !== false) { + timeout = browser.setTimeout(later, remaining); + } + return result; + }; + + throttled.cancel = function () { + browser.clearTimeout(timeout); + previous = 0; + timeout = context = args = null; + }; + + return throttled; +} + +export const timings = { + throttle, +}; diff --git a/static/src/multi_tab_service.js b/static/src/multi_tab_service.js new file mode 100644 index 0000000..b9ee0b9 --- /dev/null +++ b/static/src/multi_tab_service.js @@ -0,0 +1,223 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { browser } from "@web/core/browser/browser"; +import { EventBus } from "@odoo/owl"; + +let multiTabId = 0; +/** + * This service uses a Master/Slaves with Leader Election architecture in + * order to keep track of the main tab. Tabs are synchronized thanks to the + * localStorage. + * + * localStorage used keys are: + * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.lastPresenceByTab: + * mapping of tab ids to their last recorded presence. + * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.main : id of the current + * main tab. + * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.heartbeat : last main tab + * heartbeat time. + * + * trigger: + * - become_main_tab : when this tab became the main. + * - no_longer_main_tab : when this tab is no longer the main. + * - shared_value_updated: when one of the shared values changes. + */ +export const multiTabService = { + start() { + const bus = new EventBus(); + + // CONSTANTS + const TAB_HEARTBEAT_PERIOD = 10000; // 10 seconds + const MAIN_TAB_HEARTBEAT_PERIOD = 1500; // 1.5 seconds + const HEARTBEAT_OUT_OF_DATE_PERIOD = 5000; // 5 seconds + const HEARTBEAT_KILL_OLD_PERIOD = 15000; // 15 seconds + // Keys that should not trigger the `shared_value_updated` event. + const PRIVATE_LOCAL_STORAGE_KEYS = ["main", "heartbeat"]; + + // PROPERTIES + let _isOnMainTab = false; + let lastHeartbeat = 0; + let heartbeatTimeout; + const sanitizedOrigin = location.origin.replace(/:\/{0,2}/g, "_"); + const localStoragePrefix = `${this.name}.${sanitizedOrigin}.`; + const now = new Date().getTime(); + const tabId = `${this.name}${multiTabId++}:${now}`; + + function generateLocalStorageKey(baseKey) { + return localStoragePrefix + baseKey; + } + + function getItemFromStorage(key, defaultValue) { + const item = browser.localStorage.getItem(generateLocalStorageKey(key)); + try { + return item ? JSON.parse(item) : defaultValue; + } catch { + return item; + } + } + + function setItemInStorage(key, value) { + browser.localStorage.setItem(generateLocalStorageKey(key), JSON.stringify(value)); + } + + function startElection() { + if (_isOnMainTab) { + return; + } + // Check who's next. + const now = new Date().getTime(); + const lastPresenceByTab = getItemFromStorage("lastPresenceByTab", {}); + const heartbeatKillOld = now - HEARTBEAT_KILL_OLD_PERIOD; + let newMain; + for (const [tab, lastPresence] of Object.entries(lastPresenceByTab)) { + // Check for dead tabs. + if (lastPresence < heartbeatKillOld) { + continue; + } + newMain = tab; + break; + } + if (newMain === tabId) { + // We're next in queue. Electing as main. + lastHeartbeat = now; + setItemInStorage("heartbeat", lastHeartbeat); + setItemInStorage("main", true); + _isOnMainTab = true; + bus.trigger("become_main_tab"); + // Removing main peer from queue. + delete lastPresenceByTab[newMain]; + setItemInStorage("lastPresenceByTab", lastPresenceByTab); + } + } + + function heartbeat() { + const now = new Date().getTime(); + let heartbeatValue = getItemFromStorage("heartbeat", 0); + const lastPresenceByTab = getItemFromStorage("lastPresenceByTab", {}); + if (heartbeatValue + HEARTBEAT_OUT_OF_DATE_PERIOD < now) { + // Heartbeat is out of date. Electing new main. + startElection(); + heartbeatValue = getItemFromStorage("heartbeat", 0); + } + if (_isOnMainTab) { + // Walk through all tabs and kill old ones. + const cleanedTabs = {}; + for (const [tabId, lastPresence] of Object.entries(lastPresenceByTab)) { + if (lastPresence + HEARTBEAT_KILL_OLD_PERIOD > now) { + cleanedTabs[tabId] = lastPresence; + } + } + if (heartbeatValue !== lastHeartbeat) { + // Someone else is also main... + // It should not happen, except in some race condition situation. + _isOnMainTab = false; + lastHeartbeat = 0; + lastPresenceByTab[tabId] = now; + setItemInStorage("lastPresenceByTab", lastPresenceByTab); + bus.trigger("no_longer_main_tab"); + } else { + lastHeartbeat = now; + setItemInStorage("heartbeat", now); + setItemInStorage("lastPresenceByTab", cleanedTabs); + } + } else { + // Update own heartbeat. + lastPresenceByTab[tabId] = now; + setItemInStorage("lastPresenceByTab", lastPresenceByTab); + } + const hbPeriod = _isOnMainTab ? MAIN_TAB_HEARTBEAT_PERIOD : TAB_HEARTBEAT_PERIOD; + heartbeatTimeout = browser.setTimeout(heartbeat, hbPeriod); + } + + function onStorage({ key, newValue }) { + if (key === generateLocalStorageKey("main") && !newValue) { + // Main was unloaded. + startElection(); + } + if (PRIVATE_LOCAL_STORAGE_KEYS.includes(key)) { + return; + } + if (key && key.includes(localStoragePrefix)) { + // Only trigger the shared_value_updated event if the key is + // related to this service/origin. + const baseKey = key.replace(localStoragePrefix, ""); + bus.trigger("shared_value_updated", { key: baseKey, newValue }); + } + } + + function onPagehide() { + clearTimeout(heartbeatTimeout); + const lastPresenceByTab = getItemFromStorage("lastPresenceByTab", {}); + delete lastPresenceByTab[tabId]; + setItemInStorage("lastPresenceByTab", lastPresenceByTab); + + // Unload main. + if (_isOnMainTab) { + _isOnMainTab = false; + bus.trigger("no_longer_main_tab"); + browser.localStorage.removeItem(generateLocalStorageKey("main")); + } + } + + browser.addEventListener("pagehide", onPagehide); + browser.addEventListener("storage", onStorage); + + // REGISTER THIS TAB + const lastPresenceByTab = getItemFromStorage("lastPresenceByTab", {}); + lastPresenceByTab[tabId] = now; + setItemInStorage("lastPresenceByTab", lastPresenceByTab); + + if (!getItemFromStorage("main")) { + startElection(); + } + heartbeat(); + + return { + bus, + get currentTabId() { + return tabId; + }, + /** + * Determine whether or not this tab is the main one. + * + * @returns {boolean} + */ + isOnMainTab() { + return _isOnMainTab; + }, + /** + * Get value shared between all the tabs. + * + * @param {string} key + * @param {any} defaultValue Value to be returned if this + * key does not exist. + */ + getSharedValue(key, defaultValue) { + return getItemFromStorage(key, defaultValue); + }, + /** + * Set value shared between all the tabs. + * + * @param {string} key + * @param {any} value + */ + setSharedValue(key, value) { + if (value === undefined) { + return this.removeSharedValue(key); + } + setItemInStorage(key, value); + }, + /** + * Remove value shared between all the tabs. + * + * @param {string} key + */ + removeSharedValue(key) { + browser.localStorage.removeItem(generateLocalStorageKey(key)); + }, + }; + }, +}; + +registry.category("services").add("multi_tab", multiTabService); diff --git a/static/src/services/assets_watchdog_service.js b/static/src/services/assets_watchdog_service.js new file mode 100644 index 0000000..8850006 --- /dev/null +++ b/static/src/services/assets_watchdog_service.js @@ -0,0 +1,67 @@ +/** @odoo-module **/ + +import { _t } from "@web/core/l10n/translation"; +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; +import { session } from "@web/session"; + +export const assetsWatchdogService = { + dependencies: ["bus_service", "notification"], + + start(env, { bus_service, notification }) { + let isNotificationDisplayed = false; + let bundleNotifTimerID = null; + + bus_service.subscribe("bundle_changed", ({ server_version }) => { + if (server_version !== session.server_version) { + displayBundleChangedNotification(); + } + }); + bus_service.start(); + + /** + * Displays one notification on user's screen when assets have changed + */ + function displayBundleChangedNotification() { + if (!isNotificationDisplayed) { + // Wrap the notification inside a delay. + // The server may be overwhelmed with recomputing assets + // We wait until things settle down + browser.clearTimeout(bundleNotifTimerID); + bundleNotifTimerID = browser.setTimeout(() => { + notification.add(_t("The page appears to be out of date."), { + title: _t("Refresh"), + type: "warning", + sticky: true, + buttons: [ + { + name: _t("Refresh"), + primary: true, + onClick: () => { + browser.location.reload(); + }, + }, + ], + onClose: () => { + isNotificationDisplayed = false; + }, + }); + isNotificationDisplayed = true; + }, getBundleNotificationDelay()); + } + } + + /** + * Computes a random delay to avoid hammering the server + * when bundles change with all the users reloading + * at the same time + * + * @return {number} delay in milliseconds + */ + function getBundleNotificationDelay() { + return 10000 + Math.floor(Math.random() * 50) * 1000; + } + }, +}; + +registry.category("services").add("assetsWatchdog", assetsWatchdogService); diff --git a/static/src/services/bus_service.js b/static/src/services/bus_service.js new file mode 100644 index 0000000..2e2fa9d --- /dev/null +++ b/static/src/services/bus_service.js @@ -0,0 +1,208 @@ +/** @odoo-module **/ + +import { browser } from "@web/core/browser/browser"; +import { Deferred } from "@web/core/utils/concurrency"; +import { registry } from "@web/core/registry"; +import { session } from "@web/session"; +import { isIosApp } from "@web/core/browser/feature_detection"; +import { WORKER_VERSION } from "@bus/workers/websocket_worker"; +import { EventBus } from "@odoo/owl"; + +/** + * Communicate with a SharedWorker in order to provide a single websocket + * connection shared across multiple tabs. + * + * @emits connect + * @emits disconnect + * @emits reconnect + * @emits reconnecting + * @emits notification + */ +export const busService = { + dependencies: ["bus.parameters", "localization", "multi_tab"], + async: true, + + start(env, { multi_tab: multiTab, "bus.parameters": params }) { + const bus = new EventBus(); + const notificationBus = new EventBus(); + let worker; + let isActive = false; + let isInitialized = false; + let isUsingSharedWorker = browser.SharedWorker && !isIosApp(); + const startedAt = luxon.DateTime.now().set({ milliseconds: 0 }); + const connectionInitializedDeferred = new Deferred(); + + /** + * Send a message to the worker. + * + * @param {WorkerAction} action Action to be + * executed by the worker. + * @param {Object|undefined} data Data required for the action to be + * executed. + */ + function send(action, data) { + if (!worker) { + return; + } + const message = { action, data }; + if (isUsingSharedWorker) { + worker.port.postMessage(message); + } else { + worker.postMessage(message); + } + } + + /** + * Handle messages received from the shared worker and fires an + * event according to the message type. + * + * @param {MessageEvent} messageEv + * @param {{type: WorkerEvent, data: any}[]} messageEv.data + */ + function handleMessage(messageEv) { + const { type } = messageEv.data; + let { data } = messageEv.data; + if (type === "notification") { + data.forEach((d) => (d.message.id = d.id)); // put notification id in notif message + multiTab.setSharedValue("last_notification_id", data[data.length - 1].id); + data = data.map((notification) => notification.message); + for (const { type, payload } of data) { + notificationBus.trigger(type, payload); + } + } else if (type === "initialized") { + isInitialized = true; + connectionInitializedDeferred.resolve(); + return; + } + bus.trigger(type, data); + } + + /** + * Initialize the connection to the worker by sending it usefull + * initial informations (last notification id, debug mode, + * ...). + */ + function initializeWorkerConnection() { + // User_id has different values according to its origin: + // - frontend: number or false, + // - backend: array with only one number + // - guest page: array containing null or number + // - public pages: undefined + // Let's format it in order to ease its usage: + // - number if user is logged, false otherwise, keep + // undefined to indicate session_info is not available. + let uid = Array.isArray(session.user_id) ? session.user_id[0] : session.user_id; + if (!uid && uid !== undefined) { + uid = false; + } + send("initialize_connection", { + websocketURL: `${params.serverURL.replace("http", "ws")}/websocket`, + db: session.db, + debug: odoo.debug, + lastNotificationId: multiTab.getSharedValue("last_notification_id", 0), + uid, + startTs: startedAt.valueOf(), + }); + } + + /** + * Start the "bus_service" worker. + */ + function startWorker() { + let workerURL = `${params.serverURL}/bus/websocket_worker_bundle?v=${WORKER_VERSION}`; + if (params.serverURL !== window.origin) { + // Bus service is loaded from a different origin than the bundle + // URL. The Worker expects an URL from this origin, give it a base64 + // URL that will then load the bundle via "importScripts" which + // allows cross origin. + const source = `importScripts("${workerURL}");`; + workerURL = "data:application/javascript;base64," + window.btoa(source); + } + const workerClass = isUsingSharedWorker ? browser.SharedWorker : browser.Worker; + worker = new workerClass(workerURL, { + name: isUsingSharedWorker + ? "odoo:websocket_shared_worker" + : "odoo:websocket_worker", + }); + worker.addEventListener("error", (e) => { + if (!isInitialized && workerClass === browser.SharedWorker) { + console.warn( + 'Error while loading "bus_service" SharedWorker, fallback on Worker.' + ); + isUsingSharedWorker = false; + startWorker(); + } else if (!isInitialized) { + isInitialized = true; + connectionInitializedDeferred.resolve(); + console.warn("Bus service failed to initialized."); + } + }); + if (isUsingSharedWorker) { + worker.port.start(); + worker.port.addEventListener("message", handleMessage); + } else { + worker.addEventListener("message", handleMessage); + } + initializeWorkerConnection(); + } + browser.addEventListener("pagehide", ({ persisted }) => { + if (!persisted) { + // Page is gonna be unloaded, disconnect this client + // from the worker. + send("leave"); + } + }); + browser.addEventListener("online", () => { + if (isActive) { + send("start"); + } + }); + browser.addEventListener("offline", () => send("stop")); + + return { + addEventListener: bus.addEventListener.bind(bus), + addChannel: async (channel) => { + if (!worker) { + startWorker(); + await connectionInitializedDeferred; + } + send("add_channel", channel); + send("start"); + isActive = true; + }, + deleteChannel: (channel) => send("delete_channel", channel), + forceUpdateChannels: () => send("force_update_channels"), + trigger: bus.trigger.bind(bus), + removeEventListener: bus.removeEventListener.bind(bus), + send: (eventName, data) => send("send", { event_name: eventName, data }), + start: async () => { + if (!worker) { + startWorker(); + await connectionInitializedDeferred; + } + send("start"); + isActive = true; + }, + stop: () => { + send("leave"); + isActive = false; + }, + get isActive() { + return isActive; + }, + /** + * Subscribe to a single notification type. + * + * @param {string} notificationType + * @param {function} callback + */ + subscribe(notificationType, callback) { + notificationBus.addEventListener(notificationType, ({ detail }) => + callback(detail) + ); + }, + startedAt, + }; + }, +}; +registry.category("services").add("bus_service", busService); diff --git a/static/src/services/presence_service.js b/static/src/services/presence_service.js new file mode 100644 index 0000000..5d54da7 --- /dev/null +++ b/static/src/services/presence_service.js @@ -0,0 +1,61 @@ +/** @odoo-module **/ + +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; + +export const presenceService = { + start(env) { + const LOCAL_STORAGE_PREFIX = "presence"; + + let isOdooFocused = true; + let lastPresenceTime = + browser.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}.lastPresence`) || + new Date().getTime(); + + function onPresence() { + lastPresenceTime = new Date().getTime(); + browser.localStorage.setItem(`${LOCAL_STORAGE_PREFIX}.lastPresence`, lastPresenceTime); + } + + function onFocusChange(isFocused) { + try { + isFocused = parent.document.hasFocus(); + } catch { + // noop + } + isOdooFocused = isFocused; + browser.localStorage.setItem(`${LOCAL_STORAGE_PREFIX}.focus`, isOdooFocused); + if (isOdooFocused) { + lastPresenceTime = new Date().getTime(); + env.bus.trigger("window_focus", isOdooFocused); + } + } + + function onStorage({ key, newValue }) { + if (key === `${LOCAL_STORAGE_PREFIX}.focus`) { + isOdooFocused = JSON.parse(newValue); + env.bus.trigger("window_focus", newValue); + } + if (key === `${LOCAL_STORAGE_PREFIX}.lastPresence`) { + lastPresenceTime = JSON.parse(newValue); + } + } + browser.addEventListener("storage", onStorage); + browser.addEventListener("focus", () => onFocusChange(true)); + browser.addEventListener("blur", () => onFocusChange(false)); + browser.addEventListener("pagehide", () => onFocusChange(false)); + browser.addEventListener("click", onPresence); + browser.addEventListener("keydown", onPresence); + + return { + getLastPresence() { + return lastPresenceTime; + }, + isOdooFocused() { + return isOdooFocused; + }, + }; + }, +}; + +registry.category("services").add("presence", presenceService); diff --git a/static/src/simple_notification_service.js b/static/src/simple_notification_service.js new file mode 100644 index 0000000..7d3ffe7 --- /dev/null +++ b/static/src/simple_notification_service.js @@ -0,0 +1,15 @@ +/* @odoo-module */ + +import { registry } from "@web/core/registry"; + +export const simpleNotificationService = { + dependencies: ["bus_service", "notification"], + start(env, { bus_service, notification: notificationService }) { + bus_service.subscribe("simple_notification", ({ message, sticky, title, type }) => { + notificationService.add(message, { sticky, title, type }); + }); + bus_service.start(); + }, +}; + +registry.category("services").add("simple_notification", simpleNotificationService); diff --git a/static/src/workers/websocket_worker.js b/static/src/workers/websocket_worker.js new file mode 100644 index 0000000..b13af95 --- /dev/null +++ b/static/src/workers/websocket_worker.js @@ -0,0 +1,474 @@ +/** @odoo-module **/ + +import { debounce } from "@bus/workers/websocket_worker_utils"; + +/** + * Type of events that can be sent from the worker to its clients. + * + * @typedef { 'connect' | 'reconnect' | 'disconnect' | 'reconnecting' | 'notification' | 'initialized' } WorkerEvent + */ + +/** + * Type of action that can be sent from the client to the worker. + * + * @typedef {'add_channel' | 'delete_channel' | 'force_update_channels' | 'initialize_connection' | 'send' | 'leave' | 'stop' | 'start'} WorkerAction + */ + +export const WEBSOCKET_CLOSE_CODES = Object.freeze({ + CLEAN: 1000, + GOING_AWAY: 1001, + PROTOCOL_ERROR: 1002, + INCORRECT_DATA: 1003, + ABNORMAL_CLOSURE: 1006, + INCONSISTENT_DATA: 1007, + MESSAGE_VIOLATING_POLICY: 1008, + MESSAGE_TOO_BIG: 1009, + EXTENSION_NEGOTIATION_FAILED: 1010, + SERVER_ERROR: 1011, + RESTART: 1012, + TRY_LATER: 1013, + BAD_GATEWAY: 1014, + SESSION_EXPIRED: 4001, + KEEP_ALIVE_TIMEOUT: 4002, + RECONNECTING: 4003, +}); +// Should be incremented on every worker update in order to force +// update of the worker in browser cache. +export const WORKER_VERSION = "1.0.7"; +const MAXIMUM_RECONNECT_DELAY = 60000; + +/** + * This class regroups the logic necessary in order for the + * SharedWorker/Worker to work. Indeed, Safari and some minor browsers + * do not support SharedWorker. In order to solve this issue, a Worker + * is used in this case. The logic is almost the same than the one used + * for SharedWorker and this class implements it. + */ +export class WebsocketWorker { + INITIAL_RECONNECT_DELAY = 1000; + RECONNECT_JITTER = 1000; + + constructor() { + // Timestamp of start of most recent bus service sender + this.newestStartTs = undefined; + this.websocketURL = ""; + this.currentUID = null; + this.currentDB = null; + this.isWaitingForNewUID = true; + this.channelsByClient = new Map(); + this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY; + this.connectTimeout = null; + this.debugModeByClient = new Map(); + this.isDebug = false; + this.isReconnecting = false; + this.lastChannelSubscription = null; + this.lastNotificationId = 0; + this.messageWaitQueue = []; + this._forceUpdateChannels = debounce(this._forceUpdateChannels, 300); + this._updateChannels = debounce(this._updateChannels, 0); + + this._onWebsocketClose = this._onWebsocketClose.bind(this); + this._onWebsocketError = this._onWebsocketError.bind(this); + this._onWebsocketMessage = this._onWebsocketMessage.bind(this); + this._onWebsocketOpen = this._onWebsocketOpen.bind(this); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Send the message to all the clients that are connected to the + * worker. + * + * @param {WorkerEvent} type Event to broadcast to connected + * clients. + * @param {Object} data + */ + broadcast(type, data) { + for (const client of this.channelsByClient.keys()) { + client.postMessage({ type, data }); + } + } + + /** + * Register a client handled by this worker. + * + * @param {MessagePort} messagePort + */ + registerClient(messagePort) { + messagePort.onmessage = (ev) => { + this._onClientMessage(messagePort, ev.data); + }; + this.channelsByClient.set(messagePort, []); + } + + /** + * Send message to the given client. + * + * @param {number} client + * @param {WorkerEvent} type + * @param {Object} data + */ + sendToClient(client, type, data) { + client.postMessage({ type, data }); + } + + //-------------------------------------------------------------------------- + // PRIVATE + //-------------------------------------------------------------------------- + + /** + * Called when a message is posted to the worker by a client (i.e. a + * MessagePort connected to this worker). + * + * @param {MessagePort} client + * @param {Object} message + * @param {WorkerAction} [message.action] + * Action to execute. + * @param {Object|undefined} [message.data] Data required by the + * action. + */ + _onClientMessage(client, { action, data }) { + switch (action) { + case "send": + return this._sendToServer(data); + case "start": + return this._start(); + case "stop": + return this._stop(); + case "leave": + return this._unregisterClient(client); + case "add_channel": + return this._addChannel(client, data); + case "delete_channel": + return this._deleteChannel(client, data); + case "force_update_channels": + return this._forceUpdateChannels(); + case "initialize_connection": + return this._initializeConnection(client, data); + } + } + + /** + * Add a channel for the given client. If this channel is not yet + * known, update the subscription on the server. + * + * @param {MessagePort} client + * @param {string} channel + */ + _addChannel(client, channel) { + const clientChannels = this.channelsByClient.get(client); + if (!clientChannels.includes(channel)) { + clientChannels.push(channel); + this.channelsByClient.set(client, clientChannels); + this._updateChannels(); + } + } + + /** + * Remove a channel for the given client. If this channel is not + * used anymore, update the subscription on the server. + * + * @param {MessagePort} client + * @param {string} channel + */ + _deleteChannel(client, channel) { + const clientChannels = this.channelsByClient.get(client); + if (!clientChannels) { + return; + } + const channelIndex = clientChannels.indexOf(channel); + if (channelIndex !== -1) { + clientChannels.splice(channelIndex, 1); + this._updateChannels(); + } + } + + /** + * Update the channels on the server side even if the channels on + * the client side are the same than the last time we subscribed. + */ + _forceUpdateChannels() { + this._updateChannels({ force: true }); + } + + /** + * Remove the given client from this worker client list as well as + * its channels. If some of its channels are not used anymore, + * update the subscription on the server. + * + * @param {MessagePort} client + */ + _unregisterClient(client) { + this.channelsByClient.delete(client); + this.debugModeByClient.delete(client); + this.isDebug = Object.values(this.debugModeByClient).some( + (debugValue) => debugValue !== "" + ); + this._updateChannels(); + } + + /** + * Initialize a client connection to this worker. + * + * @param {Object} param0 + * @param {string} [param0.db] Database name. + * @param {String} [param0.debug] Current debugging mode for the + * given client. + * @param {Number} [param0.lastNotificationId] Last notification id + * known by the client. + * @param {String} [param0.websocketURL] URL of the websocket endpoint. + * @param {Number|false|undefined} [param0.uid] Current user id + * - Number: user is logged whether on the frontend/backend. + * - false: user is not logged. + * - undefined: not available (e.g. livechat support page) + * @param {Number} param0.startTs Timestamp of start of bus service sender. + */ + _initializeConnection(client, { db, debug, lastNotificationId, uid, websocketURL, startTs }) { + if (this.newestStartTs && this.newestStartTs > startTs) { + this.debugModeByClient[client] = debug; + this.isDebug = Object.values(this.debugModeByClient).some( + (debugValue) => debugValue !== "" + ); + this.sendToClient(client, "initialized"); + return; + } + this.newestStartTs = startTs; + this.websocketURL = websocketURL; + this.lastNotificationId = lastNotificationId; + this.debugModeByClient[client] = debug; + this.isDebug = Object.values(this.debugModeByClient).some( + (debugValue) => debugValue !== "" + ); + const isCurrentUserKnown = uid !== undefined; + if (this.isWaitingForNewUID && isCurrentUserKnown) { + this.isWaitingForNewUID = false; + this.currentUID = uid; + } + if ((this.currentUID !== uid && isCurrentUserKnown) || this.currentDB !== db) { + this.currentUID = uid; + this.currentDB = db; + if (this.websocket) { + this.websocket.close(WEBSOCKET_CLOSE_CODES.CLEAN); + } + this.channelsByClient.forEach((_, key) => this.channelsByClient.set(key, [])); + } + this.sendToClient(client, "initialized"); + } + + /** + * Determine whether or not the websocket associated to this worker + * is connected. + * + * @returns {boolean} + */ + _isWebsocketConnected() { + return this.websocket && this.websocket.readyState === 1; + } + + /** + * Determine whether or not the websocket associated to this worker + * is connecting. + * + * @returns {boolean} + */ + _isWebsocketConnecting() { + return this.websocket && this.websocket.readyState === 0; + } + + /** + * Determine whether or not the websocket associated to this worker + * is in the closing state. + * + * @returns {boolean} + */ + _isWebsocketClosing() { + return this.websocket && this.websocket.readyState === 2; + } + + /** + * Triggered when a connection is closed. If closure was not clean , + * try to reconnect after indicating to the clients that the + * connection was closed. + * + * @param {CloseEvent} ev + * @param {number} code close code indicating why the connection + * was closed. + * @param {string} reason reason indicating why the connection was + * closed. + */ + _onWebsocketClose({ code, reason }) { + if (this.isDebug) { + console.debug( + `%c${new Date().toLocaleString()} - [onClose]`, + "color: #c6e; font-weight: bold;", + code, + reason + ); + } + this.lastChannelSubscription = null; + if (this.isReconnecting) { + // Connection was not established but the close event was + // triggered anyway. Let the onWebsocketError method handle + // this case. + return; + } + this.broadcast("disconnect", { code, reason }); + if (code === WEBSOCKET_CLOSE_CODES.CLEAN) { + // WebSocket was closed on purpose, do not try to reconnect. + return; + } + // WebSocket was not closed cleanly, let's try to reconnect. + this.broadcast("reconnecting", { closeCode: code }); + this.isReconnecting = true; + if (code === WEBSOCKET_CLOSE_CODES.KEEP_ALIVE_TIMEOUT) { + // Don't wait to reconnect on keep alive timeout. + this.connectRetryDelay = 0; + } + if (code === WEBSOCKET_CLOSE_CODES.SESSION_EXPIRED) { + this.isWaitingForNewUID = true; + } + this._retryConnectionWithDelay(); + } + + /** + * Triggered when a connection failed or failed to established. + */ + _onWebsocketError() { + if (this.isDebug) { + console.debug( + `%c${new Date().toLocaleString()} - [onError]`, + "color: #c6e; font-weight: bold;" + ); + } + this._retryConnectionWithDelay(); + } + + /** + * Handle data received from the bus. + * + * @param {MessageEvent} messageEv + */ + _onWebsocketMessage(messageEv) { + const notifications = JSON.parse(messageEv.data); + if (this.isDebug) { + console.debug( + `%c${new Date().toLocaleString()} - [onMessage]`, + "color: #c6e; font-weight: bold;", + notifications + ); + } + this.lastNotificationId = notifications[notifications.length - 1].id; + this.broadcast("notification", notifications); + } + + /** + * Triggered on websocket open. Send message that were waiting for + * the connection to open. + */ + _onWebsocketOpen() { + if (this.isDebug) { + console.debug( + `%c${new Date().toLocaleString()} - [onOpen]`, + "color: #c6e; font-weight: bold;" + ); + } + this._updateChannels(); + this.messageWaitQueue.forEach((msg) => this.websocket.send(msg)); + this.messageWaitQueue = []; + this.broadcast(this.isReconnecting ? "reconnect" : "connect"); + this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY; + this.connectTimeout = null; + this.isReconnecting = false; + } + + /** + * Try to reconnect to the server, an exponential back off is + * applied to the reconnect attempts. + */ + _retryConnectionWithDelay() { + this.connectRetryDelay = + Math.min(this.connectRetryDelay * 1.5, MAXIMUM_RECONNECT_DELAY) + + this.RECONNECT_JITTER * Math.random(); + this.connectTimeout = setTimeout(this._start.bind(this), this.connectRetryDelay); + } + + /** + * Send a message to the server through the websocket connection. + * If the websocket is not open, enqueue the message and send it + * upon the next reconnection. + * + * @param {{event_name: string, data: any }} message Message to send to the server. + */ + _sendToServer(message) { + const payload = JSON.stringify(message); + if (!this._isWebsocketConnected()) { + this.messageWaitQueue.push(payload); + } else { + this.websocket.send(payload); + } + } + + /** + * Start the worker by opening a websocket connection. + */ + _start() { + if (this._isWebsocketConnected() || this._isWebsocketConnecting()) { + return; + } + if (this.websocket) { + this.websocket.removeEventListener("open", this._onWebsocketOpen); + this.websocket.removeEventListener("message", this._onWebsocketMessage); + this.websocket.removeEventListener("error", this._onWebsocketError); + this.websocket.removeEventListener("close", this._onWebsocketClose); + } + if (this._isWebsocketClosing()) { + // close event was not triggered and will never be, broadcast the + // disconnect event for consistency sake. + this.lastChannelSubscription = null; + this.broadcast("disconnect", { code: WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE }); + } + this.websocket = new WebSocket(this.websocketURL); + this.websocket.addEventListener("open", this._onWebsocketOpen); + this.websocket.addEventListener("error", this._onWebsocketError); + this.websocket.addEventListener("message", this._onWebsocketMessage); + this.websocket.addEventListener("close", this._onWebsocketClose); + } + + /** + * Stop the worker. + */ + _stop() { + clearTimeout(this.connectTimeout); + this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY; + this.isReconnecting = false; + this.lastChannelSubscription = null; + if (this.websocket) { + this.websocket.close(); + } + } + + /** + * Update the channel subscription on the server. Ignore if the channels + * did not change since the last subscription. + * + * @param {boolean} force Whether or not we should update the subscription + * event if the channels haven't change since last subscription. + */ + _updateChannels({ force = false } = {}) { + const allTabsChannels = [ + ...new Set([].concat.apply([], [...this.channelsByClient.values()])), + ].sort(); + const allTabsChannelsString = JSON.stringify(allTabsChannels); + const shouldUpdateChannelSubscription = + allTabsChannelsString !== this.lastChannelSubscription; + if (force || shouldUpdateChannelSubscription) { + this.lastChannelSubscription = allTabsChannelsString; + this._sendToServer({ + event_name: "subscribe", + data: { channels: allTabsChannels, last: this.lastNotificationId }, + }); + } + } +} diff --git a/static/src/workers/websocket_worker_script.js b/static/src/workers/websocket_worker_script.js new file mode 100644 index 0000000..a23c54c --- /dev/null +++ b/static/src/workers/websocket_worker_script.js @@ -0,0 +1,22 @@ +/** @odoo-module **/ +/* eslint-env worker */ +/* eslint-disable no-restricted-globals */ + +import { WebsocketWorker } from "./websocket_worker"; + +(function () { + const websocketWorker = new WebsocketWorker(); + + if (self.name.includes("shared")) { + // The script is running in a shared worker: let's register every + // tab connection to the worker in order to relay notifications + // coming from the websocket. + onconnect = function (ev) { + const currentClient = ev.ports[0]; + websocketWorker.registerClient(currentClient); + }; + } else { + // The script is running in a simple web worker. + websocketWorker.registerClient(self); + } +})(); diff --git a/static/src/workers/websocket_worker_utils.js b/static/src/workers/websocket_worker_utils.js new file mode 100644 index 0000000..01131c7 --- /dev/null +++ b/static/src/workers/websocket_worker_utils.js @@ -0,0 +1,29 @@ +/** @odoo-module **/ + +/** + * Returns a function, that, as long as it continues to be invoked, will not + * be triggered. The function will be called after it stops being called for + * N milliseconds. If `immediate` is passed, trigger the function on the + * leading edge, instead of the trailing. + * + * Inspired by https://davidwalsh.name/javascript-debounce-function + */ +export function debounce(func, wait, immediate) { + let timeout; + return function () { + const context = this; + const args = arguments; + function later() { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + } + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + }; +} diff --git a/static/tests/assets_watchdog_tests.js b/static/tests/assets_watchdog_tests.js new file mode 100644 index 0000000..263ea5e --- /dev/null +++ b/static/tests/assets_watchdog_tests.js @@ -0,0 +1,35 @@ +/* @odoo-module */ + +import { getPyEnv, startServer } from "@bus/../tests/helpers/mock_python_environment"; +import { addBusServicesToRegistry } from "@bus/../tests/helpers/test_utils"; +import { assetsWatchdogService } from "@bus/services/assets_watchdog_service"; + +import { patchWithCleanup } from "@web/../tests/helpers/utils"; +import { click, contains } from "@web/../tests/utils"; +import { createWebClient } from "@web/../tests/webclient/helpers"; +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; + +QUnit.module("Bus Assets WatchDog"); + +QUnit.test("can listen on bus and displays notifications in DOM", async (assert) => { + await startServer(); + addBusServicesToRegistry(); + registry.category("services").add("assetsWatchdog", assetsWatchdogService); + patchWithCleanup(browser, { + setTimeout(fn) { + return super.setTimeout(fn, 0); + }, + location: { + reload: () => assert.step("reloadPage"), + }, + }); + await createWebClient({}); + const pyEnv = await getPyEnv(); + pyEnv["bus.bus"]._sendone("broadcast", "bundle_changed", { + server_version: "NEW_MAJOR_VERSION", + }); + await contains(".o_notification", { text: "The page appears to be out of date." }); + await click(".o_notification_buttons .btn-primary", { text: "Refresh" }); + assert.verifySteps(["reloadPage"]); +}); diff --git a/static/tests/bus_tests.js b/static/tests/bus_tests.js new file mode 100644 index 0000000..1f4b8fc --- /dev/null +++ b/static/tests/bus_tests.js @@ -0,0 +1,393 @@ +/** @odoo-module **/ + +import { addBusServicesToRegistry } from "@bus/../tests/helpers/test_utils"; +import { startServer } from "@bus/../tests/helpers/mock_python_environment"; +import { patchWebsocketWorkerWithCleanup } from "@bus/../tests/helpers/mock_websocket"; +import { + waitForBusEvent, + waitForChannels, + waitNotifications, + waitUntilSubscribe, + waitForWorkerEvent, +} from "@bus/../tests/helpers/websocket_event_deferred"; +import { busParametersService } from "@bus/bus_parameters_service"; +import { WEBSOCKET_CLOSE_CODES } from "@bus/workers/websocket_worker"; + +import { makeTestEnv } from "@web/../tests/helpers/mock_env"; +import { makeDeferred, patchWithCleanup } from "@web/../tests/helpers/utils"; +import { browser } from "@web/core/browser/browser"; +import { session } from "@web/session"; + +QUnit.module("Bus"); + +QUnit.test("notifications received from the channel", async () => { + addBusServicesToRegistry(); + const pyEnv = await startServer(); + const env = await makeTestEnv({ activateMockServer: true }); + env.services["bus_service"].addChannel("lambda"); + await waitUntilSubscribe("lambda"); + pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta"); + pyEnv["bus.bus"]._sendone("lambda", "notifType", "epsilon"); + await waitNotifications([env, "notifType", "beta"], [env, "notifType", "epsilon"]); +}); + +QUnit.test("notifications not received after stoping the service", async () => { + addBusServicesToRegistry(); + const pyEnv = await startServer(); + const firstTabEnv = await makeTestEnv({ activateMockServer: true }); + const secondTabEnv = await makeTestEnv({ activateMockServer: true }); + firstTabEnv.services["bus_service"].start(); + secondTabEnv.services["bus_service"].start(); + firstTabEnv.services["bus_service"].addChannel("lambda"); + await waitUntilSubscribe("lambda"); + // both tabs should receive the notification + pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta"); + await waitNotifications( + [firstTabEnv, "notifType", "beta"], + [secondTabEnv, "notifType", "beta"] + ); + secondTabEnv.services["bus_service"].stop(); + await waitForWorkerEvent("leave"); + pyEnv["bus.bus"]._sendone("lambda", "notifType", "epsilon"); + await waitNotifications( + [firstTabEnv, "notifType", "epsilon"], + [secondTabEnv, "notifType", "epsilon", { received: false }] + ); +}); + +QUnit.test("notifications still received after disconnect/reconnect", async () => { + addBusServicesToRegistry(); + const pyEnv = await startServer(); + const env = await makeTestEnv({ activateMockServer: true }); + env.services["bus_service"].addChannel("lambda"); + await Promise.all([waitForBusEvent(env, "connect"), waitForChannels(["lambda"])]); + pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta"); + await waitNotifications([env, "notifType", "beta"]); + pyEnv.simulateConnectionLost(WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE); + await waitForBusEvent(env, "reconnect"); + pyEnv["bus.bus"]._sendone("lambda", "notifType", "gamma"); + await waitNotifications([env, "notifType", "gamma"]); +}); + +QUnit.test("tabs share message from a channel", async () => { + addBusServicesToRegistry(); + const pyEnv = await startServer(); + const mainEnv = await makeTestEnv({ activateMockServer: true }); + mainEnv.services["bus_service"].addChannel("lambda"); + const slaveEnv = await makeTestEnv(); + slaveEnv.services["bus_service"].addChannel("lambda"); + await waitUntilSubscribe("lambda"); + pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta"); + await waitNotifications([mainEnv, "notifType", "beta"], [slaveEnv, "notifType", "beta"]); +}); + +QUnit.test("second tab still receives notifications after main pagehide", async () => { + addBusServicesToRegistry(); + const pyEnv = await startServer(); + const mainEnv = await makeTestEnv({ activateMockServer: true }); + mainEnv.services["bus_service"].addChannel("lambda"); + // prevent second tab from receiving pagehide event. + patchWithCleanup(browser, { + addEventListener(eventName, callback) { + if (eventName === "pagehide") { + return; + } + super.addEventListener(eventName, callback); + }, + }); + const secondEnv = await makeTestEnv({ activateMockServer: true }); + secondEnv.services["bus_service"].addChannel("lambda"); + await waitUntilSubscribe("lambda"); + pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta"); + await waitNotifications([mainEnv, "notifType", "beta"], [secondEnv, "notifType", "beta"]); + // simulate unloading main + window.dispatchEvent(new Event("pagehide")); + await waitForWorkerEvent("leave"); + pyEnv["bus.bus"]._sendone("lambda", "notifType", "gamma"); + await waitNotifications( + [mainEnv, "notifType", "gamma", { received: false }], + [secondEnv, "notifType", "gamma"] + ); +}); + +QUnit.test("two tabs adding a different channel", async () => { + addBusServicesToRegistry(); + const pyEnv = await startServer(); + const firstTabEnv = await makeTestEnv({ activateMockServer: true }); + const secondTabEnv = await makeTestEnv({ activateMockServer: true }); + firstTabEnv.services["bus_service"].addChannel("alpha"); + secondTabEnv.services["bus_service"].addChannel("beta"); + await waitUntilSubscribe("alpha", "beta"); + pyEnv["bus.bus"]._sendmany([ + ["alpha", "notifType", "alpha"], + ["beta", "notifType", "beta"], + ]); + await waitNotifications( + [firstTabEnv, "notifType", "alpha"], + [secondTabEnv, "notifType", "alpha"], + [firstTabEnv, "notifType", "beta"], + [secondTabEnv, "notifType", "beta"] + ); +}); + +QUnit.test("channel management from multiple tabs", async (assert) => { + await startServer(); + addBusServicesToRegistry(); + patchWebsocketWorkerWithCleanup({ + _sendToServer({ event_name, data }) { + assert.step(`${event_name} - [${data.channels.toString()}]`); + super._sendToServer(...arguments); + }, + }); + const firstTabEnv = await makeTestEnv({ activateMockServer: true }); + const secTabEnv = await makeTestEnv({ activateMockServer: true }); + firstTabEnv.services["bus_service"].addChannel("channel1"); + await waitForChannels(["channel1"]); + // this should not trigger a subscription since the channel1 was + // aleady known. + secTabEnv.services["bus_service"].addChannel("channel1"); + await waitForChannels(["channel1"]); + // removing channel1 from first tab should not trigger + // re-subscription since the second tab still listens to this + // channel. + firstTabEnv.services["bus_service"].deleteChannel("channel1"); + await waitForChannels(["channel1"], { operation: "delete" }); + // this should trigger a subscription since the channel2 was not + // known. + secTabEnv.services["bus_service"].addChannel("channel2"); + await waitUntilSubscribe("channel2"); + assert.verifySteps(["subscribe - [channel1]", "subscribe - [channel1,channel2]"]); +}); + +QUnit.test("channels subscription after disconnection", async (assert) => { + await startServer(); + addBusServicesToRegistry(); + const worker = patchWebsocketWorkerWithCleanup(); + const env = await makeTestEnv({ activateMockServer: true }); + env.services["bus_service"].start(); + await waitUntilSubscribe(); + worker.websocket.close(WEBSOCKET_CLOSE_CODES.KEEP_ALIVE_TIMEOUT); + await Promise.all([waitForBusEvent(env, "reconnect"), waitUntilSubscribe()]); + assert.ok( + true, + "No error means waitUntilSubscribe resolves twice thus two subscriptions were triggered as expected" + ); +}); + +QUnit.test("Last notification id is passed to the worker on service start", async (assert) => { + addBusServicesToRegistry(); + const pyEnv = await startServer(); + let updateLastNotificationDeferred = makeDeferred(); + patchWebsocketWorkerWithCleanup({ + _onClientMessage(_, { action, data }) { + if (action === "initialize_connection") { + assert.step(`${action} - ${data["lastNotificationId"]}`); + updateLastNotificationDeferred.resolve(); + } + return super._onClientMessage(...arguments); + }, + }); + const env1 = await makeTestEnv(); + env1.services["bus_service"].start(); + env1.services["bus_service"].addChannel("lambda"); + await waitUntilSubscribe("lambda"); + await updateLastNotificationDeferred; + // First bus service has never received notifications thus the + // default is 0. + assert.verifySteps(["initialize_connection - 0"]); + pyEnv["bus.bus"]._sendmany([ + ["lambda", "notifType", "beta"], + ["lambda", "notifType", "beta"], + ]); + await waitForBusEvent(env1, "notification"); + updateLastNotificationDeferred = makeDeferred(); + const env2 = await makeTestEnv(); + await env2.services["bus_service"].start(); + await updateLastNotificationDeferred; + // Second bus service sends the last known notification id. + assert.verifySteps([`initialize_connection - 2`]); +}); + +QUnit.test("Websocket disconnects upon user log out", async () => { + addBusServicesToRegistry(); + patchWebsocketWorkerWithCleanup(); + // first tab connects to the worker with user logged. + patchWithCleanup(session, { user_id: 1 }); + const firstTabEnv = await makeTestEnv(); + firstTabEnv.services["bus_service"].start(); + await waitForBusEvent(firstTabEnv, "connect"); + // second tab connects to the worker after disconnection: user_id + // is now false. + patchWithCleanup(session, { user_id: false }); + const env2 = await makeTestEnv(); + env2.services["bus_service"].start(); + await waitForBusEvent(firstTabEnv, "disconnect"); +}); + +QUnit.test("Websocket reconnects upon user log in", async () => { + addBusServicesToRegistry(); + patchWebsocketWorkerWithCleanup(); + // first tab connects to the worker with no user logged. + patchWithCleanup(session, { user_id: false }); + const firstTabEnv = await makeTestEnv(); + firstTabEnv.services["bus_service"].start(); + await waitForBusEvent(firstTabEnv, "connect"); + // second tab connects to the worker after connection: user_id + // is now set. + patchWithCleanup(session, { user_id: 1 }); + const secondTabEnv = await makeTestEnv(); + secondTabEnv.services["bus_service"].start(); + await Promise.all([ + waitForBusEvent(firstTabEnv, "disconnect"), + waitForBusEvent(firstTabEnv, "connect"), + ]); +}); + +QUnit.test("WebSocket connects with URL corresponding to given serverURL", async (assert) => { + addBusServicesToRegistry(); + patchWebsocketWorkerWithCleanup(); + const serverURL = "http://random-website.com"; + patchWithCleanup(busParametersService, { + start() { + return { + ...super.start(...arguments), + serverURL, + }; + }, + }); + const websocketCreatedDeferred = makeDeferred(); + patchWithCleanup(window, { + WebSocket: function (url) { + assert.step(url); + websocketCreatedDeferred.resolve(); + return new EventTarget(); + }, + }); + const env = await makeTestEnv(); + env.services["bus_service"].start(); + await websocketCreatedDeferred; + assert.verifySteps([`${serverURL.replace("http", "ws")}/websocket`]); +}); + +QUnit.test("Disconnect on offline, re-connect on online", async () => { + addBusServicesToRegistry(); + patchWebsocketWorkerWithCleanup(); + const env = await makeTestEnv(); + await env.services["bus_service"].start(); + await waitForBusEvent(env, "connect"); + window.dispatchEvent(new Event("offline")); + await waitForBusEvent(env, "disconnect"); + window.dispatchEvent(new Event("online")); + await waitForBusEvent(env, "connect"); +}); + +QUnit.test("No disconnect on change offline/online when bus inactive", async () => { + addBusServicesToRegistry(); + patchWebsocketWorkerWithCleanup(); + const env = await makeTestEnv(); + window.dispatchEvent(new Event("offline")); + await waitForBusEvent(env, "disconnect", { received: false }); + window.dispatchEvent(new Event("online")); + await waitForBusEvent(env, "connect", { received: false }); +}); + +QUnit.test("Can reconnect after late close event", async (assert) => { + addBusServicesToRegistry(); + let subscribeSent = 0; + const closeDeferred = makeDeferred(); + const worker = patchWebsocketWorkerWithCleanup({ + _sendToServer({ event_name }) { + if (event_name === "subscribe") { + subscribeSent++; + } + }, + }); + const pyEnv = await startServer(); + const env = await makeTestEnv(); + env.services["bus_service"].start(); + await waitForBusEvent(env, "connect"); + patchWithCleanup(worker.websocket, { + close(code = WEBSOCKET_CLOSE_CODES.CLEAN, reason) { + this.readyState = 2; + if (code === WEBSOCKET_CLOSE_CODES.CLEAN) { + closeDeferred.then(() => { + // Simulate that the connection could not be closed cleanly. + super.close(WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE, reason); + }); + } else { + super.close(code, reason); + } + }, + }); + // Connection will be closed when passing offline. But the close event + // will be delayed to come after the next open event. The connection + // will thus be in the closing state in the meantime. + window.dispatchEvent(new Event("offline")); + // Worker reconnects upon the reception of the online event. + window.dispatchEvent(new Event("online")); + await waitForBusEvent(env, "disconnect"); + await waitForBusEvent(env, "connect"); + // Trigger the close event, it shouldn't have any effect since it is + // related to an old connection that is no longer in use. + closeDeferred.resolve(); + await waitForBusEvent(env, "disconnect", { received: false }); + // Server closes the connection, the worker should reconnect. + pyEnv.simulateConnectionLost(WEBSOCKET_CLOSE_CODES.KEEP_ALIVE_TIMEOUT); + await waitForBusEvent(env, "reconnecting"); + await waitForBusEvent(env, "reconnect"); + // 3 connections were opened, so 3 subscriptions are expected. + assert.strictEqual(subscribeSent, 3); +}); + +QUnit.test("Fallback on simple worker when shared worker failed to initialize", async (assert) => { + addBusServicesToRegistry(); + patchWebsocketWorkerWithCleanup(); + const originalSharedWorker = browser.SharedWorker; + const originalWorker = browser.Worker; + patchWithCleanup(browser, { + SharedWorker: function (url, options) { + assert.step("shared-worker creation"); + const sw = new originalSharedWorker(url, options); + // Simulate error during shared worker creation. + setTimeout(() => sw.dispatchEvent(new Event("error"))); + return sw; + }, + Worker: function (url, options) { + assert.step("worker creation"); + return new originalWorker(url, options); + }, + }); + patchWithCleanup(window.console, { + warn(message) { + assert.step(message); + }, + }); + const env = await makeTestEnv(); + env.services["bus_service"].start(); + await waitForBusEvent(env, "connect"); + assert.verifySteps([ + "shared-worker creation", + 'Error while loading "bus_service" SharedWorker, fallback on Worker.', + "worker creation", + ]); +}); + +QUnit.test("subscribe to single notification", async (assert) => { + addBusServicesToRegistry(); + const pyEnv = await startServer(); + const env = await makeTestEnv({ activateMockServer: true }); + env.services["bus_service"].start(); + const messageReceivedDeferred = makeDeferred(); + env.services["bus_service"].subscribe("message", (payload) => { + assert.deepEqual({ body: "hello", id: 1 }, payload); + assert.step("message"); + messageReceivedDeferred.resolve(); + }); + await waitUntilSubscribe(); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "message", { + body: "hello", + id: 1, + }); + await messageReceivedDeferred; + assert.verifySteps(["message"]); +}); diff --git a/static/tests/helpers/mock_python_environment.js b/static/tests/helpers/mock_python_environment.js new file mode 100644 index 0000000..88ff1a4 --- /dev/null +++ b/static/tests/helpers/mock_python_environment.js @@ -0,0 +1,342 @@ +/** @odoo-module **/ + +import { TEST_USER_IDS } from "@bus/../tests/helpers/test_constants"; + +import { registry } from "@web/core/registry"; +import { registerCleanup } from "@web/../tests/helpers/cleanup"; +import { makeMockServer } from "@web/../tests/helpers/mock_server"; +import { serializeDateTime, serializeDate } from "@web/core/l10n/dates"; +const { DateTime } = luxon; + +const modelDefinitionsPromise = new Promise((resolve) => { + QUnit.begin(() => resolve(getModelDefinitions())); +}); + +/** + * Fetch model definitions from the server then insert fields present in the + * `bus.model.definitions` registry. Use `addModelNamesToFetch`/`insertModelFields` + * helpers in order to add models to be fetched, default values to the fields, + * fields to a model definition. + * + * @return {Map} A map from model names to model fields definitions. + * @see model_definitions_setup.js + */ +async function getModelDefinitions() { + const modelDefinitionsRegistry = registry.category("bus.model.definitions"); + const modelNamesToFetch = modelDefinitionsRegistry.get("modelNamesToFetch"); + const fieldsToInsertRegistry = modelDefinitionsRegistry.category("fieldsToInsert"); + + // fetch the model definitions. + const formData = new FormData(); + formData.append("csrf_token", odoo.csrf_token); + formData.append("model_names_to_fetch", JSON.stringify(modelNamesToFetch)); + const response = await window.fetch("/bus/get_model_definitions", { + body: formData, + method: "POST", + }); + if (response.status !== 200) { + throw new Error("Error while fetching required models"); + } + const modelDefinitions = new Map(Object.entries(await response.json())); + + for (const [modelName, fields] of modelDefinitions) { + // insert fields present in the fieldsToInsert registry : if the field + // exists, update its default value according to the one in the + // registry; If it does not exist, add it to the model definition. + const fieldNamesToFieldToInsert = fieldsToInsertRegistry.category(modelName).getEntries(); + for (const [fname, fieldToInsert] of fieldNamesToFieldToInsert) { + if (fname in fields) { + fields[fname].default = fieldToInsert.default; + } else { + fields[fname] = fieldToInsert; + } + } + // apply default values for date like fields if none was passed. + for (const fname in fields) { + const field = fields[fname]; + if (["date", "datetime"].includes(field.type) && !field.default) { + const defaultFieldValue = + field.type === "date" + ? () => serializeDate(DateTime.utc()) + : () => serializeDateTime(DateTime.utc()); + field.default = defaultFieldValue; + } else if (fname === "active" && !("default" in field)) { + // records are active by default. + field.default = true; + } + } + } + // add models present in the fake models registry to the model definitions. + const fakeModels = modelDefinitionsRegistry.category("fakeModels").getEntries(); + for (const [modelName, fields] of fakeModels) { + modelDefinitions.set(modelName, fields); + } + return modelDefinitions; +} + +let _cookie = {}; +export const pyEnvTarget = { + cookie: { + get(key) { + return _cookie[key]; + }, + set(key, value) { + _cookie[key] = value; + }, + delete(key) { + delete _cookie[key]; + }, + }, + _authenticate(user) { + if (!user) { + throw new Error("Unauthorized"); + } + this.cookie.set("sid", user.id); + }, + /** + * Authenticate a user on the mock server given its login + * and password. + * + * @param {string} login + * @param {string|} password + */ + authenticate(login, password) { + const user = this.mockServer.getRecords( + "res.users", + [ + ["login", "=", login], + ["password", "=", password], + ], + { active_test: false } + )[0]; + this._authenticate(user); + this.cookie.set("authenticated_user_sid", this.cookie.get("sid")); + }, + /** + * Logout the current user. + */ + logout() { + if (this.cookie.get("authenticated_user_sid") === this.cookie.get("sid")) { + this.cookie.delete("authenticated_user_sid"); + } + this.cookie.delete("sid"); + const [publicUser] = this.mockServer.getRecords( + "res.users", + [["id", "=", this.publicUserId]], + { active_test: false } + ); + this.authenticate(publicUser.login, publicUser.password); + }, + /** + * Execute the provided function with the given user + * authenticated then restore the original user. + * + * @param {number} userId + * @param {Function} fn + */ + async withUser(userId, fn) { + const user = this.currentUser; + const targetUser = this.mockServer.getRecords("res.users", [["id", "=", userId]], { + active_test: false, + })[0]; + this._authenticate(targetUser); + let result; + try { + result = await fn(); + } finally { + if (user) { + this._authenticate(user); + } else { + this.logout(); + } + } + return result; + }, + /** + * The current user, either the one authenticated or the one + * impersonated by `withUser`. + */ + get currentUser() { + let user; + const currentUserId = this.cookie.get("sid"); + if ("res.users" in this.mockServer.models && currentUserId) { + user = this.mockServer.getRecords("res.users", [["id", "=", currentUserId]], { + active_test: false, + })[0]; + user = user ? { ...user, _is_public: () => user.id === this.publicUserId } : undefined; + } + return user; + }, + /** + * The current partner, either the one of the current user or + * the one of the user impersonated by `withUser`. + */ + get currentPartner() { + if ("res.partner" in this.mockServer.models && this.currentUser?.partner_id) { + return this.mockServer.getRecords( + "res.partner", + [["id", "=", this.currentUser?.partner_id]], + { active_test: false } + )[0]; + } + return undefined; + }, + get currentUserId() { + return this.currentUser?.id; + }, + get currentPartnerId() { + return this.currentPartner?.id; + }, + getData() { + return this.mockServer.models; + }, + getViews() { + return this.mockServer.archs; + }, + simulateConnectionLost(closeCode) { + this.mockServer._simulateConnectionLost(closeCode); + }, + ...TEST_USER_IDS, +}; + +let pyEnv; +/** + * Creates an environment that can be used to setup test data as well as + * creating data after test start. + * + * @param {Object} serverData serverData to pass to the mockServer. + * @param {Object} [serverData.action] actions to be passed to the mock + * server. + * @param {Object} [serverData.views] views to be passed to the mock + * server. + * @returns {Object} An environment that can be used to interact with + * the mock server (creation, deletion, update of records...) + */ +export async function startServer({ actions, views = {} } = {}) { + const models = {}; + const modelDefinitions = await modelDefinitionsPromise; + const recordsToInsertRegistry = registry + .category("bus.model.definitions") + .category("recordsToInsert"); + for (const [modelName, fields] of modelDefinitions) { + const records = []; + if (recordsToInsertRegistry.contains(modelName)) { + // prevent tests from mutating the records. + records.push(...JSON.parse(JSON.stringify(recordsToInsertRegistry.get(modelName)))); + } + models[modelName] = { fields: { ...fields }, records }; + + // generate default views for this model if none were passed. + const viewArchsSubRegistries = registry.category("bus.view.archs").subRegistries; + for (const [viewType, archsRegistry] of Object.entries(viewArchsSubRegistries)) { + views[`${modelName},false,${viewType}`] = + views[`${modelName},false,${viewType}`] || + archsRegistry.get(modelName, archsRegistry.get("default")); + } + } + pyEnv = new Proxy(pyEnvTarget, { + get(target, name) { + if (name in target) { + return target[name]; + } + const modelAPI = { + /** + * Simulate a 'create' operation on a model. + * + * @param {Object[]|Object} values records to be created. + * @returns {integer[]|integer} array of ids if more than one value was passed, + * id of created record otherwise. + */ + create(values) { + if (!values) { + return; + } + if (!Array.isArray(values)) { + values = [values]; + } + const recordIds = values.map((value) => + target.mockServer.mockCreate(name, value) + ); + return recordIds.length === 1 ? recordIds[0] : recordIds; + }, + /** + * Simulate a 'search' operation on a model. + * + * @param {Array} domain + * @param {Object} context + * @returns {integer[]} array of ids corresponding to the given domain. + */ + search(domain, context = {}) { + return target.mockServer.mockSearch(name, [domain], context); + }, + /** + * Simulate a `search_count` operation on a model. + * + * @param {Array} domain + * @return {number} count of records matching the given domain. + */ + searchCount(domain) { + return this.search(domain).length; + }, + /** + * Simulate a 'search_read' operation on a model. + * + * @param {Array} domain + * @param {Object} kwargs + * @returns {Object[]} array of records corresponding to the given domain. + */ + searchRead(domain, kwargs = {}) { + return target.mockServer.mockSearchRead(name, [domain], kwargs); + }, + /** + * Simulate an 'unlink' operation on a model. + * + * @param {integer[]} ids + * @returns {boolean} mockServer 'unlink' method always returns true. + */ + unlink(ids) { + return target.mockServer.mockUnlink(name, [ids]); + }, + /** + * Simulate a 'write' operation on a model. + * + * @param {integer[]} ids ids of records to write on. + * @param {Object} values values to write on the records matching given ids. + * @returns {boolean} mockServer 'write' method always returns true. + */ + write(ids, values) { + return target.mockServer.mockWrite(name, [ids, values]); + }, + }; + if (name === "bus.bus") { + modelAPI["_sendone"] = target.mockServer._mockBusBus__sendone.bind( + target.mockServer + ); + modelAPI["_sendmany"] = target.mockServer._mockBusBus__sendmany.bind( + target.mockServer + ); + } + return modelAPI; + }, + }); + pyEnv["mockServer"] = await makeMockServer({ actions, models, views }); + pyEnv["mockServer"].pyEnv = pyEnv; + registerCleanup(() => { + pyEnv = undefined; + _cookie = {}; + }); + if ("res.users" in pyEnv.mockServer.models) { + const adminUser = pyEnv["res.users"].searchRead([["id", "=", pyEnv.adminUserId]])[0]; + pyEnv.authenticate(adminUser.login, adminUser.password); + } + return pyEnv; +} + +/** + * + * @returns {Object} An environment that can be used to interact with the mock + * server (creation, deletion, update of records...) + */ +export function getPyEnv() { + return pyEnv || startServer(); +} diff --git a/static/tests/helpers/mock_server.js b/static/tests/helpers/mock_server.js new file mode 100644 index 0000000..7667fe4 --- /dev/null +++ b/static/tests/helpers/mock_server.js @@ -0,0 +1,172 @@ +/** @odoo-module **/ + +import { TEST_USER_IDS } from "@bus/../tests/helpers/test_constants"; +import { patchWebsocketWorkerWithCleanup } from "@bus/../tests/helpers/mock_websocket"; + +import { patch } from "@web/core/utils/patch"; +import { MockServer } from "@web/../tests/helpers/mock_server"; +import { registry } from "@web/core/registry"; + +QUnit.testDone(() => { + const callbackRegistry = registry.category("mock_server_websocket_callbacks"); + callbackRegistry.getEntries().map(([key]) => callbackRegistry.remove(key)); +}); + +patch(MockServer.prototype, { + init() { + super.init(...arguments); + Object.assign(this, TEST_USER_IDS); + const self = this; + this.notificationQueue = []; + this.websocketWorker = patchWebsocketWorkerWithCleanup({ + _sendToServer(message) { + self._performWebsocketRequest(message); + super._sendToServer(message); + }, + }); + this.lastBusNotificationId = 0; + this.channelsByUser = {}; + for (const modelName in this.models) { + const records = Array.isArray(this.models[modelName].records) + ? this.models[modelName].records + : []; + for (const record of records) { + Object.defineProperty(record, "__model", { value: modelName }); + } + } + }, + + mockCreate(modelName, valsList, kwargs = {}) { + const result = super.mockCreate(modelName, valsList, kwargs); + const returnArrayOfIds = Array.isArray(result); + const recordIds = Array.isArray(result) ? result : [result]; + for (const recordId of recordIds) { + const record = this.models[modelName].records.find((r) => r.id === recordId); + Object.defineProperty(record, "__model", { value: modelName }); + } + return returnArrayOfIds ? recordIds : recordIds[0]; + }, + + mockSearchRead(modelName, domain, kwargs = {}) { + const records = super.mockSearchRead(modelName, domain, kwargs); + for (const record of records) { + Object.defineProperty(record, "__model", { value: modelName }); + } + return records; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @param {Object} message Message sent through the websocket to the + * server. + * @param {string} [message.event_name] + * @param {any} [message.data] + */ + _performWebsocketRequest({ event_name, data }) { + if (event_name === "update_presence") { + const { inactivity_period, im_status_ids_by_model } = data; + this._mockIrWebsocket__updatePresence(inactivity_period, im_status_ids_by_model); + } else if (event_name === "subscribe") { + const { channels } = data; + this.channelsByUser[this.pyEnv?.currentUser] = this.pyEnv + ? this._mockIrWebsocket__buildBusChannelList(channels) + : channels; + } + const callbackFn = registry + .category("mock_server_websocket_callbacks") + .get(event_name, null); + if (callbackFn) { + callbackFn(data); + } + }, + /** + * Simulates `_sendone` on `bus.bus`. + * + * @param {string} channel + * @param {string} notificationType + * @param {any} message + */ + _mockBusBus__sendone(channel, notificationType, message) { + this._mockBusBus__sendmany([[channel, notificationType, message]]); + }, + /** + * Simulates `_sendmany` on `bus.bus`. + * + * @param {Array} notifications + */ + _mockBusBus__sendmany(notifications) { + if (!notifications.length) { + return; + } + const values = []; + const authenticatedUserId = + "res.users" in this.models + ? this.pyEnv.cookie.get("authenticated_user_sid") + : undefined; + const authenticatedUser = authenticatedUserId + ? this.pyEnv["res.users"].searchRead([["id", "=", authenticatedUserId]], { + context: { active_test: false }, + })[0] + : null; + const channels = + this.channelsByUser[authenticatedUser] ?? this._mockIrWebsocket__buildBusChannelList(); + notifications = notifications.filter(([target]) => + channels.some((channel) => { + if (typeof target === "string") { + return channel === target; + } + if (Array.isArray(channel) !== Array.isArray(target)) { + return false; + } + if (Array.isArray(channel)) { + const { __model: cModel, id: cId } = channel[0]; + const { __model: tModel, id: tId } = target[0]; + return cModel === tModel && cId === tId && channel[1] === target[1]; + } + return channel?.__model === target.__model && channel?.id === target?.id; + }) + ); + if (notifications.length === 0) { + return; + } + for (const notification of notifications) { + const [type, payload] = notification.slice(1, notification.length); + values.push({ id: ++this.lastBusNotificationId, message: { payload, type } }); + } + this.notificationQueue.push(...values); + if (this.notificationQueue.length === values.length) { + this._planNotificationSending(); + } + }, + + /** + * Helper to send the pending notifications to the client. This method is + * push to the micro task queue to simulate server-side batching of + * notifications. + */ + _planNotificationSending() { + queueMicrotask(() => { + if (this.debug) { + console.group("%c[BUS]", "color: #c6e; font-weight: bold;"); + for (const { message } of this.notificationQueue) { + console.log(message.type, message.payload); + } + console.groupEnd(); + } + this.websocketWorker.broadcast("notification", this.notificationQueue); + this.notificationQueue = []; + }); + }, + /** + * Simulate the lost of the connection by simulating a closeEvent on + * the worker websocket. + * + * @param {number} closeCode the code to close the connection with. + */ + _simulateConnectionLost(closeCode) { + this.websocketWorker.websocket.close(closeCode); + }, +}); diff --git a/static/tests/helpers/mock_server/models/ir_websocket.js b/static/tests/helpers/mock_server/models/ir_websocket.js new file mode 100644 index 0000000..0c76a52 --- /dev/null +++ b/static/tests/helpers/mock_server/models/ir_websocket.js @@ -0,0 +1,58 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { MockServer } from "@web/../tests/helpers/mock_server"; + +patch(MockServer.prototype, { + /** + * Simulates `_update_presence` on `ir.websocket`. + * + * @param inactivityPeriod + * @param imStatusIdsByModel + */ + _mockIrWebsocket__updatePresence(inactivityPeriod, imStatusIdsByModel) { + const imStatusNotifications = this._mockIrWebsocket__getImStatus(imStatusIdsByModel); + if (Object.keys(imStatusNotifications).length > 0) { + this._mockBusBus__sendone( + this.pyEnv.currentPartner, + "mail.record/insert", + imStatusNotifications + ); + } + }, + /** + * Simulates `_get_im_status` on `ir.websocket`. + * + * @param {Object} imStatusIdsByModel + * @param {Number[]|undefined} res.partner ids of res.partners whose im_status + * should be monitored. + */ + _mockIrWebsocket__getImStatus(imStatusIdsByModel) { + const imStatus = {}; + const { "res.partner": partnerIds } = imStatusIdsByModel; + if (partnerIds) { + imStatus["Persona"] = this.mockSearchRead("res.partner", [[["id", "in", partnerIds]]], { + context: { active_test: false }, + fields: ["im_status"], + }).map((p) => ({ ...p, type: "partner" })); + } + return imStatus; + }, + /** + * Simulates `_build_bus_channel_list` on `ir.websocket`. + */ + _mockIrWebsocket__buildBusChannelList(channels = []) { + channels = [...channels]; + channels.push("broadcast"); + const authenticatedUserId = this.pyEnv.cookie.get("authenticated_user_sid"); + const authenticatedPartner = authenticatedUserId + ? this.pyEnv["res.partner"].searchRead([["user_ids", "in", [authenticatedUserId]]], { + context: { active_test: false }, + })[0] + : null; + if (authenticatedPartner) { + channels.push(authenticatedPartner); + } + return channels; + }, +}); diff --git a/static/tests/helpers/mock_services.js b/static/tests/helpers/mock_services.js new file mode 100644 index 0000000..5452c70 --- /dev/null +++ b/static/tests/helpers/mock_services.js @@ -0,0 +1,15 @@ +/** @odoo-module **/ + +import { presenceService } from "@bus/services/presence_service"; + +export function makeFakePresenceService(params = {}) { + return { + ...presenceService, + start(env) { + return { + ...presenceService.start(env), + ...params, + }; + }, + }; +} diff --git a/static/tests/helpers/mock_websocket.js b/static/tests/helpers/mock_websocket.js new file mode 100644 index 0000000..32b5fa5 --- /dev/null +++ b/static/tests/helpers/mock_websocket.js @@ -0,0 +1,108 @@ +/** @odoo-module **/ + +import { WebsocketWorker } from "@bus/workers/websocket_worker"; +import { browser } from "@web/core/browser/browser"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; +import { registerCleanup } from "@web/../tests/helpers/cleanup"; + +class WebSocketMock extends EventTarget { + constructor() { + super(); + this.readyState = 0; + + queueMicrotask(() => { + this.readyState = 1; + const openEv = new Event("open"); + this.onopen(openEv); + this.dispatchEvent(openEv); + }); + } + + close(code = 1000, reason) { + this.readyState = 3; + const closeEv = new CloseEvent("close", { + code, + reason, + wasClean: code === 1000, + }); + this.onclose(closeEv); + this.dispatchEvent(closeEv); + } + + onclose(closeEv) {} + onerror(errorEv) {} + onopen(openEv) {} + + send(data) { + if (this.readyState !== 1) { + const errorEv = new Event("error"); + this.onerror(errorEv); + this.dispatchEvent(errorEv); + throw new DOMException("Failed to execute 'send' on 'WebSocket': State is not OPEN"); + } + } +} + +class SharedWorkerMock extends EventTarget { + constructor(websocketWorker) { + super(); + this._websocketWorker = websocketWorker; + this._messageChannel = new MessageChannel(); + this.port = this._messageChannel.port1; + // port 1 should be started by the service itself. + this._messageChannel.port2.start(); + this._websocketWorker.registerClient(this._messageChannel.port2); + } +} + +class WorkerMock extends SharedWorkerMock { + constructor(websocketWorker) { + super(websocketWorker); + this.port.start(); + this.postMessage = this.port.postMessage.bind(this.port); + } +} + +let websocketWorker; +/** + * @param {*} params Parameters used to patch the websocket worker. + * @returns {WebsocketWorker} Instance of the worker which will run during the + * test. Usefull to interact with the worker in order to test the + * websocket behavior. + */ +export function patchWebsocketWorkerWithCleanup(params = {}) { + patchWithCleanup(window, { + WebSocket: function () { + return new WebSocketMock(); + }, + }); + patchWithCleanup(websocketWorker || WebsocketWorker.prototype, params); + websocketWorker = websocketWorker || new WebsocketWorker(); + websocketWorker.INITIAL_RECONNECT_DELAY = 0; + websocketWorker.RECONNECT_JITTER = 0; + patchWithCleanup(browser, { + SharedWorker: function () { + const sharedWorker = new SharedWorkerMock(websocketWorker); + registerCleanup(() => { + sharedWorker._messageChannel.port1.close(); + sharedWorker._messageChannel.port2.close(); + }); + return sharedWorker; + }, + Worker: function () { + const worker = new WorkerMock(websocketWorker); + registerCleanup(() => { + worker._messageChannel.port1.close(); + worker._messageChannel.port2.close(); + }); + return worker; + }, + }); + registerCleanup(() => { + if (websocketWorker) { + clearTimeout(websocketWorker.connectTimeout); + websocketWorker = null; + } + }); + return websocketWorker; +} diff --git a/static/tests/helpers/model_definitions_helpers.js b/static/tests/helpers/model_definitions_helpers.js new file mode 100644 index 0000000..74ea54c --- /dev/null +++ b/static/tests/helpers/model_definitions_helpers.js @@ -0,0 +1,57 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; + +const modelDefinitionsRegistry = registry.category("bus.model.definitions"); +const customModelFieldsRegistry = modelDefinitionsRegistry.category("fieldsToInsert"); +const recordsToInsertRegistry = modelDefinitionsRegistry.category("recordsToInsert"); +const fakeModelsRegistry = modelDefinitionsRegistry.category("fakeModels"); +/** + * Add models whose definitions need to be fetched on the server. + * + * @param {string[]} modelName + */ +export function addModelNamesToFetch(modelNames) { + if (!modelDefinitionsRegistry.contains("modelNamesToFetch")) { + modelDefinitionsRegistry.add("modelNamesToFetch", []); + } + modelDefinitionsRegistry.get("modelNamesToFetch").push(...modelNames); +} + +/** + * Add models that will be added to the model definitions. We should + * avoid to rely on fake models and use real models instead. + * + * @param {string} modelName + * @param {Object} fields + */ +export function addFakeModel(modelName, fields) { + fakeModelsRegistry.add(modelName, fields); +} + +/** + * Add model fields that are not present on the server side model's definitions + * but are required to ease testing or add default values for existing fields. + * + * @param {string} modelName + * @param {Object} fieldNamesToFields + */ +export function insertModelFields(modelName, fieldNamesToFields) { + const modelCustomFieldsRegistry = customModelFieldsRegistry.category(modelName); + for (const fname in fieldNamesToFields) { + modelCustomFieldsRegistry.add(fname, fieldNamesToFields[fname]); + } +} + +/** + * Add records to the initial server data. + * + * @param {string} modelName + * @param {Object[]} records + */ +export function insertRecords(modelName, records) { + if (!recordsToInsertRegistry.contains(modelName)) { + recordsToInsertRegistry.add(modelName, []); + } + recordsToInsertRegistry.get(modelName).push(...records); +} diff --git a/static/tests/helpers/model_definitions_setup.js b/static/tests/helpers/model_definitions_setup.js new file mode 100644 index 0000000..3c660d3 --- /dev/null +++ b/static/tests/helpers/model_definitions_setup.js @@ -0,0 +1,79 @@ +/** @odoo-module **/ + +import { TEST_GROUP_IDS, TEST_USER_IDS } from "@bus/../tests/helpers/test_constants"; +import { + addModelNamesToFetch, + insertModelFields, + insertRecords, +} from "@bus/../tests/helpers/model_definitions_helpers"; + +//-------------------------------------------------------------------------- +// Models +//-------------------------------------------------------------------------- + +addModelNamesToFetch([ + "ir.attachment", + "ir.model", + "ir.model.fields", + "res.company", + "res.country", + "res.groups", + "res.partner", + "res.users", +]); + +//-------------------------------------------------------------------------- +// Insertion of fields +//-------------------------------------------------------------------------- + +insertModelFields("res.partner", { + description: { string: "description", type: "text" }, + is_company: { default: () => false }, +}); + +//-------------------------------------------------------------------------- +// Insertion of records +//-------------------------------------------------------------------------- + +insertRecords("res.company", [{ id: 1 }]); +insertRecords("res.groups", [{ id: TEST_GROUP_IDS.groupUserId, name: "Internal User" }]); +insertRecords("res.users", [ + { + display_name: "Your Company, Mitchell Admin", + id: TEST_USER_IDS.adminUserId, + name: "Mitchell Admin", + login: "admin", + password: "admin", + partner_id: TEST_USER_IDS.adminPartnerId, + }, + { + active: false, + display_name: "Public user", + login: "public", + password: "public", + id: TEST_USER_IDS.publicUserId, + name: "Public user", + partner_id: TEST_USER_IDS.publicPartnerId, + }, +]); +insertRecords("res.partner", [ + { + active: false, + display_name: "Public user", + name: "Public user", + id: TEST_USER_IDS.publicPartnerId, + is_public: true, + }, + { + display_name: "Your Company, Mitchell Admin", + id: TEST_USER_IDS.adminPartnerId, + name: "Mitchell Admin", + }, + { + active: false, + display_name: "OdooBot", + id: TEST_USER_IDS.odoobotId, + im_status: "bot", + name: "OdooBot", + }, +]); diff --git a/static/tests/helpers/test_constants.js b/static/tests/helpers/test_constants.js new file mode 100644 index 0000000..efbbd29 --- /dev/null +++ b/static/tests/helpers/test_constants.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +export const TEST_GROUP_IDS = { + groupUserId: 11, +}; + +export const TEST_USER_IDS = { + odoobotId: 2, + adminPartnerId: 3, + adminUserId: 2, + publicPartnerId: 4, + publicUserId: 3, +}; diff --git a/static/tests/helpers/test_utils.js b/static/tests/helpers/test_utils.js new file mode 100644 index 0000000..110a7a7 --- /dev/null +++ b/static/tests/helpers/test_utils.js @@ -0,0 +1,17 @@ +/* @odoo-module */ + +import { busParametersService } from "@bus/bus_parameters_service"; +import { busService } from "@bus/services/bus_service"; +import { multiTabService } from "@bus/multi_tab_service"; +import { presenceService } from "@bus/services/presence_service"; + +import { registry } from "@web/core/registry"; + +export function addBusServicesToRegistry() { + registry + .category("services") + .add("bus.parameters", busParametersService) + .add("bus_service", busService) + .add("presence", presenceService) + .add("multi_tab", multiTabService); +} diff --git a/static/tests/helpers/view_definitions_setup.js b/static/tests/helpers/view_definitions_setup.js new file mode 100644 index 0000000..07a6fe8 --- /dev/null +++ b/static/tests/helpers/view_definitions_setup.js @@ -0,0 +1,30 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; + +const viewArchsRegistry = registry.category("bus.view.archs"); +const activityArchsRegistry = viewArchsRegistry.category("activity"); +const formArchsRegistry = viewArchsRegistry.category("form"); +const kanbanArchsRegistry = viewArchsRegistry.category("kanban"); +const listArchsRegistry = viewArchsRegistry.category("list"); +const searchArchsRegistry = viewArchsRegistry.category("search"); + +activityArchsRegistry.add("default", ""); +formArchsRegistry.add("default", "
"); +kanbanArchsRegistry.add("default", ""); +listArchsRegistry.add("default", ""); +searchArchsRegistry.add("default", ""); + +formArchsRegistry.add( + "res.partner", + ` + + + +
+ + + +
+ ` +); diff --git a/static/tests/helpers/websocket_event_deferred.js b/static/tests/helpers/websocket_event_deferred.js new file mode 100644 index 0000000..0ab3b94 --- /dev/null +++ b/static/tests/helpers/websocket_event_deferred.js @@ -0,0 +1,226 @@ +/* @odoo-module */ + +import { patchWebsocketWorkerWithCleanup } from "@bus/../tests/helpers/mock_websocket"; + +import { registry } from "@web/core/registry"; +import { patch } from "@web/core/utils/patch"; +import { registerCleanup } from "@web/../tests/helpers/cleanup"; +import { makeDeferred } from "@web/../tests/helpers/utils"; + +// should be enough to decide whether or not notifications/channel +// subscriptions... are received. +const TIMEOUT = 500; +const callbackRegistry = registry.category("mock_server_websocket_callbacks"); + +/** + * Returns a deferred that resolves when a websocket subscription is + * done. If channels are provided, the deferred will only resolve when + * we subscribe to all of them. + * + * @param {...string} [requiredChannels] + * @returns {import("@web/core/utils/concurrency").Deferred} + */ +export function waitUntilSubscribe(...requiredChannels) { + const subscribeDeferred = makeDeferred(); + const failTimeout = setTimeout(() => { + const errMsg = `Subscription to ${JSON.stringify(requiredChannels)} not received.`; + subscribeDeferred.reject(new Error(errMsg)); + QUnit.assert.ok(false, errMsg); + }, TIMEOUT); + const lastCallback = callbackRegistry.get("subscribe", () => {}); + callbackRegistry.add( + "subscribe", + (data) => { + const { channels } = data; + lastCallback(data); + const allChannelsSubscribed = requiredChannels.every((channel) => + channels.includes(channel) + ); + if (allChannelsSubscribed) { + subscribeDeferred.resolve(); + QUnit.assert.ok( + true, + `Subscription to ${JSON.stringify(requiredChannels)} received.` + ); + clearTimeout(failTimeout); + } + }, + { force: true } + ); + return subscribeDeferred; +} + +/** + * Returns a deferred that resolves when the given channel(s) addition/deletion + * is notified to the websocket worker. + * + * @param {string[]} channels + * @param {object} [options={}] + * @param {"add"|"delete"} [options.operation="add"] + * + * @returns {import("@web/core/utils/concurrency").Deferred} */ +export function waitForChannels(channels, { operation = "add" } = {}) { + const missingChannels = new Set(channels); + const deferred = makeDeferred(); + function check({ crashOnFail = false } = {}) { + const success = missingChannels.size === 0; + if (!success && !crashOnFail) { + return; + } + unpatch(); + clearTimeout(failTimeout); + const msg = success + ? `Channel(s) [${channels.join(", ")}] ${operation === "add" ? "added" : "deleted"}.` + : `Waited ${TIMEOUT}ms for [${channels.join(", ")}] to be ${ + operation === "add" ? "added" : "deleted" + }`; + QUnit.assert.ok(success, msg); + if (success) { + deferred.resolve(); + } else { + deferred.reject(new Error(msg)); + } + } + const failTimeout = setTimeout(() => check({ crashOnFail: true }), TIMEOUT); + registerCleanup(() => { + if (missingChannels.length > 0) { + check({ crashOnFail: true }); + } + }); + const worker = patchWebsocketWorkerWithCleanup(); + const workerMethod = operation === "add" ? "_addChannel" : "_deleteChannel"; + const unpatch = patch(worker, { + async [workerMethod](client, channel) { + await super[workerMethod](client, channel); + missingChannels.delete(channel); + check(); + }, + }); + return deferred; +} + +/** + * @typedef {Object} ExpectedNotificationOptions + * @property {boolean} [received=true] + * @typedef {[env: import("@web/env").OdooEnv, notificationType: string, notificationPayload: any, options: ExpectedNotificationOptions]} ExpectedNotification + */ + +/** + * Wait for a notification to be received/not received. Returns + * a deferred that resolves when the assertion is done. + * + * @param {ExpectedNotification} notification + * @returns {import("@web/core/utils/concurrency").Deferred} + */ +function _waitNotification(notification) { + const [env, type, payload, { received = true } = {}] = notification; + const notificationDeferred = makeDeferred(); + const failTimeout = setTimeout(() => { + QUnit.assert.ok( + !received, + `Notification of type "${type}" with payload ${payload} not received.` + ); + env.services["bus_service"].removeEventListener("notification", callback); + notificationDeferred.resolve(); + }, TIMEOUT); + const callback = ({ detail: notifications }) => { + for (const notification of notifications) { + if (notification.type !== type) { + continue; + } + if ( + payload === undefined || + JSON.stringify(notification.payload) === JSON.stringify(payload) + ) { + QUnit.assert.ok( + received, + `Notification of type "${type}" with payload ${JSON.stringify( + notification.payload + )} receveived.` + ); + notificationDeferred.resolve(); + clearTimeout(failTimeout); + env.services["bus_service"].removeEventListener("notification", callback); + } + } + }; + env.services["bus_service"].addEventListener("notification", callback); + return notificationDeferred; +} + +/** + * Wait for the expected notifications to be received/not received. Returns + * a deferred that resolves when the assertion is done. + * + * @param {ExpectedNotification[]} expectedNotifications + * @returns {import("@web/core/utils/concurrency").Deferred} + */ +export function waitNotifications(...expectedNotifications) { + return Promise.all( + expectedNotifications.map((expectedNotification) => _waitNotification(expectedNotification)) + ); +} + +/** + * Returns a deferred that resolves when an event matching the given type is + * received from the bus service. + * + * @typedef {"connect"|"disconnect"|"reconnect"|"reconnecting"|"notification"} EventType + * @param {import("@web/env").OdooEnv} env + * @param {EventType} eventType + * @param {object} [options={}] + * @param {boolean} [options.received=true] + */ +export function waitForBusEvent(env, eventType, { received = true } = {}) { + const eventReceivedDeferred = makeDeferred(); + const failTimeout = setTimeout(() => { + env.services["bus_service"].removeEventListener(eventType, callback); + QUnit.assert.ok( + !received, + received + ? `Waited ${TIMEOUT}ms for ${eventType} event.` + : `Event of type "${eventType}" not received.` + ); + eventReceivedDeferred.resolve(); + }, TIMEOUT); + const callback = () => { + env.services["bus_service"].removeEventListener(eventType, callback); + QUnit.assert.ok(received, `Event of type "${eventType}" received.`); + eventReceivedDeferred.resolve(); + clearTimeout(failTimeout); + }; + env.services["bus_service"].addEventListener(eventType, callback); + return eventReceivedDeferred; +} + +/** + * Returns a deferred that resolves when an event matching the given type is + * received by the websocket worker. + * + * @param {import("@web/env").OdooEnv} env + * @param {import("@bus/workers/websocket_worker").WorkerAction} targetAction + * @param {object} [options={}] + * @param {boolean} [options.received=true] + */ +export function waitForWorkerEvent(targetAction) { + const eventReiceivedDeferred = makeDeferred(); + const failTimeout = setTimeout(() => { + unpatch(); + QUnit.assert.ok(false, `Waited ${TIMEOUT}ms for ${targetAction} to be received.`); + eventReiceivedDeferred.resolve(); + }, TIMEOUT); + const worker = patchWebsocketWorkerWithCleanup(); + const unpatch = patch(worker, { + _onClientMessage(_, { action }) { + super._onClientMessage(...arguments); + if (targetAction === action) { + unpatch(); + QUnit.assert.ok(true, `Action "${action}" received.`); + eventReiceivedDeferred.resolve(); + clearTimeout(failTimeout); + } + }, + }); + registerCleanup(unpatch); + return eventReiceivedDeferred; +} diff --git a/static/tests/multi_tab_service_tests.js b/static/tests/multi_tab_service_tests.js new file mode 100644 index 0000000..3ba887b --- /dev/null +++ b/static/tests/multi_tab_service_tests.js @@ -0,0 +1,95 @@ +/** @odoo-module **/ + +import { addBusServicesToRegistry } from "@bus/../tests/helpers/test_utils"; + +import { browser } from "@web/core/browser/browser"; +import { makeTestEnv } from "@web/../tests/helpers/mock_env"; +import { patchWithCleanup, nextTick } from "@web/../tests/helpers/utils"; + +QUnit.module("multi_tab_service_tests.js"); + +QUnit.test("multi tab service elects new master on pagehide", async function (assert) { + addBusServicesToRegistry(); + const firstTabEnv = await makeTestEnv(); + assert.ok(firstTabEnv.services["multi_tab"].isOnMainTab(), "only tab should be the main one"); + + // prevent second tab from receiving pagehide event. + patchWithCleanup(browser, { + addEventListener(eventName, callback) { + if (eventName === "pagehide") { + return; + } + super.addEventListener(eventName, callback); + }, + }); + const secondTabEnv = await makeTestEnv(); + firstTabEnv.services["multi_tab"].bus.addEventListener("no_longer_main_tab", () => + assert.step("tab1 no_longer_main_tab") + ); + secondTabEnv.services["multi_tab"].bus.addEventListener("no_longer_main_tab", () => + assert.step("tab2 no_longer_main_tab") + ); + window.dispatchEvent(new Event("pagehide")); + + // Let the multi tab elect a new main. + await nextTick(); + assert.notOk(firstTabEnv.services["multi_tab"].isOnMainTab()); + assert.ok(secondTabEnv.services["multi_tab"].isOnMainTab()); + assert.verifySteps(["tab1 no_longer_main_tab"]); +}); + +QUnit.test("multi tab allow to share values between tabs", async function (assert) { + addBusServicesToRegistry(); + const firstTabEnv = await makeTestEnv(); + const secondTabEnv = await makeTestEnv(); + + firstTabEnv.services["multi_tab"].setSharedValue("foo", 1); + assert.deepEqual(secondTabEnv.services["multi_tab"].getSharedValue("foo"), 1); + firstTabEnv.services["multi_tab"].setSharedValue("foo", 2); + assert.deepEqual(secondTabEnv.services["multi_tab"].getSharedValue("foo"), 2); + + firstTabEnv.services["multi_tab"].removeSharedValue("foo"); + assert.notOk(secondTabEnv.services["multi_tab"].getSharedValue("foo")); +}); + +QUnit.test("multi tab triggers shared_value_updated", async function (assert) { + addBusServicesToRegistry(); + const firstTabEnv = await makeTestEnv(); + const secondTabEnv = await makeTestEnv(); + + secondTabEnv.services["multi_tab"].bus.addEventListener( + "shared_value_updated", + ({ detail }) => { + assert.step(`${detail.key} - ${JSON.parse(detail.newValue)}`); + } + ); + firstTabEnv.services["multi_tab"].setSharedValue("foo", "bar"); + firstTabEnv.services["multi_tab"].setSharedValue("foo", "foo"); + firstTabEnv.services["multi_tab"].removeSharedValue("foo"); + + await nextTick(); + assert.verifySteps(["foo - bar", "foo - foo", "foo - null"]); +}); + +QUnit.test("multi tab triggers become_master", async function (assert) { + addBusServicesToRegistry(); + await makeTestEnv(); + // prevent second tab from receiving pagehide event. + patchWithCleanup(browser, { + addEventListener(eventName, callback) { + if (eventName === "pagehide") { + return; + } + super.addEventListener(eventName, callback); + }, + }); + const secondTabEnv = await makeTestEnv(); + secondTabEnv.services["multi_tab"].bus.addEventListener("become_main_tab", () => + assert.step("become_main_tab") + ); + window.dispatchEvent(new Event("pagehide")); + + // Let the multi tab elect a new main. + await nextTick(); + assert.verifySteps(["become_main_tab"]); +}); diff --git a/static/tests/simple_notification_tests.js b/static/tests/simple_notification_tests.js new file mode 100644 index 0000000..e11e24f --- /dev/null +++ b/static/tests/simple_notification_tests.js @@ -0,0 +1,69 @@ +/* @odoo-module */ + +import { simpleNotificationService } from "@bus/simple_notification_service"; +import { addBusServicesToRegistry } from "@bus/../tests/helpers/test_utils"; +import { startServer } from "@bus/../tests/helpers/mock_python_environment"; + +import { registry } from "@web/core/registry"; +import { browser } from "@web/core/browser/browser"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; +import { contains } from "@web/../tests/utils"; +import { createWebClient } from "@web/../tests/webclient/helpers"; + +QUnit.module("simple_notification"); + +QUnit.test("receive and display simple notification with message", async () => { + const pyEnv = await startServer(); + addBusServicesToRegistry(); + registry.category("services").add("simple_notification", simpleNotificationService); + await createWebClient({}); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "simple_notification", { + message: "simple notification", + }); + await contains(".o_notification_content", { text: "simple notification" }); +}); + +QUnit.test("receive and display simple notification with title", async () => { + const pyEnv = await startServer(); + addBusServicesToRegistry(); + registry.category("services").add("simple_notification", simpleNotificationService); + await createWebClient({}); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "simple_notification", { + message: "simple notification", + title: "simple title", + }); + await contains(".o_notification_title", { text: "simple title" }); +}); + +QUnit.test("receive and display simple notification with specific type", async () => { + const pyEnv = await startServer(); + addBusServicesToRegistry(); + registry.category("services").add("simple_notification", simpleNotificationService); + await createWebClient({}); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "simple_notification", { + message: "simple notification", + type: "info", + }); + await contains(".o_notification.border-info"); +}); + +QUnit.test("receive and display simple notification as sticky", async () => { + const pyEnv = await startServer(); + addBusServicesToRegistry(); + registry.category("services").add("simple_notification", simpleNotificationService); + await createWebClient({}); + patchWithCleanup(browser, { + setTimeout(fn) { + /** + * Non-sticky notifications are removed after a delay. If thenotification is still + * present when this delay is set to 0 it means it is a sticky one. + */ + return super.setTimeout(fn, 0); + }, + }); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "simple_notification", { + message: "simple notification", + sticky: true, + }); + await contains(".o_notification"); +}); diff --git a/static/tests/websocket_worker_tests.js b/static/tests/websocket_worker_tests.js new file mode 100644 index 0000000..46a3862 --- /dev/null +++ b/static/tests/websocket_worker_tests.js @@ -0,0 +1,94 @@ +/** @odoo-module */ + +import { WEBSOCKET_CLOSE_CODES } from "@bus/workers/websocket_worker"; +import { patchWebsocketWorkerWithCleanup } from "@bus/../tests/helpers/mock_websocket"; + +import { nextTick, patchWithCleanup } from "@web/../tests/helpers/utils"; + +QUnit.module("Websocket Worker"); + +QUnit.test("connect event is broadcasted after calling start", async function (assert) { + const worker = patchWebsocketWorkerWithCleanup({ + broadcast(type) { + assert.step(`broadcast ${type}`); + }, + }); + worker._start(); + // Wait for the websocket to connect. + await nextTick(); + assert.verifySteps(["broadcast connect"]); +}); + +QUnit.test("disconnect event is broadcasted", async function (assert) { + const worker = patchWebsocketWorkerWithCleanup({ + broadcast(type) { + assert.step(`broadcast ${type}`); + }, + }); + worker._start(); + // Wait for the websocket to connect. + await nextTick(); + worker.websocket.close(WEBSOCKET_CLOSE_CODES.CLEAN); + // Wait for the websocket to disconnect. + await nextTick(); + + assert.verifySteps(["broadcast connect", "broadcast disconnect"]); +}); + +QUnit.test("reconnecting/reconnect event is broadcasted", async function (assert) { + // Patch setTimeout in order for the worker to reconnect immediatly. + patchWithCleanup(window, { + setTimeout: (fn) => fn(), + }); + const worker = patchWebsocketWorkerWithCleanup({ + broadcast(type) { + assert.step(`broadcast ${type}`); + }, + }); + worker._start(); + // Wait for the websocket to connect. + await nextTick(); + worker.websocket.close(WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE); + // Wait for the disconnect/reconnecting/reconnect events. + await nextTick(); + + assert.verifySteps([ + "broadcast connect", + "broadcast disconnect", + "broadcast reconnecting", + "broadcast reconnect", + ]); +}); + +QUnit.test("notification event is broadcasted", async function (assert) { + const notifications = [ + { + id: 70, + message: { + type: "bundle_changed", + payload: { + server_version: "15.5alpha1+e", + }, + }, + }, + ]; + const worker = patchWebsocketWorkerWithCleanup({ + broadcast(type, message) { + if (type === "notification") { + assert.step(`broadcast ${type}`); + assert.deepEqual(message, notifications); + } + }, + }); + worker._start(); + // Wait for the websocket to connect. + await nextTick(); + + worker.websocket.dispatchEvent( + new MessageEvent("message", { + data: JSON.stringify(notifications), + }) + ); + + assert.verifySteps(["broadcast notification"]); +}); diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..ad34fef --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,9 @@ +from . import common +from . import test_assetsbundle +from . import test_health +from . import test_ir_model +from . import test_ir_websocket +from . import test_notify +from . import test_websocket_caryall +from . import test_websocket_controller +from . import test_websocket_rate_limiting diff --git a/tests/common.py b/tests/common.py new file mode 100644 index 0000000..1fe0398 --- /dev/null +++ b/tests/common.py @@ -0,0 +1,139 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json +import struct +from threading import Event +import unittest +from unittest.mock import patch + +try: + import websocket +except ImportError: + websocket = None + +import odoo.tools +from odoo.tests import HOST, HttpCase +from ..websocket import CloseCode, Websocket, WebsocketConnectionHandler +from ..models.bus import dispatch, hashable, channel_with_db + + +class WebsocketCase(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + if websocket is None: + cls._logger.warning("websocket-client module is not installed") + raise unittest.SkipTest("websocket-client module is not installed") + cls._WEBSOCKET_URL = f"ws://{HOST}:{odoo.tools.config['http_port']}/websocket" + websocket_allowed_patch = patch.object(WebsocketConnectionHandler, "websocket_allowed", return_value=True) + cls.startClassPatcher(websocket_allowed_patch) + + def setUp(self): + super().setUp() + self._websockets = set() + # Used to ensure websocket connections have been closed + # properly. + self._websocket_events = set() + original_serve_forever = WebsocketConnectionHandler._serve_forever + + def _mocked_serve_forever(*args): + websocket_closed_event = Event() + self._websocket_events.add(websocket_closed_event) + original_serve_forever(*args) + websocket_closed_event.set() + + self._serve_forever_patch = patch.object( + WebsocketConnectionHandler, + '_serve_forever', + wraps=_mocked_serve_forever + ) + self.startPatcher(self._serve_forever_patch) + + def tearDown(self): + self._close_websockets() + super().tearDown() + + def _close_websockets(self): + """ + Close all the connected websockets and wait for the connection + to terminate. + """ + for ws in self._websockets: + if ws.connected: + ws.close(CloseCode.CLEAN) + self.wait_remaining_websocket_connections() + + def websocket_connect(self, *args, **kwargs): + """ + Connect a websocket. If no cookie is given, the connection is + opened with a default session. The created websocket is closed + at the end of the test. + """ + if 'cookie' not in kwargs: + self.session = self.authenticate(None, None) + kwargs['cookie'] = f'session_id={self.session.sid}' + if 'timeout' not in kwargs: + kwargs['timeout'] = 5 + ws = websocket.create_connection( + type(self)._WEBSOCKET_URL, *args, **kwargs + ) + ws.ping() + ws.recv_data_frame(control_frame=True) # pong + self._websockets.add(ws) + return ws + + def subscribe(self, websocket, channels=None, last=None, wait_for_dispatch=True): + """ Subscribe the websocket to the given channels. + + :param websocket: The websocket of the client. + :param channels: The list of channels to subscribe to. + :param last: The last notification id the client received. + :param wait_for_dispatch: Whether to wait for the notification + dispatching trigerred by the subscription. + """ + dispatch_bus_notification_done = Event() + original_dispatch_bus_notifications = Websocket._dispatch_bus_notifications + + def _mocked_dispatch_bus_notifications(self, *args): + original_dispatch_bus_notifications(self, *args) + dispatch_bus_notification_done.set() + + with patch.object(Websocket, '_dispatch_bus_notifications', _mocked_dispatch_bus_notifications): + sub = {'event_name': 'subscribe', 'data': { + 'channels': channels or [], + }} + if last: + sub['data']['last'] = last + websocket.send(json.dumps(sub)) + if wait_for_dispatch: + dispatch_bus_notification_done.wait(timeout=5) + + def trigger_notification_dispatching(self, channels): + """ Notify the websockets subscribed to the given channels that new + notifications are available. Usefull since the bus is not able to do + it during tests. + """ + channels = [ + hashable(channel_with_db(self.registry.db_name, c)) for c in channels + ] + websockets = set() + for channel in channels: + websockets.update(dispatch._channels_to_ws.get(hashable(channel), [])) + for websocket in websockets: + websocket.trigger_notification_dispatching() + + def wait_remaining_websocket_connections(self): + """ Wait for the websocket connections to terminate. """ + for event in self._websocket_events: + event.wait(5) + + def assert_close_with_code(self, websocket, expected_code): + """ + Assert that the websocket is closed with the expected_code. + """ + opcode, payload = websocket.recv_data() + # ensure it's a close frame + self.assertEqual(opcode, 8) + code = struct.unpack('!H', payload[:2])[0] + # ensure the close code is the one we expected + self.assertEqual(code, expected_code) diff --git a/tests/test_assetsbundle.py b/tests/test_assetsbundle.py new file mode 100644 index 0000000..2a5e810 --- /dev/null +++ b/tests/test_assetsbundle.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import odoo.tests +from odoo.osv import expression + + +@odoo.tests.tagged('post_install', '-at_install', 'assets_bundle') +class BusWebTests(odoo.tests.HttpCase): + + def test_bundle_sends_bus(self): + """ + Tests two things: + - Messages are posted to the bus when assets change + i.e. their hash has been recomputed and differ from the attachment's + - The interface deals with those bus messages by displaying one notification + """ + # start from a clean slate + self.env['ir.attachment'].search([('name', 'ilike', 'web.assets_%')]).unlink() + self.env.registry.clear_cache() + + sendones = [] + def patched_sendone(self, channel, notificationType, message): + """ Control API and number of messages posted to the bus linked to + bundle_changed events """ + if notificationType == 'bundle_changed': + sendones.append((channel, message)) + + self.patch(type(self.env['bus.bus']), '_sendone', patched_sendone) + + self.assertEqual(self.url_open('/web/assets/any/web.assets_web.min.js', allow_redirects=False).status_code, 200) + self.assertEqual(self.url_open('/web/assets/any/web.assets_web.min.css', allow_redirects=False).status_code, 200) + self.assertEqual(self.url_open('/web/assets/any/web.assets_backend.min.js', allow_redirects=False).status_code, 200) + self.assertEqual(self.url_open('/web/assets/any/web.assets_backend.min.css', allow_redirects=False).status_code, 200) + + # One sendone for each asset bundle and for each CSS / JS + self.assertEqual( + len(sendones), + 2, + 'Received %s' % '\n'.join('%s - %s' % (tmp[0], tmp[1]) for tmp in sendones) + ) + for (channel, message) in sendones: + self.assertEqual(channel, 'broadcast') + self.assertEqual(len(message), 1) + self.assertTrue(isinstance(message.get('server_version'), str)) diff --git a/tests/test_health.py b/tests/test_health.py new file mode 100644 index 0000000..67f9b31 --- /dev/null +++ b/tests/test_health.py @@ -0,0 +1,12 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import HttpCase + + +class TestBusController(HttpCase): + def test_health(self): + response = self.url_open('/websocket/health') + self.assertEqual(response.status_code, 200) + payload = response.json() + self.assertEqual(payload['status'], 'pass') + self.assertFalse(response.cookies.get('session_id')) diff --git a/tests/test_ir_model.py b/tests/test_ir_model.py new file mode 100644 index 0000000..963c558 --- /dev/null +++ b/tests/test_ir_model.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo +from odoo.tests import HttpCase + + +@odoo.tests.tagged('-at_install', 'post_install') +class TestGetModelDefinitions(HttpCase): + def test_access_cr(self): + """ Checks that get_model_definitions does not return anything else than models """ + with self.assertRaises(KeyError): + self.env['ir.model']._get_model_definitions(['res.users', 'cr']) + + def test_access_all_model_fields(self): + """ + Check that get_model_definitions return all the models + and their fields + """ + model_definitions = self.env['ir.model']._get_model_definitions([ + 'res.users', 'res.partner' + ]) + # models are retrieved + self.assertIn('res.users', model_definitions) + self.assertIn('res.partner', model_definitions) + # check that model fields are retrieved + self.assertTrue( + all(fname in model_definitions['res.users'].keys() for fname in ['email', 'name', 'partner_id']) + ) + self.assertTrue( + all(fname in model_definitions['res.partner'].keys() for fname in ['active', 'date', 'name']) + ) + + def test_relational_fields_with_missing_model(self): + """ + Check that get_model_definitions only returns relational fields + if the model is requested + """ + model_definitions = self.env['ir.model']._get_model_definitions([ + 'res.partner' + ]) + # since res.country is not requested, country_id shouldn't be in + # the model definition fields + self.assertNotIn('country_id', model_definitions['res.partner']) + + model_definitions = self.env['ir.model']._get_model_definitions([ + 'res.partner', 'res.country', + ]) + # res.country is requested, country_id should be present on res.partner + self.assertIn('country_id', model_definitions['res.partner']) diff --git a/tests/test_ir_websocket.py b/tests/test_ir_websocket.py new file mode 100644 index 0000000..c6eec9d --- /dev/null +++ b/tests/test_ir_websocket.py @@ -0,0 +1,13 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import common + + +class TestIrWebsocket(common.HttpCase): + def test_only_allow_string_channels_from_frontend(self): + with self.assertRaises(ValueError): + self.env['ir.websocket']._subscribe({ + 'inactivity_period': 1000, + 'last': 0, + 'channels': [('odoo', 'discuss.channel', 5)], + }) diff --git a/tests/test_notify.py b/tests/test_notify.py new file mode 100644 index 0000000..3fd6437 --- /dev/null +++ b/tests/test_notify.py @@ -0,0 +1,49 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import BaseCase + +from ..models.bus import json_dump, get_notify_payloads, NOTIFY_PAYLOAD_MAX_LENGTH + + +class NotifyTests(BaseCase): + + def test_get_notify_payloads(self): + """ + Asserts that the implementation of `get_notify_payloads` + actually splits correctly large payloads + """ + def check_payloads_size(payloads): + for payload in payloads: + self.assertLess(len(payload.encode()), NOTIFY_PAYLOAD_MAX_LENGTH) + + channel = ('dummy_db', 'dummy_model', 12345) + channels = [channel] + self.assertLess(len(json_dump(channels).encode()), NOTIFY_PAYLOAD_MAX_LENGTH) + payloads = get_notify_payloads(channels) + self.assertEqual(len(payloads), 1, + "The payload is less then the threshold, " + "there should be 1 payload only, as it shouldn't be split") + channels = [channel] * 100 + self.assertLess(len(json_dump(channels).encode()), NOTIFY_PAYLOAD_MAX_LENGTH) + payloads = get_notify_payloads(channels) + self.assertEqual(len(payloads), 1, + "The payload is less then the threshold, " + "there should be 1 payload only, as it shouldn't be split") + check_payloads_size(payloads) + channels = [channel] * 1000 + self.assertGreaterEqual(len(json_dump(channels).encode()), NOTIFY_PAYLOAD_MAX_LENGTH) + payloads = get_notify_payloads(channels) + self.assertGreater(len(payloads), 1, + "Payload was larger than the threshold, it should've been split") + check_payloads_size(payloads) + + fat_channel = tuple(item * 1000 for item in channel) + channels = [fat_channel] + self.assertEqual(len(channels), 1, "There should be only 1 channel") + self.assertGreaterEqual(len(json_dump(channels).encode()), NOTIFY_PAYLOAD_MAX_LENGTH) + payloads = get_notify_payloads(channels) + self.assertEqual(len(payloads), 1, + "Payload was larger than the threshold, but shouldn't be split, " + "as it contains only 1 channel") + with self.assertRaises(AssertionError): + check_payloads_size(payloads) diff --git a/tests/test_websocket_caryall.py b/tests/test_websocket_caryall.py new file mode 100644 index 0000000..f9d5f15 --- /dev/null +++ b/tests/test_websocket_caryall.py @@ -0,0 +1,258 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import gc +import json +import os +from collections import defaultdict +from datetime import timedelta +from freezegun import freeze_time +from threading import Event +from unittest.mock import patch +from weakref import WeakSet +try: + from websocket._exceptions import WebSocketBadStatusException +except ImportError: + pass + +from odoo.api import Environment +from odoo.tests import common, new_test_user +from .common import WebsocketCase +from .. import websocket as websocket_module +from ..models.bus import dispatch +from ..models.ir_websocket import IrWebsocket +from ..websocket import ( + CloseCode, + Frame, + Opcode, + TimeoutManager, + TimeoutReason, + Websocket, + WebsocketConnectionHandler, +) + +@common.tagged('post_install', '-at_install') +class TestWebsocketCaryall(WebsocketCase): + def test_lifecycle_hooks(self): + events = [] + with patch.object(Websocket, '_Websocket__event_callbacks', defaultdict(set)): + @Websocket.onopen + def onopen(env, websocket): # pylint: disable=unused-variable + self.assertIsInstance(env, Environment) + self.assertIsInstance(websocket, Websocket) + events.append('open') + + @Websocket.onclose + def onclose(env, websocket): # pylint: disable=unused-variable + self.assertIsInstance(env, Environment) + self.assertIsInstance(websocket, Websocket) + events.append('close') + + ws = self.websocket_connect() + ws.close(CloseCode.CLEAN) + self.wait_remaining_websocket_connections() + self.assertEqual(events, ['open', 'close']) + + def test_instances_weak_set(self): + with patch.object(websocket_module, "_websocket_instances", WeakSet()): + first_ws = self.websocket_connect() + second_ws = self.websocket_connect() + self.assertEqual(len(websocket_module._websocket_instances), 2) + first_ws.close(CloseCode.CLEAN) + second_ws.close(CloseCode.CLEAN) + self.wait_remaining_websocket_connections() + # serve_forever_patch prevent websocket instances from being + # collected. Stop it now. + self._serve_forever_patch.stop() + gc.collect() + self.assertEqual(len(websocket_module._websocket_instances), 0) + + def test_timeout_manager_no_response_timeout(self): + with freeze_time('2022-08-19') as frozen_time: + timeout_manager = TimeoutManager() + # A PING frame was just sent, if no pong has been received + # within TIMEOUT seconds, the connection should have timed out. + timeout_manager.acknowledge_frame_sent(Frame(Opcode.PING)) + self.assertEqual(timeout_manager._awaited_opcode, Opcode.PONG) + frozen_time.tick(delta=timedelta(seconds=TimeoutManager.TIMEOUT / 2)) + self.assertFalse(timeout_manager.has_timed_out()) + frozen_time.tick(delta=timedelta(seconds=TimeoutManager.TIMEOUT / 2)) + self.assertTrue(timeout_manager.has_timed_out()) + self.assertEqual(timeout_manager.timeout_reason, TimeoutReason.NO_RESPONSE) + + timeout_manager = TimeoutManager() + # A CLOSE frame was just sent, if no close has been received + # within TIMEOUT seconds, the connection should have timed out. + timeout_manager.acknowledge_frame_sent(Frame(Opcode.CLOSE)) + self.assertEqual(timeout_manager._awaited_opcode, Opcode.CLOSE) + frozen_time.tick(delta=timedelta(seconds=TimeoutManager.TIMEOUT / 2)) + self.assertFalse(timeout_manager.has_timed_out()) + frozen_time.tick(delta=timedelta(seconds=TimeoutManager.TIMEOUT / 2)) + self.assertTrue(timeout_manager.has_timed_out()) + self.assertEqual(timeout_manager.timeout_reason, TimeoutReason.NO_RESPONSE) + + def test_timeout_manager_keep_alive_timeout(self): + with freeze_time('2022-08-19') as frozen_time: + timeout_manager = TimeoutManager() + frozen_time.tick(delta=timedelta(seconds=timeout_manager._keep_alive_timeout / 2)) + self.assertFalse(timeout_manager.has_timed_out()) + frozen_time.tick(delta=timedelta(seconds=timeout_manager._keep_alive_timeout / 2 + 1)) + self.assertTrue(timeout_manager.has_timed_out()) + self.assertEqual(timeout_manager.timeout_reason, TimeoutReason.KEEP_ALIVE) + + def test_timeout_manager_reset_wait_for(self): + timeout_manager = TimeoutManager() + # PING frame + timeout_manager.acknowledge_frame_sent(Frame(Opcode.PING)) + self.assertEqual(timeout_manager._awaited_opcode, Opcode.PONG) + timeout_manager.acknowledge_frame_receipt(Frame(Opcode.PONG)) + self.assertIsNone(timeout_manager._awaited_opcode) + + # CLOSE frame + timeout_manager.acknowledge_frame_sent(Frame(Opcode.CLOSE)) + self.assertEqual(timeout_manager._awaited_opcode, Opcode.CLOSE) + timeout_manager.acknowledge_frame_receipt(Frame(Opcode.CLOSE)) + self.assertIsNone(timeout_manager._awaited_opcode) + + def test_user_login(self): + websocket = self.websocket_connect() + new_test_user(self.env, login='test_user', password='Password!1') + self.authenticate('test_user', 'Password!1') + # The session with whom the websocket connected has been + # deleted. WebSocket should disconnect in order for the + # session to be updated. + self.subscribe(websocket, wait_for_dispatch=False) + self.assert_close_with_code(websocket, CloseCode.SESSION_EXPIRED) + + def test_user_logout_incoming_message(self): + new_test_user(self.env, login='test_user', password='Password!1') + user_session = self.authenticate('test_user', 'Password!1') + websocket = self.websocket_connect(cookie=f'session_id={user_session.sid};') + self.url_open('/web/session/logout') + # The session with whom the websocket connected has been + # deleted. WebSocket should disconnect in order for the + # session to be updated. + self.subscribe(websocket, wait_for_dispatch=False) + self.assert_close_with_code(websocket, CloseCode.SESSION_EXPIRED) + + def test_user_logout_outgoing_message(self): + new_test_user(self.env, login='test_user', password='Password!1') + user_session = self.authenticate('test_user', 'Password!1') + websocket = self.websocket_connect(cookie=f'session_id={user_session.sid};') + self.subscribe(websocket, ['channel1'], self.env['bus.bus']._bus_last_id()) + self.url_open('/web/session/logout') + # Simulate postgres notify. The session with whom the websocket + # connected has been deleted. WebSocket should be closed without + # receiving the message. + self.env['bus.bus']._sendone('channel1', 'notif type', 'message') + self.trigger_notification_dispatching(["channel1"]) + self.assert_close_with_code(websocket, CloseCode.SESSION_EXPIRED) + + def test_channel_subscription_disconnect(self): + websocket = self.websocket_connect() + self.subscribe(websocket, ['my_channel'], self.env['bus.bus']._bus_last_id()) + # channel is added as expected to the channel to websocket map. + self.assertIn((self.env.registry.db_name, 'my_channel'), dispatch._channels_to_ws) + websocket.close(CloseCode.CLEAN) + self.wait_remaining_websocket_connections() + # channel is removed as expected when removing the last + # websocket that was listening to this channel. + self.assertNotIn((self.env.registry.db_name, 'my_channel'), dispatch._channels_to_ws) + + def test_channel_subscription_update(self): + websocket = self.websocket_connect() + self.subscribe(websocket, ['my_channel'], self.env['bus.bus']._bus_last_id()) + # channel is added as expected to the channel to websocket map. + self.assertIn((self.env.registry.db_name, 'my_channel'), dispatch._channels_to_ws) + self.subscribe(websocket, ['my_channel_2'], self.env['bus.bus']._bus_last_id()) + # channel is removed as expected when updating the subscription. + self.assertNotIn((self.env.registry.db_name, 'my_channel'), dispatch._channels_to_ws) + + def test_trigger_notification(self): + websocket = self.websocket_connect() + self.subscribe(websocket, ['my_channel'], self.env['bus.bus']._bus_last_id()) + self.env['bus.bus']._sendone('my_channel', 'notif_type', 'message') + self.trigger_notification_dispatching(["my_channel"]) + notifications = json.loads(websocket.recv()) + self.assertEqual(1, len(notifications)) + self.assertEqual(notifications[0]['message']['type'], 'notif_type') + self.assertEqual(notifications[0]['message']['payload'], 'message') + self.env['bus.bus']._sendone('my_channel', 'notif_type', 'another_message') + self.trigger_notification_dispatching(["my_channel"]) + notifications = json.loads(websocket.recv()) + # First notification has been received, we should only receive + # the second one. + self.assertEqual(1, len(notifications)) + self.assertEqual(notifications[0]['message']['type'], 'notif_type') + self.assertEqual(notifications[0]['message']['payload'], 'another_message') + + def test_subscribe_higher_last_notification_id(self): + server_last_notification_id = self.env['bus.bus'].sudo().search([], limit=1, order='id desc').id or 0 + client_last_notification_id = server_last_notification_id + 1 + + with patch.object(Websocket, 'subscribe', side_effect=Websocket.subscribe, autospec=True) as mock: + websocket = self.websocket_connect() + self.subscribe(websocket, ['my_channel'], client_last_notification_id) + self.assertEqual(mock.call_args[0][2], 0) + + def test_subscribe_lower_last_notification_id(self): + server_last_notification_id = self.env['bus.bus'].sudo().search([], limit=1, order='id desc').id or 0 + client_last_notification_id = server_last_notification_id - 1 + + with patch.object(Websocket, 'subscribe', side_effect=Websocket.subscribe, autospec=True) as mock: + websocket = self.websocket_connect() + self.subscribe(websocket, ['my_channel'], client_last_notification_id) + self.assertEqual(mock.call_args[0][2], client_last_notification_id) + + def test_subscribe_to_custom_channel(self): + channel = self.env["res.partner"].create({"name": "John"}) + websocket = self.websocket_connect() + with patch.object(IrWebsocket, "_build_bus_channel_list", return_value=[channel]): + self.subscribe(websocket, [], self.env['bus.bus']._bus_last_id()) + self.env["bus.bus"]._sendmany([ + (channel, "notif_on_global_channel", "message"), + ((channel, "PRIVATE"), "notif_on_private_channel", "message"), + ]) + self.trigger_notification_dispatching([channel, (channel, "PRIVATE")]) + notifications = json.loads(websocket.recv()) + self.assertEqual(len(notifications), 1) + self.assertEqual(notifications[0]['message']['type'], 'notif_on_global_channel') + self.assertEqual(notifications[0]['message']['payload'], 'message') + + with patch.object(IrWebsocket, "_build_bus_channel_list", return_value=[(channel, "PRIVATE")]): + self.subscribe(websocket, [], self.env['bus.bus']._bus_last_id()) + self.env["bus.bus"]._sendmany([ + (channel, "notif_on_global_channel", "message"), + ((channel, "PRIVATE"), "notif_on_private_channel", "message"), + ]) + self.trigger_notification_dispatching([channel, (channel, "PRIVATE")]) + notifications = json.loads(websocket.recv()) + self.assertEqual(len(notifications), 1) + self.assertEqual(notifications[0]['message']['type'], 'notif_on_private_channel') + self.assertEqual(notifications[0]['message']['payload'], 'message') + + def test_no_cursor_when_no_callback_for_lifecycle_event(self): + with patch.object(Websocket, '_Websocket__event_callbacks', defaultdict(set)): + with patch('odoo.addons.bus.websocket.acquire_cursor') as mock: + self.websocket_connect() + self.assertFalse(mock.called) + + @patch.dict(os.environ, {"ODOO_BUS_PUBLIC_SAMESITE_WS": "True"}) + def test_public_configuration(self): + new_test_user(self.env, login='test_user', password='Password!1') + user_session = self.authenticate('test_user', 'Password!1') + serve_forever_called_event = Event() + original_serve_forever = WebsocketConnectionHandler._serve_forever + + def serve_forever(websocket, *args): + original_serve_forever(websocket, *args) + self.assertNotEqual(websocket._session.sid, user_session.sid) + self.assertNotEqual(websocket._session.uid, user_session.uid) + serve_forever_called_event.set() + + with patch.object(WebsocketConnectionHandler, '_serve_forever', side_effect=serve_forever) as mock: + self.websocket_connect( + cookie=f'session_id={user_session.sid};', + origin="http://example.com" + ) + serve_forever_called_event.wait(timeout=5) + self.assertTrue(mock.called) diff --git a/tests/test_websocket_controller.py b/tests/test_websocket_controller.py new file mode 100644 index 0000000..0ff5297 --- /dev/null +++ b/tests/test_websocket_controller.py @@ -0,0 +1,71 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json + +from odoo.tests import JsonRpcException +from odoo.addons.base.tests.common import HttpCaseWithUserDemo + + +class TestWebsocketController(HttpCaseWithUserDemo): + def test_websocket_peek(self): + result = self.make_jsonrpc_request('/websocket/peek_notifications', { + 'channels': [], + 'last': 0, + 'is_first_poll': True, + }) + + # Response containing channels/notifications is retrieved and is + # conform to excpectations. + self.assertIsNotNone(result) + channels = result.get('channels') + self.assertIsNotNone(channels) + self.assertIsInstance(channels, list) + notifications = result.get('notifications') + self.assertIsNotNone(notifications) + self.assertIsInstance(notifications, list) + + result = self.make_jsonrpc_request('/websocket/peek_notifications', { + 'channels': [], + 'last': 0, + 'is_first_poll': False, + }) + + # Reponse is received as long as the session is valid. + self.assertIsNotNone(result) + + def test_websocket_peek_session_expired_login(self): + session = self.authenticate(None, None) + # first rpc should be fine + self.make_jsonrpc_request('/websocket/peek_notifications', { + 'channels': [], + 'last': 0, + 'is_first_poll': True, + }) + + self.authenticate('admin', 'admin') + # rpc with outdated session should lead to error. + headers = {'Cookie': f'session_id={session.sid};'} + with self.assertRaises(JsonRpcException, msg='odoo.http.SessionExpiredException'): + self.make_jsonrpc_request('/websocket/peek_notifications', { + 'channels': [], + 'last': 0, + 'is_first_poll': False, + }, headers=headers) + + def test_websocket_peek_session_expired_logout(self): + session = self.authenticate('demo', 'demo') + # first rpc should be fine + self.make_jsonrpc_request('/websocket/peek_notifications', { + 'channels': [], + 'last': 0, + 'is_first_poll': True, + }) + self.url_open('/web/session/logout') + # rpc with outdated session should lead to error. + headers = {'Cookie': f'session_id={session.sid};'} + with self.assertRaises(JsonRpcException, msg='odoo.http.SessionExpiredException'): + self.make_jsonrpc_request('/websocket/peek_notifications', { + 'channels': [], + 'last': 0, + 'is_first_poll': False, + }, headers=headers) diff --git a/tests/test_websocket_rate_limiting.py b/tests/test_websocket_rate_limiting.py new file mode 100644 index 0000000..b1063d4 --- /dev/null +++ b/tests/test_websocket_rate_limiting.py @@ -0,0 +1,66 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json +import time + +try: + from websocket._exceptions import WebSocketProtocolException +except ImportError: + pass + +from odoo.tests import common +from .common import WebsocketCase +from ..websocket import CloseCode, Websocket + +@common.tagged('post_install', '-at_install') +class TestWebsocketRateLimiting(WebsocketCase): + def test_rate_limiting_base_ok(self): + ws = self.websocket_connect() + + for _ in range(Websocket.RL_BURST + 1): + ws.send(json.dumps({'event_name': 'test_rate_limiting'})) + time.sleep(Websocket.RL_DELAY) + + def test_rate_limiting_base_ko(self): + ws = self.websocket_connect() + + # Websocket client's close codes are not up to date. Indeed, the + # 1013 close code results in a protocol exception while it is a + # valid, registered close code ("TRY LATER") : + # https://www.iana.org/assignments/websocket/websocket.xhtml + with self.assertRaises(WebSocketProtocolException) as cm: + for _ in range(Websocket.RL_BURST + 1): + ws.send(json.dumps({'event_name': 'test_rate_limiting'})) + self.assert_close_with_code(ws, CloseCode.TRY_LATER) + self.assertEqual(str(cm.exception), 'Invalid close opcode.') + + def test_rate_limiting_opening_burst(self): + ws = self.websocket_connect() + + # first RL_BURST requests are accepted. + for _ in range(Websocket.RL_BURST): + ws.send(json.dumps({'event_name': 'test_rate_limiting'})) + + # sending at a correct rate after burst should be accepted. + for _ in range(2): + time.sleep(Websocket.RL_DELAY) + ws.send(json.dumps({'event_name': 'test_rate_limiting'})) + + def test_rate_limiting_start_ok_end_ko(self): + ws = self.websocket_connect() + + # first requests are legit and should be accepted + for _ in range(Websocket.RL_BURST + 1): + ws.send(json.dumps({'event_name': 'test_rate_limiting'})) + time.sleep(Websocket.RL_DELAY) + + # Websocket client's close codes are not up to date. Indeed, the + # 1013 close code results in a protocol exception while it is a + # valid, registered close code ("TRY LATER") : + # https://www.iana.org/assignments/websocket/websocket.xhtml + with self.assertRaises(WebSocketProtocolException) as cm: + # those requests are illicit and should not be accepted. + for _ in range(Websocket.RL_BURST * 2): + ws.send(json.dumps({'event_name': 'test_rate_limiting'})) + self.assert_close_with_code(ws, CloseCode.TRY_LATER) + self.assertEqual(str(cm.exception), 'Invalid close opcode.') diff --git a/websocket.py b/websocket.py new file mode 100644 index 0000000..e2c899d --- /dev/null +++ b/websocket.py @@ -0,0 +1,949 @@ +import base64 +import functools +import hashlib +import json +import logging +import os +import psycopg2 +import random +import socket +import struct +import selectors +import threading +import time +from collections import defaultdict, deque +from contextlib import closing, suppress +from enum import IntEnum +from psycopg2.pool import PoolError +from urllib.parse import urlparse +from weakref import WeakSet + +from werkzeug.local import LocalStack +from werkzeug.exceptions import BadRequest, HTTPException, ServiceUnavailable + +import odoo +from odoo import api +from .models.bus import dispatch +from odoo.http import root, Request, Response, SessionExpiredException, get_default_session +from odoo.modules.registry import Registry +from odoo.service import model as service_model +from odoo.service.server import CommonServer +from odoo.service.security import check_session +from odoo.tools import config + +_logger = logging.getLogger(__name__) + + +MAX_TRY_ON_POOL_ERROR = 10 +DELAY_ON_POOL_ERROR = 0.03 + + +def acquire_cursor(db): + """ Try to acquire a cursor up to `MAX_TRY_ON_POOL_ERROR` """ + for tryno in range(1, MAX_TRY_ON_POOL_ERROR + 1): + with suppress(PoolError): + return odoo.registry(db).cursor() + time.sleep(random.uniform(DELAY_ON_POOL_ERROR, DELAY_ON_POOL_ERROR * tryno)) + raise PoolError('Failed to acquire cursor after %s retries' % MAX_TRY_ON_POOL_ERROR) + + +# ------------------------------------------------------ +# EXCEPTIONS +# ------------------------------------------------------ + +class UpgradeRequired(HTTPException): + code = 426 + description = "Wrong websocket version was given during the handshake" + + def get_headers(self, environ=None): + headers = super().get_headers(environ) + headers.append(( + 'Sec-WebSocket-Version', + '; '.join(WebsocketConnectionHandler.SUPPORTED_VERSIONS) + )) + return headers + + +class WebsocketException(Exception): + """ Base class for all websockets exceptions """ + + +class ConnectionClosed(WebsocketException): + """ + Raised when the other end closes the socket without performing + the closing handshake. + """ + + +class InvalidCloseCodeException(WebsocketException): + def __init__(self, code): + super().__init__(f"Invalid close code: {code}") + + +class InvalidDatabaseException(WebsocketException): + """ + When raised: the database probably does not exists anymore, the + database is corrupted or the database version doesn't match the + server version. + """ + + +class InvalidStateException(WebsocketException): + """ + Raised when an operation is forbidden in the current state. + """ + + +class InvalidWebsocketRequest(WebsocketException): + """ + Raised when a websocket request is invalid (format, wrong args). + """ + + +class PayloadTooLargeException(WebsocketException): + """ + Raised when a websocket message is too large. + """ + + +class ProtocolError(WebsocketException): + """ + Raised when a frame format doesn't match expectations. + """ + + +class RateLimitExceededException(Exception): + """ + Raised when a client exceeds the number of request in a given + time. + """ + + +# ------------------------------------------------------ +# WEBSOCKET LIFECYCLE +# ------------------------------------------------------ + + +class LifecycleEvent(IntEnum): + OPEN = 0 + CLOSE = 1 + + +# ------------------------------------------------------ +# WEBSOCKET +# ------------------------------------------------------ + + +class Opcode(IntEnum): + CONTINUE = 0x00 + TEXT = 0x01 + BINARY = 0x02 + CLOSE = 0x08 + PING = 0x09 + PONG = 0x0A + + +class CloseCode(IntEnum): + CLEAN = 1000 + GOING_AWAY = 1001 + PROTOCOL_ERROR = 1002 + INCORRECT_DATA = 1003 + ABNORMAL_CLOSURE = 1006 + INCONSISTENT_DATA = 1007 + MESSAGE_VIOLATING_POLICY = 1008 + MESSAGE_TOO_BIG = 1009 + EXTENSION_NEGOTIATION_FAILED = 1010 + SERVER_ERROR = 1011 + RESTART = 1012 + TRY_LATER = 1013 + BAD_GATEWAY = 1014 + SESSION_EXPIRED = 4001 + KEEP_ALIVE_TIMEOUT = 4002 + + +class ConnectionState(IntEnum): + OPEN = 0 + CLOSING = 1 + CLOSED = 2 + + +DATA_OP = {Opcode.TEXT, Opcode.BINARY} +CTRL_OP = {Opcode.CLOSE, Opcode.PING, Opcode.PONG} +HEARTBEAT_OP = {Opcode.PING, Opcode.PONG} + +VALID_CLOSE_CODES = { + code for code in CloseCode if code is not CloseCode.ABNORMAL_CLOSURE +} +CLEAN_CLOSE_CODES = {CloseCode.CLEAN, CloseCode.GOING_AWAY, CloseCode.RESTART} +RESERVED_CLOSE_CODES = range(3000, 5000) + +_XOR_TABLE = [bytes(a ^ b for a in range(256)) for b in range(256)] + + +class Frame: + def __init__( + self, + opcode, + payload=b'', + fin=True, + rsv1=False, + rsv2=False, + rsv3=False + ): + self.opcode = opcode + self.payload = payload + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + + +class CloseFrame(Frame): + def __init__(self, code, reason): + if code not in VALID_CLOSE_CODES and code not in RESERVED_CLOSE_CODES: + raise InvalidCloseCodeException(code) + payload = struct.pack('!H', code) + if reason: + payload += reason.encode('utf-8') + self.code = code + self.reason = reason + super().__init__(Opcode.CLOSE, payload) + + +_websocket_instances = WeakSet() + + +class Websocket: + __event_callbacks = defaultdict(set) + # Maximum size for a message in bytes, whether it is sent as one + # frame or many fragmented ones. + MESSAGE_MAX_SIZE = 2 ** 20 + # Proxies usually close a connection after 1 minute of inactivity. + # Therefore, a PING frame have to be sent if no frame is either sent + # or received within CONNECTION_TIMEOUT - 15 seconds. + CONNECTION_TIMEOUT = 60 + INACTIVITY_TIMEOUT = CONNECTION_TIMEOUT - 15 + # How many requests can be made in excess of the given rate. + RL_BURST = int(config['websocket_rate_limit_burst']) + # How many seconds between each request. + RL_DELAY = float(config['websocket_rate_limit_delay']) + + def __init__(self, sock, session): + # Session linked to the current websocket connection. + self._session = session + self._db = session.db + self.__socket = sock + self._close_sent = False + self._close_received = False + self._timeout_manager = TimeoutManager() + # Used for rate limiting. + self._incoming_frame_timestamps = deque(maxlen=self.RL_BURST) + # Used to notify the websocket that bus notifications are + # available. + self.__notif_sock_w, self.__notif_sock_r = socket.socketpair() + self._channels = set() + self._last_notif_sent_id = 0 + # Websocket start up + self.__selector = ( + selectors.PollSelector() + if odoo.evented and hasattr(selectors, 'PollSelector') + else selectors.DefaultSelector() + ) + self.__selector.register(self.__socket, selectors.EVENT_READ) + self.__selector.register(self.__notif_sock_r, selectors.EVENT_READ) + self.state = ConnectionState.OPEN + _websocket_instances.add(self) + self._trigger_lifecycle_event(LifecycleEvent.OPEN) + + # ------------------------------------------------------ + # PUBLIC METHODS + # ------------------------------------------------------ + + def get_messages(self): + while self.state is not ConnectionState.CLOSED: + try: + readables = { + selector_key[0].fileobj for selector_key in + self.__selector.select(self.INACTIVITY_TIMEOUT) + } + if self._timeout_manager.has_timed_out() and self.state is ConnectionState.OPEN: + self.disconnect( + CloseCode.ABNORMAL_CLOSURE + if self._timeout_manager.timeout_reason is TimeoutReason.NO_RESPONSE + else CloseCode.KEEP_ALIVE_TIMEOUT + ) + continue + if not readables: + self._send_ping_frame() + continue + if self.__notif_sock_r in readables: + self._dispatch_bus_notifications() + if self.__socket in readables: + message = self._process_next_message() + if message is not None: + yield message + except Exception as exc: + self._handle_transport_error(exc) + + def disconnect(self, code, reason=None): + """ + Initiate the closing handshake that is, send a close frame + to the other end which will then send us back an + acknowledgment. Upon the reception of this acknowledgment, + the `_terminate` method will be called to perform an + orderly shutdown. Note that we don't need to wait for the + acknowledgment if the connection was failed beforewards. + """ + if code is not CloseCode.ABNORMAL_CLOSURE: + self._send_close_frame(code, reason) + else: + self._terminate() + + @classmethod + def onopen(cls, func): + cls.__event_callbacks[LifecycleEvent.OPEN].add(func) + return func + + @classmethod + def onclose(cls, func): + cls.__event_callbacks[LifecycleEvent.CLOSE].add(func) + return func + + def subscribe(self, channels, last): + """ Subscribe to bus channels. """ + self._channels = channels + if self._last_notif_sent_id < last: + self._last_notif_sent_id = last + # Dispatch past notifications if there are any. + self.trigger_notification_dispatching() + + def trigger_notification_dispatching(self): + """ + Warn the socket that notifications are available. Ignore if a + dispatch is already planned or if the socket is already in the + closing state. + """ + if self.state is not ConnectionState.OPEN: + return + readables = { + selector_key[0].fileobj for selector_key in + self.__selector.select(0) + } + if self.__notif_sock_r not in readables: + # Send a random bit to mark the socket as readable. + self.__notif_sock_w.send(b'x') + + # ------------------------------------------------------ + # PRIVATE METHODS + # ------------------------------------------------------ + + def _get_next_frame(self): + # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + # +-+-+-+-+-------+-+-------------+-------------------------------+ + # |F|R|R|R| opcode|M| Payload len | Extended payload length | + # |I|S|S|S| (4) |A| (7) | (16/64) | + # |N|V|V|V| |S| | (if payload len==126/127) | + # | |1|2|3| |K| | | + # +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + # | Extended payload length continued, if payload len == 127 | + # + - - - - - - - - - - - - - - - +-------------------------------+ + # | |Masking-key, if MASK set to 1 | + # +-------------------------------+-------------------------------+ + # | Masking-key (continued) | Payload Data | + # +-------------------------------- - - - - - - - - - - - - - - - + + # : Payload Data continued ... : + # + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + # | Payload Data continued ... | + # +---------------------------------------------------------------+ + def recv_bytes(n): + """ Pull n bytes from the socket """ + data = bytearray() + while len(data) < n: + received_data = self.__socket.recv(n - len(data)) + if not received_data: + raise ConnectionClosed() + data.extend(received_data) + return data + + def is_bit_set(byte, n): + """ + Check whether nth bit of byte is set or not (from left + to right). + """ + return byte & (1 << (7 - n)) + + def apply_mask(payload, mask): + # see: https://www.willmcgugan.com/blog/tech/post/speeding-up-websockets-60x/ + a, b, c, d = (_XOR_TABLE[n] for n in mask) + payload[::4] = payload[::4].translate(a) + payload[1::4] = payload[1::4].translate(b) + payload[2::4] = payload[2::4].translate(c) + payload[3::4] = payload[3::4].translate(d) + return payload + + self._limit_rate() + first_byte, second_byte = recv_bytes(2) + fin, rsv1, rsv2, rsv3 = (is_bit_set(first_byte, n) for n in range(4)) + try: + opcode = Opcode(first_byte & 0b00001111) + except ValueError as exc: + raise ProtocolError(exc) + payload_length = second_byte & 0b01111111 + + if rsv1 or rsv2 or rsv3: + raise ProtocolError("Reserved bits must be unset") + if not is_bit_set(second_byte, 0): + raise ProtocolError("Frame must be masked") + if opcode in CTRL_OP: + if not fin: + raise ProtocolError("Control frames cannot be fragmented") + if payload_length > 125: + raise ProtocolError( + "Control frames payload must be smaller than 126" + ) + if payload_length == 126: + payload_length = struct.unpack('!H', recv_bytes(2))[0] + elif payload_length == 127: + payload_length = struct.unpack('!Q', recv_bytes(8))[0] + if payload_length > self.MESSAGE_MAX_SIZE: + raise PayloadTooLargeException() + + mask = recv_bytes(4) + payload = apply_mask(recv_bytes(payload_length), mask) + frame = Frame(opcode, bytes(payload), fin, rsv1, rsv2, rsv3) + self._timeout_manager.acknowledge_frame_receipt(frame) + return frame + + def _process_next_message(self): + """ + Process the next message coming throught the socket. If a + data message can be extracted, return its decoded payload. + As per the RFC, only control frames will be processed once + the connection reaches the closing state. + """ + frame = self._get_next_frame() + if frame.opcode in CTRL_OP: + self._handle_control_frame(frame) + return + if self.state is not ConnectionState.OPEN: + # After receiving a control frame indicating the connection + # should be closed, a peer discards any further data + # received. + return + if frame.opcode is Opcode.CONTINUE: + raise ProtocolError("Unexpected continuation frame") + message = frame.payload + if not frame.fin: + message = self._recover_fragmented_message(frame) + return ( + message.decode('utf-8') + if message is not None and frame.opcode is Opcode.TEXT else message + ) + + def _recover_fragmented_message(self, initial_frame): + message_fragments = bytearray(initial_frame.payload) + while True: + frame = self._get_next_frame() + if frame.opcode in CTRL_OP: + # Control frames can be received in the middle of a + # fragmented message, process them as soon as possible. + self._handle_control_frame(frame) + if self.state is not ConnectionState.OPEN: + return + continue + if frame.opcode is not Opcode.CONTINUE: + raise ProtocolError("A continuation frame was expected") + message_fragments.extend(frame.payload) + if len(message_fragments) > self.MESSAGE_MAX_SIZE: + raise PayloadTooLargeException() + if frame.fin: + return bytes(message_fragments) + + def _send(self, message): + if self.state is not ConnectionState.OPEN: + raise InvalidStateException( + "Trying to send a frame on a closed socket" + ) + opcode = Opcode.BINARY + if not isinstance(message, (bytes, bytearray)): + opcode = Opcode.TEXT + self._send_frame(Frame(opcode, message)) + + def _send_frame(self, frame): + if frame.opcode in CTRL_OP and len(frame.payload) > 125: + raise ProtocolError( + "Control frames should have a payload length smaller than 126" + ) + if isinstance(frame.payload, str): + frame.payload = frame.payload.encode('utf-8') + elif not isinstance(frame.payload, (bytes, bytearray)): + frame.payload = json.dumps(frame.payload).encode('utf-8') + + output = bytearray() + first_byte = ( + (0b10000000 if frame.fin else 0) + | (0b01000000 if frame.rsv1 else 0) + | (0b00100000 if frame.rsv2 else 0) + | (0b00010000 if frame.rsv3 else 0) + | frame.opcode + ) + payload_length = len(frame.payload) + if payload_length < 126: + output.extend( + struct.pack('!BB', first_byte, payload_length) + ) + elif payload_length < 65536: + output.extend( + struct.pack('!BBH', first_byte, 126, payload_length) + ) + else: + output.extend( + struct.pack('!BBQ', first_byte, 127, payload_length) + ) + output.extend(frame.payload) + self.__socket.sendall(output) + self._timeout_manager.acknowledge_frame_sent(frame) + if not isinstance(frame, CloseFrame): + return + self.state = ConnectionState.CLOSING + self._close_sent = True + if frame.code not in CLEAN_CLOSE_CODES or self._close_received: + return self._terminate() + # After sending a control frame indicating the connection + # should be closed, a peer does not send any further data. + self.__selector.unregister(self.__notif_sock_r) + + def _send_close_frame(self, code, reason=None): + """ Send a close frame. """ + self._send_frame(CloseFrame(code, reason)) + + def _send_ping_frame(self): + """ Send a ping frame """ + self._send_frame(Frame(Opcode.PING)) + + def _send_pong_frame(self, payload): + """ Send a pong frame """ + self._send_frame(Frame(Opcode.PONG, payload)) + + def _terminate(self): + """ Close the underlying TCP socket. """ + with suppress(OSError, TimeoutError): + self.__socket.shutdown(socket.SHUT_WR) + # Call recv until obtaining a return value of 0 indicating + # the other end has performed an orderly shutdown. A timeout + # is set to ensure the connection will be closed even if + # the other end does not close the socket properly. + self.__socket.settimeout(1) + while self.__socket.recv(4096): + pass + self.__selector.unregister(self.__socket) + self.__selector.close() + self.__socket.close() + self.state = ConnectionState.CLOSED + dispatch.unsubscribe(self) + self._trigger_lifecycle_event(LifecycleEvent.CLOSE) + + def _handle_control_frame(self, frame): + if frame.opcode is Opcode.PING: + self._send_pong_frame(frame.payload) + elif frame.opcode is Opcode.CLOSE: + self.state = ConnectionState.CLOSING + self._close_received = True + code, reason = CloseCode.CLEAN, None + if len(frame.payload) >= 2: + code = struct.unpack('!H', frame.payload[:2])[0] + reason = frame.payload[2:].decode('utf-8') + elif frame.payload: + raise ProtocolError("Malformed closing frame") + if not self._close_sent: + self._send_close_frame(code, reason) + else: + self._terminate() + + def _handle_transport_error(self, exc): + """ + Find out which close code should be sent according to given + exception and call `self.disconnect` in order to close the + connection cleanly. + """ + code, reason = CloseCode.SERVER_ERROR, str(exc) + if isinstance(exc, (ConnectionClosed, OSError)): + code = CloseCode.ABNORMAL_CLOSURE + elif isinstance(exc, (ProtocolError, InvalidCloseCodeException)): + code = CloseCode.PROTOCOL_ERROR + elif isinstance(exc, UnicodeDecodeError): + code = CloseCode.INCONSISTENT_DATA + elif isinstance(exc, PayloadTooLargeException): + code = CloseCode.MESSAGE_TOO_BIG + elif isinstance(exc, (PoolError, RateLimitExceededException)): + code = CloseCode.TRY_LATER + elif isinstance(exc, SessionExpiredException): + code = CloseCode.SESSION_EXPIRED + if code is CloseCode.SERVER_ERROR: + reason = None + registry = Registry(self._session.db) + sequence = registry.registry_sequence + registry = registry.check_signaling() + if sequence != registry.registry_sequence: + _logger.warning("Bus operation aborted; registry has been reloaded") + else: + _logger.error(exc, exc_info=True) + self.disconnect(code, reason) + + def _limit_rate(self): + """ + This method is a simple rate limiter designed not to allow + more than one request by `RL_DELAY` seconds. `RL_BURST` specify + how many requests can be made in excess of the given rate at the + begining. When requests are received too fast, raises the + `RateLimitExceededException`. + """ + now = time.time() + if len(self._incoming_frame_timestamps) >= self.RL_BURST: + elapsed_time = now - self._incoming_frame_timestamps[0] + if elapsed_time < self.RL_DELAY * self.RL_BURST: + raise RateLimitExceededException() + self._incoming_frame_timestamps.append(now) + + def _trigger_lifecycle_event(self, event_type): + """ + Trigger a lifecycle event that is, call every function + registered for this event type. Every callback is given both the + environment and the related websocket. + """ + if not self.__event_callbacks[event_type]: + return + with closing(acquire_cursor(self._db)) as cr: + env = api.Environment(cr, self._session.uid, self._session.context) + for callback in self.__event_callbacks[event_type]: + try: + service_model.retrying(functools.partial(callback, env, self), env) + except Exception: + _logger.warning( + 'Error during Websocket %s callback', + LifecycleEvent(event_type).name, + exc_info=True + ) + + def _dispatch_bus_notifications(self): + """ + Dispatch notifications related to the registered channels. If + the session is expired, close the connection with the + `SESSION_EXPIRED` close code. If no cursor can be acquired, + close the connection with the `TRY_LATER` close code. + """ + session = root.session_store.get(self._session.sid) + if not session: + raise SessionExpiredException() + with acquire_cursor(session.db) as cr: + env = api.Environment(cr, session.uid, session.context) + if session.uid is not None and not check_session(session, env): + raise SessionExpiredException() + # Mark the notification request as processed. + self.__notif_sock_r.recv(1) + notifications = env['bus.bus']._poll(self._channels, self._last_notif_sent_id) + if not notifications: + return + self._last_notif_sent_id = notifications[-1]['id'] + self._send(notifications) + + +class TimeoutReason(IntEnum): + KEEP_ALIVE = 0 + NO_RESPONSE = 1 + + +class TimeoutManager: + """ + This class handles the Websocket timeouts. If no response to a + PING/CLOSE frame is received after `TIMEOUT` seconds or if the + connection is opened for more than `self._keep_alive_timeout` seconds, + the connection is considered to have timed out. To determine if the + connection has timed out, use the `has_timed_out` method. + """ + TIMEOUT = 15 + # Timeout specifying how many seconds the connection should be kept + # alive. + KEEP_ALIVE_TIMEOUT = int(config['websocket_keep_alive_timeout']) + + def __init__(self): + super().__init__() + self._awaited_opcode = None + # Time in which the connection was opened. + self._opened_at = time.time() + # Custom keep alive timeout for each TimeoutManager to avoid multiple + # connections timing out at the same time. + self._keep_alive_timeout = ( + self.KEEP_ALIVE_TIMEOUT + random.uniform(0, self.KEEP_ALIVE_TIMEOUT / 2) + ) + self.timeout_reason = None + # Start time recorded when we started awaiting an answer to a + # PING/CLOSE frame. + self._waiting_start_time = None + + def acknowledge_frame_receipt(self, frame): + if self._awaited_opcode is frame.opcode: + self._awaited_opcode = None + self._waiting_start_time = None + + def acknowledge_frame_sent(self, frame): + """ + Acknowledge a frame was sent. If this frame is a PING/CLOSE + frame, start waiting for an answer. + """ + if self.has_timed_out(): + return + if frame.opcode is Opcode.PING: + self._awaited_opcode = Opcode.PONG + elif frame.opcode is Opcode.CLOSE: + self._awaited_opcode = Opcode.CLOSE + if self._awaited_opcode is not None: + self._waiting_start_time = time.time() + + def has_timed_out(self): + """ + Determine whether the connection has timed out or not. The + connection times out when the answer to a CLOSE/PING frame + is not received within `TIMEOUT` seconds or if the connection + is opened for more than `self._keep_alive_timeout` seconds. + """ + now = time.time() + if now - self._opened_at >= self._keep_alive_timeout: + self.timeout_reason = TimeoutReason.KEEP_ALIVE + return True + if self._awaited_opcode and now - self._waiting_start_time >= self.TIMEOUT: + self.timeout_reason = TimeoutReason.NO_RESPONSE + return True + return False + + +# ------------------------------------------------------ +# WEBSOCKET SERVING +# ------------------------------------------------------ + + +_wsrequest_stack = LocalStack() +wsrequest = _wsrequest_stack() + +class WebsocketRequest: + def __init__(self, db, httprequest, websocket): + self.db = db + self.httprequest = httprequest + self.session = None + self.ws = websocket + + def __enter__(self): + _wsrequest_stack.push(self) + return self + + def __exit__(self, *args): + _wsrequest_stack.pop() + + def serve_websocket_message(self, message): + try: + jsonrequest = json.loads(message) + event_name = jsonrequest['event_name'] # mandatory + except KeyError as exc: + raise InvalidWebsocketRequest( + f'Key {exc.args[0]!r} is missing from request' + ) from exc + except ValueError as exc: + raise InvalidWebsocketRequest( + f'Invalid JSON data, {exc.args[0]}' + ) from exc + data = jsonrequest.get('data') + self.session = self._get_session() + + try: + self.registry = Registry(self.db) + self.registry.check_signaling() + except ( + AttributeError, psycopg2.OperationalError, psycopg2.ProgrammingError + ) as exc: + raise InvalidDatabaseException() from exc + + with closing(acquire_cursor(self.db)) as cr: + self.env = api.Environment(cr, self.session.uid, self.session.context) + threading.current_thread().uid = self.env.uid + service_model.retrying( + functools.partial(self._serve_ir_websocket, event_name, data), + self.env, + ) + + def _serve_ir_websocket(self, event_name, data): + """ + Delegate most of the processing to the ir.websocket model + which is extensible by applications. Directly call the + appropriate ir.websocket method since only two events are + tolerated: `subscribe` and `update_presence`. + """ + self.env['ir.websocket']._authenticate() + if event_name == 'subscribe': + self.env['ir.websocket']._subscribe(data) + if event_name == 'update_presence': + self.env['ir.websocket']._update_bus_presence(**data) + + def _get_session(self): + session = root.session_store.get(self.ws._session.sid) + if not session: + raise SessionExpiredException() + return session + + def update_env(self, user=None, context=None, su=None): + """ + Update the environment of the current websocket request. + """ + Request.update_env(self, user, context, su) + + def update_context(self, **overrides): + """ + Override the environment context of the current request with the + values of ``overrides``. To replace the entire context, please + use :meth:`~update_env` instead. + """ + self.update_env(context=dict(self.env.context, **overrides)) + + +class WebsocketConnectionHandler: + SUPPORTED_VERSIONS = {'13'} + # Given by the RFC in order to generate Sec-WebSocket-Accept from + # Sec-WebSocket-Key value. + _HANDSHAKE_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + _REQUIRED_HANDSHAKE_HEADERS = { + 'connection', 'host', 'sec-websocket-key', + 'sec-websocket-version', 'upgrade', 'origin', + } + + @classmethod + def websocket_allowed(cls, request): + return not request.registry.in_test_mode() + + @classmethod + def open_connection(cls, request): + """ + Open a websocket connection if the handshake is successfull. + :return: Response indicating the server performed a connection + upgrade. + :raise: UpgradeRequired if there is no intersection between the + versions the client supports and those we support. + :raise: BadRequest if the handshake data is incorrect. + """ + if not cls.websocket_allowed(request): + raise ServiceUnavailable("Websocket is disabled in test mode") + cls._handle_public_configuration(request) + try: + response = cls._get_handshake_response(request.httprequest.headers) + socket = request.httprequest._HTTPRequest__environ['socket'] + session, db, httprequest = request.session, request.db, request.httprequest + response.call_on_close(lambda: cls._serve_forever( + Websocket(socket, session), + db, + httprequest, + )) + # Force save the session. Session must be persisted to handle + # WebSocket authentication. + request.session.is_dirty = True + return response + except KeyError as exc: + raise RuntimeError( + f"Couldn't bind the websocket. Is the connection opened on the evented port ({config['gevent_port']})?" + ) from exc + except HTTPException as exc: + # The HTTP stack does not log exceptions derivated from the + # HTTPException class since they are valid responses. + _logger.error(exc) + raise + + + + @classmethod + def _get_handshake_response(cls, headers): + """ + :return: Response indicating the server performed a connection + upgrade. + :raise: BadRequest + :raise: UpgradeRequired + """ + cls._assert_handshake_validity(headers) + # sha-1 is used as it is required by + # https://datatracker.ietf.org/doc/html/rfc6455#page-7 + accept_header = hashlib.sha1( + (headers['sec-websocket-key'] + cls._HANDSHAKE_GUID).encode()).digest() + accept_header = base64.b64encode(accept_header) + return Response(status=101, headers={ + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Accept': accept_header.decode(), + }) + + @classmethod + def _handle_public_configuration(cls, request): + if not os.getenv('ODOO_BUS_PUBLIC_SAMESITE_WS'): + return + headers = request.httprequest.headers + origin_url = urlparse(headers.get('origin')) + if origin_url.netloc != headers.get('host') or origin_url.scheme != request.httprequest.scheme: + request.session = root.session_store.new() + request.session.update(get_default_session(), db=request.session.db) + request.session.is_explicit = True + + @classmethod + def _assert_handshake_validity(cls, headers): + """ + :raise: UpgradeRequired if there is no intersection between + the version the client supports and those we support. + :raise: BadRequest in case of invalid handshake. + """ + missing_or_empty_headers = { + header for header in cls._REQUIRED_HANDSHAKE_HEADERS + if header not in headers + } + if missing_or_empty_headers: + raise BadRequest( + f"""Empty or missing header(s): {', '.join(missing_or_empty_headers)}""" + ) + + if headers['upgrade'].lower() != 'websocket': + raise BadRequest('Invalid upgrade header') + if 'upgrade' not in headers['connection'].lower(): + raise BadRequest('Invalid connection header') + if headers['sec-websocket-version'] not in cls.SUPPORTED_VERSIONS: + raise UpgradeRequired() + + key = headers['sec-websocket-key'] + try: + decoded_key = base64.b64decode(key, validate=True) + except ValueError: + raise BadRequest("Sec-WebSocket-Key should be b64 encoded") + if len(decoded_key) != 16: + raise BadRequest( + "Sec-WebSocket-Key should be of length 16 once decoded" + ) + + @classmethod + def _serve_forever(cls, websocket, db, httprequest): + """ + Process incoming messages and dispatch them to the application. + """ + current_thread = threading.current_thread() + current_thread.type = 'websocket' + for message in websocket.get_messages(): + with WebsocketRequest(db, httprequest, websocket) as req: + try: + req.serve_websocket_message(message) + except SessionExpiredException: + websocket.disconnect(CloseCode.SESSION_EXPIRED) + except PoolError: + websocket.disconnect(CloseCode.TRY_LATER) + except Exception: + _logger.exception("Exception occurred during websocket request handling") + + +def _kick_all(): + """ Disconnect all the websocket instances. """ + for websocket in _websocket_instances: + if websocket.state is ConnectionState.OPEN: + websocket.disconnect(CloseCode.GOING_AWAY) + + +CommonServer.on_stop(_kick_all)