diff --git a/odoo-bringout-oca-ocb-calendar_sms/README.md b/odoo-bringout-oca-ocb-calendar_sms/README.md index 7027095..87b4a97 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/README.md +++ b/odoo-bringout-oca-ocb-calendar_sms/README.md @@ -10,36 +10,15 @@ pip install odoo-bringout-oca-ocb-calendar_sms ## Dependencies -This addon depends on: - calendar - sms -## Manifest Information - -- **Name**: Calendar - SMS -- **Version**: 1.1 -- **Category**: Hidden -- **License**: LGPL-3 -- **Installable**: False - ## Source -Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `calendar_sms`. +- Repository: https://github.com/OCA/OCB +- Branch: 19.0 +- Path: addons/calendar_sms ## License -This package maintains the original LGPL-3 license from the upstream Odoo project. - -## Documentation - -- Overview: doc/OVERVIEW.md -- Architecture: doc/ARCHITECTURE.md -- Models: doc/MODELS.md -- Controllers: doc/CONTROLLERS.md -- Wizards: doc/WIZARDS.md -- Install: doc/INSTALL.md -- Usage: doc/USAGE.md -- Configuration: doc/CONFIGURATION.md -- Dependencies: doc/DEPENDENCIES.md -- Troubleshooting: doc/TROUBLESHOOTING.md -- FAQ: doc/FAQ.md +This package preserves the original LGPL-3 license. diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/__manifest__.py b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/__manifest__.py index 53f5c1d..d6b0501 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/__manifest__.py +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/__manifest__.py @@ -6,12 +6,13 @@ 'version': "1.1", 'summary': 'Send text messages as event reminders', 'description': "Send text messages as event reminders", - 'category': 'Hidden', + 'category': 'Productivity/Calendar', 'depends': ['calendar', 'sms'], 'data': [ 'data/sms_data.xml', 'views/calendar_views.xml', ], 'auto_install': True, + 'author': 'Odoo S.A.', 'license': 'LGPL-3', } diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/af.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/af.po index 67209f7..ab99c57 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/af.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/af.po @@ -1,22 +1,22 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# # Translators: # Martin Trigaux, 2022 -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\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-Team: Afrikaans (https://www.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" -"Language: af\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: calendar_sms @@ -42,15 +42,19 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder -msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" msgstr "" #. module: calendar_sms @@ -76,7 +80,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Send SMS Text Message" msgstr "" @@ -93,7 +96,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/am.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/am.po index e83654e..fea93cf 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/am.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/am.po @@ -1,18 +1,18 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\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" -"Language: am\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: calendar_sms @@ -38,15 +38,19 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder -msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" msgstr "" #. module: calendar_sms @@ -72,7 +76,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Send SMS Text Message" msgstr "" @@ -89,7 +92,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ar.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ar.po index f5f8421..f5e6a5d 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ar.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ar.po @@ -1,25 +1,28 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 # Hassan Najm , 2022 # Malaz Abuidris , 2023 -# +# "Tiffany Chang (tic)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Malaz Abuidris , 2023\n" -"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-16 13:38+0000\n" +"Last-Translator: \"Tiffany Chang (tic)\" \n" +"Language-Team: Arabic \n" +"Language: 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" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -29,7 +32,14 @@ msgstr "فعالية التقويم" #. module: calendar_sms #: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder msgid "Calendar Event: Reminder" -msgstr "فعالية التقويم: تذكير " +msgstr "فعالية التقويم: تذكير" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "اسم العرض" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm @@ -44,64 +54,67 @@ msgstr "إدارة تنبيه الفعالية" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." -msgstr "تذكير الفعالية: %(name)s, %(time)s. " +msgstr "تذكير الفعالية: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"تذكير الفعالية: {{ object.name }}، {{ " -"object.get_display_time_tz(object.partner_id.tz) }} " +"تذكير الفعالية: {{ object.name }}، " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "المُعرف" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" -msgstr "الرسائل النصية القصيرة " +msgstr "الرسائل النصية القصيرة" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id msgid "SMS Template" -msgstr "قالب الرسائل النصية القصيرة " +msgstr "قالب الرسائل النصية القصيرة" #. module: calendar_sms #: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms msgid "SMS Text Message" -msgstr "الرسائة النصية القصيرة " - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "إرسال رسالة نصية قصيرة " +msgstr "الرسائة النصية القصيرة" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "إرسال رسالة نصية قصيرة " +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "إرسال رسالة نصية قصيرة" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" -msgstr "إرسال رسالة نصية قصيرة للحاضرين " +msgstr "إرسال رسالة نصية قصيرة للحاضرين" #. module: calendar_sms #: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id msgid "Template used to render SMS reminder content." -msgstr "قالب مُستَخدَم لتكوين محتوى التذكير عبر الرسائل النصية القصيرة. " +msgstr "قالب مُستَخدَم لتكوين محتوى التذكير عبر الرسائل النصية القصيرة." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" -msgstr "لا يوجد أي حاضرين في هذه الفعاليات " +msgstr "لا يوجد أي حاضرين في هذه الفعاليات" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "النوع" + +#~ msgid "Send SMS Text Message" +#~ msgstr "إرسال رسالة نصية قصيرة " diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/az.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/az.po index 998adf0..8dcf1ab 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/az.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/az.po @@ -1,24 +1,26 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Jumshud Sultanov , 2022 # erpgo translator , 2022 -# +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: erpgo translator , 2022\n" -"Language-Team: Azerbaijani (https://app.transifex.com/odoo/teams/41243/az/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:07+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Azerbaijani \n" +"Language: az\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: az\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -30,6 +32,13 @@ msgstr "Təqvim hadisəsi" msgid "Calendar Event: Reminder" msgstr "Təqvim hadisəsi: Xatırlatma" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Göstəriləcək Ad" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -43,17 +52,23 @@ msgstr "Hadisə Xəbərdarlığı Meneceri" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" + #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" @@ -70,17 +85,12 @@ msgid "SMS Text Message" msgstr "SMS Mətn Mesajı" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "SMS Göndər" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -94,7 +104,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/bg.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/bg.po index 31d1bc8..cf2bbbb 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/bg.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/bg.po @@ -1,29 +1,29 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Igor Sheludko , 2023 # Ивайло Малинов , 2023 # Rosen Vladimirov , 2023 # Albena Mincheva , 2023 # Martin Trigaux, 2023 -# Petko Karamotchev, 2024 -# Martin Dinovski, 2025 -# +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Martin Dinovski, 2025\n" -"Language-Team: Bulgarian (https://app.transifex.com/odoo/teams/41243/bg/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:07+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Bulgarian \n" +"Language: 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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -35,6 +35,13 @@ msgstr "Календар Събитие" msgid "Calendar Event: Reminder" msgstr "Събитие в календара: Напомняне" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Име за показване" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -48,17 +55,23 @@ msgstr "Събитие - управление аларми" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" + #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" @@ -75,17 +88,12 @@ msgid "SMS Text Message" msgstr "Тексови съобщения" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Изпращане на SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -94,13 +102,11 @@ msgstr "Изпрати SMS на присъстващите" #. module: calendar_sms #: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id msgid "Template used to render SMS reminder content." -msgstr "" -"Шаблон, използван за изобразяване на съдържание за напомняне чрез SMS." +msgstr "Шаблон, използван за изобразяване на съдържание за напомняне чрез SMS." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Няма присъстващи на тези събития" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/bs.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/bs.po index 1c7c4de..44b14e8 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/bs.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/bs.po @@ -1,110 +1,110 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms +# * calendar_sms # +# Translators: +# Martin Trigaux, 2018 +# Boško Stojaković , 2018 msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server saas~11.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-12-15 12:50+0000\n" -"Last-Translator: \n" -"Language-Team: \n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2018-09-21 13:17+0000\n" +"Last-Translator: Boško Stojaković , 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: \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" -# taken from hr.po #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event msgid "Calendar Event" -msgstr "Događaj na kalendaru" +msgstr "" #. module: calendar_sms #: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder msgid "Calendar Event: Reminder" -msgstr "Kalendarski događaj: Podsjetnik" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "" -# taken from hr.po #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" -msgstr "Podsjetnik događaja" +msgstr "" -# taken from hr.po #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager msgid "Event Alarm Manager" -msgstr "Upravljanje podsjetnicima događaja" +msgstr "" #. module: calendar_sms #. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py -#, python-format +#: code:addons/calendar_sms/models/calendar_event.py:0 msgid "Event reminder: %(name)s, %(time)s." -msgstr "Podsjetnik za događaj: %(name)s, %(time)s." +msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" msgstr "" -"Podsjetnik za događaj: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" -msgstr "SMS" +msgstr "" -# taken from hr.po #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id msgid "SMS Template" -msgstr "Predložak SMS poruke" +msgstr "" -# taken from hr.po #. module: calendar_sms #: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms msgid "SMS Text Message" -msgstr "SMS poruka" +msgstr "" -# taken from hr.po -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "Pošalji SMS" - -# taken from hr.po #. module: calendar_sms #. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py -#, python-format -msgid "Send SMS Text Message" -msgstr "Send SMS Text Message" +#: code:addons/calendar_sms/models/calendar_event.py:0 +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "" -# taken from hr.po #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" -msgstr "Pošalji SMS svim polaznicima" +msgstr "" #. module: calendar_sms #: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id msgid "Template used to render SMS reminder content." -msgstr "Predložak koji se koristi za renderiranje sadržaja SMS podsjetnika." +msgstr "" -# taken from hr.po #. module: calendar_sms #. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py -#, python-format +#: code:addons/calendar_sms/models/calendar_event.py:0 msgid "There are no attendees on these events" -msgstr "SMS Tekst Poruka reminder sent !" +msgstr "" -# taken from hr.po #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ca.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ca.po index 7b12cb1..2896e62 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ca.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ca.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # AncesLatino2004, 2022 # jabelchi, 2022 @@ -9,20 +9,23 @@ # marcescu, 2022 # Martin Trigaux, 2022 # Ivan Espinola, 2023 -# +# +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Ivan Espinola, 2023\n" -"Language-Team: Catalan (https://app.transifex.com/odoo/teams/41243/ca/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:07+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Catalan \n" +"Language: 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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -34,6 +37,13 @@ msgstr "Esdeveniment del calendari" msgid "Calendar Event: Reminder" msgstr "Esdeveniment del calendari: Recordatori" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Nom mostrat" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -47,18 +57,24 @@ msgstr "Gestor d'alarmes d'esdeveniment" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Recordatori de l'esdeveniment: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Recordatori d'esdeveniments: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Recordatori d'esdeveniments: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -76,17 +92,12 @@ msgid "SMS Text Message" msgstr "Missatge de text SMS" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Enviar SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Envia un missatge de text SMS" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -102,7 +113,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "No hi ha assistents en aquests esdeveniments" @@ -110,3 +120,6 @@ msgstr "No hi ha assistents en aquests esdeveniments" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Tipus" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Envia un missatge de text SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/calendar_sms.pot b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/calendar_sms.pot index 32302ef..a2333da 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/calendar_sms.pot +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/calendar_sms.pot @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server 19.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-12-15 12:50+0000\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2026-01-25 18:36+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -25,6 +25,13 @@ msgstr "" msgid "Calendar Event: Reminder" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -38,7 +45,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" @@ -49,6 +55,13 @@ msgid "" "object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "" + #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" @@ -64,16 +77,11 @@ msgstr "" msgid "SMS Text Message" msgstr "" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" msgstr "" #. module: calendar_sms @@ -89,7 +97,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/cs.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/cs.po index 332de0b..c0e885f 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/cs.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/cs.po @@ -1,24 +1,28 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: -# Jiří Podhorecký , 2022 -# Marta Wacławek, 2025 -# +# Jiří Podhorecký, 2022 +# +# "Dylan Kiss (dyki)" , 2025. +# "Marta (wacm)" , 2026. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Marta Wacławek, 2025\n" -"Language-Team: Czech (https://app.transifex.com/odoo/teams/41243/cs/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2026-02-25 14:42+0000\n" +"Last-Translator: \"Marta (wacm)\" \n" +"Language-Team: Czech \n" +"Language: 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" +"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" +"X-Generator: Weblate 5.14.3\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -30,31 +34,44 @@ msgstr "Událost kalendáře" msgid "Calendar Event: Reminder" msgstr "Událost kalendáře: Připomenutí" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Zobrazovaný název" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" -msgstr "Alarm událostí" +msgstr "Notifikace kalendářové události" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager msgid "Event Alarm Manager" -msgstr "Správce alarmů událostí" +msgstr "Správce notifikací kalendářové události" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Připomenutí události: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Připomínka o události: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Připomínka o události: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -72,17 +89,12 @@ msgid "SMS Text Message" msgstr "SMS textová zpráva" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Odeslat SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Odeslat SMS textovou zprávu" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -96,7 +108,6 @@ msgstr "Šablona použitá pro vykreslení obsahu SMS upomínky." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Na těchto akcích nejsou žádní účastníci" @@ -104,3 +115,6 @@ msgstr "Na těchto akcích nejsou žádní účastníci" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Typ" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Odeslat SMS textovou zprávu" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/da.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/da.po index 40711ca..446430f 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/da.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/da.po @@ -1,24 +1,26 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 # Sanne Kristensen , 2022 -# +# "Dylan Kiss (dyki)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Sanne Kristensen , 2022\n" -"Language-Team: Danish (https://app.transifex.com/odoo/teams/41243/da/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-14 21:13+0000\n" +"Last-Translator: \"Dylan Kiss (dyki)\" \n" +"Language-Team: Danish \n" +"Language: 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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -30,6 +32,13 @@ msgstr "Kalender begivenhed" msgid "Calendar Event: Reminder" msgstr "Kalender begivenhed: Påmindelse" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Vis navn" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -43,17 +52,23 @@ msgstr "Arrangements notifikations ansvarlig" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Begivenheds påmindelse: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" + #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" @@ -70,17 +85,12 @@ msgid "SMS Text Message" msgstr "SMS besked" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Send SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Send SMS tekst besked" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -94,7 +104,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Der er ingen deltagere til disse arrangementer" @@ -102,3 +111,6 @@ msgstr "Der er ingen deltagere til disse arrangementer" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Type" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Send SMS tekst besked" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/de.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/de.po index 3a40595..0400b55 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/de.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/de.po @@ -1,24 +1,28 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: +# Martin Trigaux, 2022 # Larissa Manderfeld, 2023 -# Martin Trigaux, 2023 -# +# +# "Dylan Kiss (dyki)" , 2025. +# "Larissa Manderfeld (lman)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Martin Trigaux, 2023\n" -"Language-Team: German (https://app.transifex.com/odoo/teams/41243/de/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-10-31 12:43+0000\n" +"Last-Translator: \"Larissa Manderfeld (lman)\" \n" +"Language-Team: German \n" +"Language: 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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -30,6 +34,13 @@ msgstr "Kalender-Ereignis" msgid "Calendar Event: Reminder" msgstr "Kalender-Ereignis: Erinnerung" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Anzeigename" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -43,18 +54,24 @@ msgstr "Manager von Ereignisalarmen" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Veranstaltungserinnerung: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Veranstaltungserinnerung: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Veranstaltungserinnerung: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -72,17 +89,12 @@ msgid "SMS Text Message" msgstr "SMS-Textnachricht" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "SMS versenden" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "SMS-Textnachricht versenden" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -96,7 +108,6 @@ msgstr "Vorlage für die Darstellung von SMS-Erinnerungsinhalten." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Für diese Veranstaltung gibt es keine Teilnehmer" @@ -104,3 +115,6 @@ msgstr "Für diese Veranstaltung gibt es keine Teilnehmer" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Typ" + +#~ msgid "Send SMS Text Message" +#~ msgstr "SMS-Textnachricht senden" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/el.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/el.po index 26e838c..49ef6f6 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/el.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/el.po @@ -1,33 +1,44 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# # Translators: # Martin Trigaux, 2018 # Kostas Goutoudis , 2018 +# "Dylan Kiss (dyki)" , 2025. +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server saas~11.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Kostas Goutoudis , 2018\n" -"Language-Team: Greek (https://www.transifex.com/odoo/teams/41243/el/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:07+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Greek \n" +"Language: el\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: el\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: selection:calendar.alarm,type:0 -msgid "Email" -msgstr "Email" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" -msgstr "Συμβάν" +msgid "Calendar Event" +msgstr "Συμβάν Ημερολογίου" + +#. module: calendar_sms +#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder +msgid "Calendar Event: Reminder" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Εμφάνιση Ονόματος" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm @@ -40,33 +51,64 @@ msgid "Event Alarm Manager" msgstr "" #. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "Υπενθύμιση συμβάντος: %s στις %s." +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Event reminder: %(name)s, %(time)s." +msgstr "" #. module: calendar_sms -#: selection:calendar.alarm,type:0 -msgid "Notification" -msgstr "Ειδοποίηση" +#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +msgid "" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" #. module: calendar_sms -#: selection:calendar.alarm,type:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "Κωδικός" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "SMS" +msgstr "SMS" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +msgid "SMS Template" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms msgid "SMS Text Message" msgstr "Κείμενο SMS" #. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "Η υπενθύμιση με SMS έχει αποσταλεί!" +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "Αποστολή SMS" #. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" msgstr "Αποστολή SMS στους συμμετέχοντες" #. module: calendar_sms -#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__type +#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +msgid "Template used to render SMS reminder content." +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "There are no attendees on these events" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Τύπος" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/en_GB.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/en_GB.po deleted file mode 100644 index 34e2889..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/en_GB.po +++ /dev/null @@ -1,48 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" -"PO-Revision-Date: 2017-09-20 11:33+0000\n" -"Language-Team: English (United Kingdom) (https://www.transifex.com/odoo/teams/41243/en_GB/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: en_GB\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es.po index 427b634..efbdb9e 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es.po @@ -1,25 +1,27 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 -# Larissa Manderfeld, 2023 -# Wil Odoo, 2024 -# +# +# "Dylan Kiss (dyki)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Wil Odoo, 2024\n" -"Language-Team: Spanish (https://app.transifex.com/odoo/teams/41243/es/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-14 21:17+0000\n" +"Last-Translator: \"Dylan Kiss (dyki)\" \n" +"Language-Team: Spanish \n" +"Language: 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" +"Plural-Forms: nplurals=3; plural=(n == 1) ? 0 : ((n != 0 && n % 1000000 == " +"0) ? 1 : 2);\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -31,6 +33,13 @@ msgstr "Evento del calendario" msgid "Calendar Event: Reminder" msgstr "Evento del calendario: recordatorio" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Nombre para mostrar" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -44,18 +53,24 @@ msgstr "Gerente de alertas de eventos" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Recordatorio de evento: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Recordatorio de evento: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Recordatorio de evento: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -73,17 +88,12 @@ msgid "SMS Text Message" msgstr "Mensaje de texto SMS" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Enviar SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Enviar mensaje de texto SMS" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -99,7 +109,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "No hay asistentes para estos eventos" @@ -107,3 +116,6 @@ msgstr "No hay asistentes para estos eventos" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Tipo" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Manda Mensaje de Texto SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_MX.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_419.po similarity index 64% rename from odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_MX.po rename to odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_419.po index e65a3dc..fe6a4da 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_MX.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_419.po @@ -1,36 +1,42 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# -# Translators: -# Martin Trigaux, 2022 -# Braulio D. López Vázquez , 2022 -# Fernanda Alvarez, 2023 -# +# * calendar_sms +# +# "Dylan Kiss (dyki)" , 2025. +# "Fernanda Alvarez (mfar)" , 2025. msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server 18.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Fernanda Alvarez, 2023\n" -"Language-Team: Spanish (Mexico) (https://app.transifex.com/odoo/teams/41243/es_MX/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-10-30 17:02+0000\n" +"Last-Translator: \"Fernanda Alvarez (mfar)\" \n" +"Language-Team: Spanish (Latin America) \n" +"Language: es_419\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_MX\n" -"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event msgid "Calendar Event" -msgstr "Evento de calendario" +msgstr "Evento del calendario" #. module: calendar_sms #: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder msgid "Calendar Event: Reminder" msgstr "Evento de calendario: recordatorio" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Nombre para mostrar" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -44,18 +50,24 @@ msgstr "Gestor de alertas de eventos" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Recordatorio de evento: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Recordatorio de evento: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Recordatorio de evento: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -73,17 +85,12 @@ msgid "SMS Text Message" msgstr "Mensaje de texto SMS" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Enviar SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Enviar mensaje de texto SMS" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -99,7 +106,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "No hay asistentes para estos eventos" @@ -107,3 +113,6 @@ msgstr "No hay asistentes para estos eventos" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Tipo" + +#~ msgid "Notify Responsible" +#~ msgstr "Notificar al responsable" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_BO.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_BO.po deleted file mode 100644 index 25ebcf7..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_BO.po +++ /dev/null @@ -1,48 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" -"PO-Revision-Date: 2017-09-20 11:33+0000\n" -"Language-Team: Spanish (Bolivia) (https://www.transifex.com/odoo/teams/41243/es_BO/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: es_BO\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_CL.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_CL.po index 1ce35e4..d1887c3 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_CL.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_CL.po @@ -1,48 +1,101 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 10.saas~18+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" "PO-Revision-Date: 2017-09-20 11:33+0000\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" -"Language: es_CL\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" +msgid "Calendar Event" +msgstr "" + +#. module: calendar_sms +#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder +msgid "Calendar Event: Reminder" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" +msgid "Event Alarm" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" +msgid "Event Alarm Manager" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Event reminder: %(name)s, %(time)s." +msgstr "" + +#. module: calendar_sms +#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "SMS" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +msgid "SMS Template" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms +msgid "SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Send SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "Send SMS to attendees" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +msgid "Template used to render SMS reminder content." +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "There are no attendees on these events" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type +msgid "Type" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_CO.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_CO.po deleted file mode 100644 index 2866e4b..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_CO.po +++ /dev/null @@ -1,48 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" -"PO-Revision-Date: 2017-09-20 11:33+0000\n" -"Language-Team: Spanish (Colombia) (https://www.transifex.com/odoo/teams/41243/es_CO/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: es_CO\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_CR.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_CR.po deleted file mode 100644 index a0db671..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_CR.po +++ /dev/null @@ -1,48 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" -"PO-Revision-Date: 2017-09-20 11:33+0000\n" -"Language-Team: Spanish (Costa Rica) (https://www.transifex.com/odoo/teams/41243/es_CR/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: es_CR\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_DO.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_DO.po deleted file mode 100644 index 39d2ecc..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_DO.po +++ /dev/null @@ -1,48 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" -"PO-Revision-Date: 2017-09-20 11:33+0000\n" -"Language-Team: Spanish (Dominican Republic) (https://www.transifex.com/odoo/teams/41243/es_DO/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: es_DO\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_EC.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_EC.po deleted file mode 100644 index 59fe331..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_EC.po +++ /dev/null @@ -1,48 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" -"PO-Revision-Date: 2017-09-20 11:33+0000\n" -"Language-Team: Spanish (Ecuador) (https://www.transifex.com/odoo/teams/41243/es_EC/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: es_EC\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_PE.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_PE.po deleted file mode 100644 index 5f80197..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_PE.po +++ /dev/null @@ -1,48 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" -"PO-Revision-Date: 2017-09-20 11:33+0000\n" -"Language-Team: Spanish (Peru) (https://www.transifex.com/odoo/teams/41243/es_PE/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: es_PE\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_PY.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_PY.po deleted file mode 100644 index bb084d6..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_PY.po +++ /dev/null @@ -1,48 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" -"PO-Revision-Date: 2017-09-20 11:33+0000\n" -"Language-Team: Spanish (Paraguay) (https://www.transifex.com/odoo/teams/41243/es_PY/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: es_PY\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_VE.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_VE.po deleted file mode 100644 index 9b0cd0d..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/es_VE.po +++ /dev/null @@ -1,48 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" -"PO-Revision-Date: 2017-09-20 11:33+0000\n" -"Language-Team: Spanish (Venezuela) (https://www.transifex.com/odoo/teams/41243/es_VE/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: es_VE\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/et.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/et.po index d3341db..09a1926 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/et.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/et.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Eneli Õigus , 2022 # Andre Roomet , 2022 @@ -11,20 +11,22 @@ # Algo Kärp , 2022 # Patrick-Jordan Kiudorv, 2022 # Anna, 2023 -# +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Anna, 2023\n" -"Language-Team: Estonian (https://app.transifex.com/odoo/teams/41243/et/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:31+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Estonian \n" +"Language: 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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -36,6 +38,13 @@ msgstr "Kalendrisündmus" msgid "Calendar Event: Reminder" msgstr "Kalender: sündmuse meeldetuletus" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Kuvatav nimi" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -49,18 +58,24 @@ msgstr "Ürituste juht" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Sündmuse meeldetuletus: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Sündmuse meeldetuletus: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Sündmuse meeldetuletus: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -77,16 +92,11 @@ msgstr "SMS mall" msgid "SMS Text Message" msgstr "SMS tekstisõnum" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "Saada SMS" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" msgstr "Saada SMS" #. module: calendar_sms @@ -102,7 +112,6 @@ msgstr "Meilide meeldetuletuse sisu koostamiseks kasutatav mall." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Nendel üritustel pole ühtegi osalejat" @@ -110,3 +119,6 @@ msgstr "Nendel üritustel pole ühtegi osalejat" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Tüüp" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Saada SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/eu.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/eu.po index 8c56fd2..6317daf 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/eu.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/eu.po @@ -1,48 +1,101 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 10.saas~18+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" "PO-Revision-Date: 2017-09-20 11:33+0000\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" -"Language: eu\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" +msgid "Calendar Event" +msgstr "" + +#. module: calendar_sms +#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder +msgid "Calendar Event: Reminder" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" +msgid "Event Alarm" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" +msgid "Event Alarm Manager" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Event reminder: %(name)s, %(time)s." +msgstr "" + +#. module: calendar_sms +#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "SMS" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +msgid "SMS Template" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms +msgid "SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Send SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "Send SMS to attendees" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +msgid "Template used to render SMS reminder content." +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "There are no attendees on these events" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type +msgid "Type" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fa.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fa.po index ea718e1..d4e1e67 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fa.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fa.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Mohsen Mohammadi , 2023 # سید محمد آذربرا , 2023 @@ -9,21 +9,22 @@ # Mohammad Tahmasebi , 2023 # Martin Trigaux, 2023 # Hanna Kheradroosta, 2023 -# Mostafa Barmshory , 2024 -# +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Mostafa Barmshory , 2024\n" -"Language-Team: Persian (https://app.transifex.com/odoo/teams/41243/fa/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:07+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Persian \n" +"Language: 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" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -35,6 +36,13 @@ msgstr "رخداد گاهشمار" msgid "Calendar Event: Reminder" msgstr "رویداد تقویم: یادآور" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "نام نمایشی" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -48,18 +56,24 @@ msgstr "مدیریت اطلاع رسانی رویداد" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "یادآوری رویداد: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"یادآوری رویداد: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"یادآوری رویداد: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "شناسه" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -77,17 +91,12 @@ msgid "SMS Text Message" msgstr "پیام متنی" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "ارسال پیامک" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "ارسال پیامک SMS" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -101,7 +110,6 @@ msgstr "الگوی استفاده‌شده برای نمایش محتوای یا #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "هیچ شرکت کننده ای در این رویدادها وجود ندارد" @@ -109,3 +117,6 @@ msgstr "هیچ شرکت کننده ای در این رویدادها وجود ن #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "نوع" + +#~ msgid "Send SMS Text Message" +#~ msgstr "ارسال پیامک SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fi.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fi.po index 14a36b3..638d386 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fi.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fi.po @@ -1,27 +1,29 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Pekka Ikonen, 2022 # Jarmo Kortetjärvi , 2022 # Kari Lindgren , 2022 # Tuomo Aura , 2022 # Ossi Mantylahti , 2023 -# +# "Tiffany Chang (tic)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Ossi Mantylahti , 2023\n" -"Language-Team: Finnish (https://app.transifex.com/odoo/teams/41243/fi/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-16 15:20+0000\n" +"Last-Translator: \"Tiffany Chang (tic)\" \n" +"Language-Team: Finnish \n" +"Language: 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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -33,6 +35,13 @@ msgstr "Kalenteritapahtuma" msgid "Calendar Event: Reminder" msgstr "Kalenteritapahtuma: muistutus" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Näyttönimi" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -46,18 +55,24 @@ msgstr "Hälytysten hallinta" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Muistutus tapahtumasta: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Muistutus tapahtumasta: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Muistutus tapahtumasta: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "Tunnus" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -74,16 +89,11 @@ msgstr "SMS Malli" msgid "SMS Text Message" msgstr "Tekstiviesti" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "Lähetä tekstiviesti" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" msgstr "Lähetä tekstiviesti" #. module: calendar_sms @@ -99,7 +109,6 @@ msgstr "Malli, jota käytetään tekstiviestimuistutuksen sisällön esittämise #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Näissä tapahtumissa ei ole osallistujia" @@ -107,3 +116,6 @@ msgstr "Näissä tapahtumissa ei ole osallistujia" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Tyyppi" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Lähetä tekstiviesti" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fo.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fo.po index ffb12fc..b36e085 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fo.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fo.po @@ -1,48 +1,101 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 10.saas~18+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" "PO-Revision-Date: 2017-09-20 11:33+0000\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" -"Language: fo\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" +msgid "Calendar Event" +msgstr "" + +#. module: calendar_sms +#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder +msgid "Calendar Event: Reminder" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" +msgid "Event Alarm" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" +msgid "Event Alarm Manager" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Event reminder: %(name)s, %(time)s." +msgstr "" + +#. module: calendar_sms +#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "SMS" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +msgid "SMS Template" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms +msgid "SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Send SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "Send SMS to attendees" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +msgid "Template used to render SMS reminder content." +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "There are no attendees on these events" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type +msgid "Type" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fr.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fr.po index 99c4f63..fd30a33 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fr.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fr.po @@ -1,25 +1,28 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 # Cécile Collart , 2022 # Jolien De Paepe, 2023 -# +# "Dylan Kiss (dyki)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Jolien De Paepe, 2023\n" -"Language-Team: French (https://app.transifex.com/odoo/teams/41243/fr/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-14 21:10+0000\n" +"Last-Translator: \"Dylan Kiss (dyki)\" \n" +"Language-Team: French \n" +"Language: 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" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : ((n != 0 && n % " +"1000000 == 0) ? 1 : 2);\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -31,6 +34,13 @@ msgstr "Événement calendrier" msgid "Calendar Event: Reminder" msgstr "Evénement calendrier : Rappel" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Nom d'affichage" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -44,18 +54,24 @@ msgstr "Gestionnaire de rappel d'événements" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Rappel d'événement : %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Rappel d'événement : {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Rappel d'événement : {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -65,23 +81,18 @@ msgstr "SMS" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id msgid "SMS Template" -msgstr "Modèle de SMS" +msgstr "Modèle SMS" #. module: calendar_sms #: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms msgid "SMS Text Message" msgstr "SMS" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "Envoyer un SMS" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" msgstr "Envoyer un SMS" #. module: calendar_sms @@ -97,7 +108,6 @@ msgstr "Modèle utilisé pour afficher le contenu des rappels SMS." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Il n'y a pas de participants à ces événements" @@ -105,3 +115,6 @@ msgstr "Il n'y a pas de participants à ces événements" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Type" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Envoyer un SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fr_CA.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fr_CA.po index 3bb7f09..9ebc066 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fr_CA.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/fr_CA.po @@ -1,48 +1,101 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 10.saas~18+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" "PO-Revision-Date: 2017-09-20 11:33+0000\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" -"Language: fr_CA\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" +msgid "Calendar Event" +msgstr "" + +#. module: calendar_sms +#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder +msgid "Calendar Event: Reminder" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" +msgid "Event Alarm" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" +msgid "Event Alarm Manager" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Event reminder: %(name)s, %(time)s." +msgstr "" + +#. module: calendar_sms +#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "SMS" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +msgid "SMS Template" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms +msgid "SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Send SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "Send SMS to attendees" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +msgid "Template used to render SMS reminder content." +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "There are no attendees on these events" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type +msgid "Type" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/gl.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/gl.po index e32155f..b78077b 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/gl.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/gl.po @@ -1,48 +1,101 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 10.saas~18+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" "PO-Revision-Date: 2017-09-20 11:33+0000\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" -"Language: gl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" +msgid "Calendar Event" +msgstr "" + +#. module: calendar_sms +#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder +msgid "Calendar Event: Reminder" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" +msgid "Event Alarm" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" +msgid "Event Alarm Manager" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Event reminder: %(name)s, %(time)s." +msgstr "" + +#. module: calendar_sms +#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "SMS" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +msgid "SMS Template" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms +msgid "SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Send SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "Send SMS to attendees" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +msgid "Template used to render SMS reminder content." +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "There are no attendees on these events" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type +msgid "Type" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/gu.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/gu.po index 735e711..1ea5a41 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/gu.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/gu.po @@ -1,22 +1,21 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: -# Qaidjohar Barbhaya, 2023 -# +# Martin Trigaux, 2018 msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server saas~11.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+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" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2018-09-21 13:17+0000\n" +"Last-Translator: Martin Trigaux, 2018\n" +"Language-Team: Gujarati (https://www.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" -"Language: gu\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: calendar_sms @@ -42,15 +41,19 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder -msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" msgstr "" #. module: calendar_sms @@ -76,7 +79,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Send SMS Text Message" msgstr "" @@ -93,11 +95,10 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" -msgstr "Type" +msgstr "પ્રકાર" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/he.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/he.po index 4841c4a..8176292 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/he.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/he.po @@ -1,27 +1,30 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Amit Spilman , 2022 # Yihya Hugirat , 2022 # ZVI BLONDER , 2022 # Lilach Gilliam , 2022 -# Ha Ketem , 2024 -# +# or balmas , 2025. +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Ha Ketem , 2024\n" -"Language-Team: Hebrew (https://app.transifex.com/odoo/teams/41243/he/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:07+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Hebrew \n" +"Language: 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=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;\n" +"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && " +"n % 10 == 0) ? 2 : 3));\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -33,6 +36,13 @@ msgstr "אירוע לוח שנה" msgid "Calendar Event: Reminder" msgstr "אירוע לוח שנה: תזכורת" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "שם לתצוגה" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -46,18 +56,24 @@ msgstr "מנהל התראות אירוע" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "תזכורת לארוע: %(name)s, %(time)s" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"תזכורת לאירוע: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"תזכורת לאירוע: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "מזהה" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -75,17 +91,12 @@ msgid "SMS Text Message" msgstr "הודעת SMS" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "שלח SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "שלח הודעת SMS" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -99,7 +110,6 @@ msgstr "תבנית המשמשת לעיבוד תוכן תזכורת SMS." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "אין משתתפים באירועים אלו" @@ -107,3 +117,6 @@ msgstr "אין משתתפים באירועים אלו" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "סוג" + +#~ msgid "Send SMS Text Message" +#~ msgstr "שלח הודעת SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hi.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hi.po index 9694b4d..dd20c8d 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hi.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hi.po @@ -1,24 +1,23 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# -# Translators: -# Wil Odoo, 2024 -# Ujjawal Pathak, 2025 -# +# * calendar_sms +# +# Weblate , 2025. msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server 18.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Ujjawal Pathak, 2025\n" -"Language-Team: Hindi (https://app.transifex.com/odoo/teams/41243/hi/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:13+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Hindi \n" +"Language: hi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: hi\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -30,30 +29,43 @@ msgstr "कैलेंडर इवेंट" msgid "Calendar Event: Reminder" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "डिस्प्ले का नाम" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" -msgstr "" +msgstr "इवेंट अलार्म" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager msgid "Event Alarm Manager" -msgstr "" +msgstr "इवेंट अलार्म मैनेजर" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "आईडी" + #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" @@ -62,23 +74,18 @@ msgstr "एसएमएस" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id msgid "SMS Template" -msgstr "" +msgstr "एसएमएस टेम्प्लेट" #. module: calendar_sms #: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms msgid "SMS Text Message" msgstr "" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" msgstr "" #. module: calendar_sms @@ -94,11 +101,10 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" -msgstr "" +msgstr "इन इवेंट में कोई प्रतिभागी नहीं हैं" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" -msgstr "प्रकार" +msgstr "टाइप" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hr.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hr.po index facf8cd..ca2160e 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hr.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hr.po @@ -1,26 +1,28 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Matej Mijoč, 2022 # Martin Trigaux, 2022 # Bole , 2022 -# Luka Carević , 2025 -# +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Luka Carević , 2025\n" -"Language-Team: Croatian (https://app.transifex.com/odoo/teams/41243/hr/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:07+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Croatian \n" +"Language: hr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: hr\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" +"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" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -32,6 +34,13 @@ msgstr "Događaj na kalendaru" msgid "Calendar Event: Reminder" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Naziv" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -45,17 +54,23 @@ msgstr "Upravljanje podsjetnicima događaja" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" + #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" @@ -72,17 +87,12 @@ msgid "SMS Text Message" msgstr "SMS poruka" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Pošalji SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Pošalji SMS poruku" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -96,7 +106,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Nema sudionika na tim događajima" @@ -104,3 +113,6 @@ msgstr "Nema sudionika na tim događajima" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Vrsta" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Pošalji SMS poruku" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hu.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hu.po index b0712bb..03e29da 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hu.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hu.po @@ -1,25 +1,27 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # krnkris, 2022 # Ákos Nagy , 2022 # Tamás Németh , 2022 -# +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Tamás Németh , 2022\n" -"Language-Team: Hungarian (https://app.transifex.com/odoo/teams/41243/hu/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:07+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Hungarian \n" +"Language: 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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -31,6 +33,13 @@ msgstr "Naptári esemény" msgid "Calendar Event: Reminder" msgstr "Naptári esemény: emlékeztető" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Megjelenített név" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -44,17 +53,23 @@ msgstr "Esemény riasztás kezelő" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" + #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" @@ -70,16 +85,11 @@ msgstr "SMS sablon" msgid "SMS Text Message" msgstr "SMS üzenet" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "SMS küldése" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" msgstr "SMS küldése" #. module: calendar_sms @@ -95,11 +105,13 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" -msgstr "" +msgstr "Ezeknek az eseményeknek nincs résztvevője" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Típus" + +#~ msgid "Send SMS Text Message" +#~ msgstr "SMS küldése" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hy.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hy.po deleted file mode 100644 index 35ff47f..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/hy.po +++ /dev/null @@ -1,99 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+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: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Calendar Event" -msgstr "" - -#. module: calendar_sms -#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder -msgid "Calendar Event: Reminder" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event Alarm" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "Event Alarm Manager" -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Event reminder: %(name)s, %(time)s." -msgstr "" - -#. module: calendar_sms -#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder -msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited -msgid "SMS" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id -msgid "SMS Template" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms -msgid "SMS Text Message" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id -msgid "Template used to render SMS reminder content." -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "There are no attendees on these events" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type -msgid "Type" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/id.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/id.po index 0fb9d16..246247e 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/id.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/id.po @@ -1,24 +1,26 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 -# Abe Manyo, 2023 -# +# +# "Dylan Kiss (dyki)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Abe Manyo, 2023\n" -"Language-Team: Indonesian (https://app.transifex.com/odoo/teams/41243/id/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-16 02:35+0000\n" +"Last-Translator: \"Dylan Kiss (dyki)\" \n" +"Language-Team: Indonesian \n" +"Language: 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" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -30,6 +32,13 @@ msgstr "Acara Kalender" msgid "Calendar Event: Reminder" msgstr "Acara Kalender: Pengingat" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Nama Tampilan" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -43,18 +52,24 @@ msgstr "Manajer Alarm Acara" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Pengingat acara: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Pengingat acara: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Pengingat acara: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -72,21 +87,16 @@ msgid "SMS Text Message" msgstr "SMS Pesan Teks" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Kirim SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Kirim Pesan Teks SMS" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" -msgstr "Kirim SMS ke pesertra" +msgstr "Kirim SMS ke peserta" #. module: calendar_sms #: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id @@ -96,7 +106,6 @@ msgstr "Templat yang digunakan untuk merender konten pengingat SMS." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Tidak ada peserta pada acara-acara tersebut" @@ -104,3 +113,6 @@ msgstr "Tidak ada peserta pada acara-acara tersebut" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Jenis" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Kirim Pesan Teks SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/is.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/is.po index de8c473..50876db 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/is.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/is.po @@ -1,18 +1,18 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server 15.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" "PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Language-Team: Icelandic (https://app.transifex.com/odoo/teams/41243/is/)\n" +"Language-Team: Icelandic (https://www.transifex.com/odoo/teams/41243/is/)\n" +"Language: 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: calendar_sms @@ -38,15 +38,19 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder -msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" msgstr "" #. module: calendar_sms @@ -72,7 +76,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Send SMS Text Message" msgstr "" @@ -89,11 +92,10 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" -msgstr "" +msgstr "Gerð" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/it.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/it.po index 169f882..cd67bbd 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/it.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/it.po @@ -1,24 +1,28 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 -# Sergio Zanchetta , 2023 -# +# Dario Cucchiar, 2023 +# +# "Dylan Kiss (dyki)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Sergio Zanchetta , 2023\n" -"Language-Team: Italian (https://app.transifex.com/odoo/teams/41243/it/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-14 21:11+0000\n" +"Last-Translator: \"Dylan Kiss (dyki)\" \n" +"Language-Team: Italian \n" +"Language: 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" +"Plural-Forms: nplurals=3; plural=(n == 1) ? 0 : ((n != 0 && n % 1000000 == " +"0) ? 1 : 2);\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -30,6 +34,13 @@ msgstr "Evento in calendario" msgid "Calendar Event: Reminder" msgstr "Evento in calendario - Promemoria" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -43,18 +54,24 @@ msgstr "Gestore avvisi evento" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Promemoria evento: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Promemoria evento: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Promemoria evento: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -72,17 +89,12 @@ msgid "SMS Text Message" msgstr "Messaggio SMS" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Invia SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Invia messaggio SMS" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -96,7 +108,6 @@ msgstr "Modello utilizzato per rendere il contenuto del promemoria SMS." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Non ci sono partecipanti a questi eventi" @@ -104,3 +115,6 @@ msgstr "Non ci sono partecipanti a questi eventi" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Tipologia" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Invia messaggio SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ja.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ja.po index 8f801e1..9b634c8 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ja.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ja.po @@ -1,24 +1,27 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 -# Junko Augias, 2023 -# +# +# "Dylan Kiss (dyki)" , 2025. +# "Junko Augias (juau)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Junko Augias, 2023\n" -"Language-Team: Japanese (https://app.transifex.com/odoo/teams/41243/ja/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-10-21 08:00+0000\n" +"Last-Translator: \"Junko Augias (juau)\" \n" +"Language-Team: Japanese \n" +"Language: 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" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -28,7 +31,14 @@ msgstr "カレンダーイベント" #. module: calendar_sms #: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder msgid "Calendar Event: Reminder" -msgstr "カレンダーイベント: リマインダ" +msgstr "カレンダイベント: リマインダ" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "表示名" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm @@ -43,18 +53,24 @@ msgstr "イベントアラーム管理" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "イベントリマインダー: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"イベントリマインダ: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"イベントリマインダ: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -71,22 +87,17 @@ msgstr "SMSテンプレート" msgid "SMS Text Message" msgstr "SMSテキストメッセージ" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "SMS配信" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "SMSテキストメッセージを送信" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "SMS送信" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" -msgstr "出席者にSMS配信" +msgstr "出席者にSMS送信" #. module: calendar_sms #: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id @@ -96,7 +107,6 @@ msgstr "SMSリマインダーコンテンツのレンダリングに使用され #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "これらのイベントの参加者がいません" @@ -104,3 +114,6 @@ msgstr "これらのイベントの参加者がいません" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "タイプ" + +#~ msgid "Send SMS Text Message" +#~ msgstr "SMSテキストメッセージを送信" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ka.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ka.po index 8b0145d..bfbfa78 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ka.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ka.po @@ -1,48 +1,101 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 10.saas~18+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" "PO-Revision-Date: 2017-09-20 11:33+0000\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" -"Language: ka\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" +msgid "Calendar Event" +msgstr "" + +#. module: calendar_sms +#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder +msgid "Calendar Event: Reminder" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" +msgid "Event Alarm" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" +msgid "Event Alarm Manager" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Event reminder: %(name)s, %(time)s." +msgstr "" + +#. module: calendar_sms +#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "SMS" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +msgid "SMS Template" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms +msgid "SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Send SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "Send SMS to attendees" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +msgid "Template used to render SMS reminder content." +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "There are no attendees on these events" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type +msgid "Type" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/kab.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/kab.po index 7f9fc05..897817e 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/kab.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/kab.po @@ -1,48 +1,106 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 10.saas~18+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" "PO-Revision-Date: 2017-09-20 11:33+0000\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" -"Language: kab\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" +msgid "Calendar Event" +msgstr "" + +#. module: calendar_sms +#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder +msgid "Calendar Event: Reminder" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" +msgid "Event Alarm" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" +msgid "Event Alarm Manager" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Event reminder: %(name)s, %(time)s." +msgstr "" + +#. module: calendar_sms +#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +msgid "" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "SMS" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +msgid "SMS Template" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms +msgid "SMS Text Message" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "Send SMS to attendees" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +msgid "Template used to render SMS reminder content." +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "There are no attendees on these events" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type +msgid "Type" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/km.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/km.po index c297892..20d0376 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/km.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/km.po @@ -1,23 +1,23 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: -# Lux Sok , 2023 -# Chan Nath , 2023 -# +# Sengtha Chay , 2018 +# Chan Nath , 2018 +# Samkhann Seang , 2018 msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server saas~11.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Chan Nath , 2023\n" -"Language-Team: Khmer (https://app.transifex.com/odoo/teams/41243/km/)\n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2018-09-21 13:17+0000\n" +"Last-Translator: Samkhann Seang , 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" -"Language: km\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: calendar_sms @@ -33,25 +33,29 @@ msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" -msgstr "ការជូនដំណឹងព្រឹត្ដិការណ៍" +msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager msgid "Event Alarm Manager" -msgstr "កម្មវិធីគ្រប់គ្រងការជូនដំណឹងព្រឹត្តិការណ៍" +msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder -msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" msgstr "" #. module: calendar_sms @@ -67,24 +71,23 @@ msgstr "" #. module: calendar_sms #: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms msgid "SMS Text Message" -msgstr "អត្ថបទសារ SMS" +msgstr "" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" -msgstr "ផ្ញើរសារ SMS" +msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Send SMS Text Message" msgstr "" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" -msgstr "ផ្ញើសារទៅអ្នកចូលរួម" +msgstr "" #. module: calendar_sms #: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id @@ -94,7 +97,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ko.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ko.po index 32b4a64..e657a3f 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ko.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ko.po @@ -1,25 +1,27 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 # Sarah Park, 2023 -# Daye Jeong, 2023 -# +# +# "Dylan Kiss (dyki)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Daye Jeong, 2023\n" -"Language-Team: Korean (https://app.transifex.com/odoo/teams/41243/ko/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-16 04:38+0000\n" +"Last-Translator: \"Dylan Kiss (dyki)\" \n" +"Language-Team: Korean \n" +"Language: 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" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -31,6 +33,13 @@ msgstr "행사 일정표" msgid "Calendar Event: Reminder" msgstr "일정표 행사 : 미리 알림" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "표시명" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -44,18 +53,24 @@ msgstr "일정 알림 관리자" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "행사 미리 알림 : %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"행사 알림: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -73,17 +88,12 @@ msgid "SMS Text Message" msgstr "SMS 문자 메시지" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "SMS 보내기" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "SMS 문자메시지 전송" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -97,7 +107,6 @@ msgstr "SMS 알림 내용을 렌더링하는 데 사용되는 서식입니다." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "해당 행사에 참석자가 없습니다." @@ -105,3 +114,6 @@ msgstr "해당 행사에 참석자가 없습니다." #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "유형" + +#~ msgid "Send SMS Text Message" +#~ msgstr "SMS 문자메시지 전송" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/be.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ku.po similarity index 65% rename from odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/be.po rename to odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ku.po index 4345a3f..3b9b860 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/be.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ku.po @@ -1,19 +1,23 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# +# Weblate , 2025. msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server 18.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Language-Team: Belarusian (https://app.transifex.com/odoo/teams/41243/be/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:13+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Kurdish (Central) \n" +"Language: ku\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: be\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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -25,6 +29,13 @@ msgstr "" msgid "Calendar Event: Reminder" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -38,17 +49,23 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" + #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" @@ -64,16 +81,11 @@ msgstr "" msgid "SMS Text Message" msgstr "" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" msgstr "" #. module: calendar_sms @@ -89,11 +101,10 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" -msgstr "" +msgstr "جۆر" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lb.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lb.po index 7fb5add..972d446 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lb.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lb.po @@ -1,18 +1,18 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server saas~12.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-26 08:16+0000\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" -"Language: lb\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: calendar_sms @@ -36,16 +36,31 @@ msgid "Event Alarm Manager" msgstr "" #. module: calendar_sms -#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder -msgid "" -"Event reminder: ${object.name} on ${object.start_datetime or " -"object.start_date}" +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:0 -#, python-format -msgid "Event reminder: %s on %s." +#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "SMS" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +msgid "SMS Template" msgstr "" #. module: calendar_sms @@ -54,10 +69,32 @@ msgid "SMS Text Message" msgstr "" #. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.calendar_event_act_window_sms_composer_single +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Send SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +msgid "Template used to render SMS reminder content." +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "There are no attendees on these events" +msgstr "" + #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lo.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lo.po index bbbd017..da8d896 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lo.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lo.po @@ -1,22 +1,18 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# -# Translators: -# ສີສຸວັນ ສັງບົວບຸລົມ , 2023 -# +# * calendar_sms +# msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server 10.saas~18+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: ສີສຸວັນ ສັງບົວບຸລົມ , 2023\n" -"Language-Team: Lao (https://app.transifex.com/odoo/teams/41243/lo/)\n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" +"PO-Revision-Date: 2017-09-20 11:33+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" -"Language: lo\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: calendar_sms @@ -42,15 +38,19 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder -msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" msgstr "" #. module: calendar_sms @@ -76,7 +76,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Send SMS Text Message" msgstr "" @@ -93,11 +92,10 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" -msgstr "ປະເພດ" +msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lt.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lt.po index bfe0d95..d6389e5 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lt.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lt.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Gailius Kazlauskas , 2022 # Jonas Zinkevicius , 2022 @@ -9,21 +9,26 @@ # Silvija Butko , 2022 # Martin Trigaux, 2022 # Linas Versada , 2022 -# Greta Šeštokė, 2024 -# +# "Tiffany Chang (tic)" , 2025. +# Vytenis Viruišis , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Greta Šeštokė, 2024\n" -"Language-Team: Lithuanian (https://app.transifex.com/odoo/teams/41243/lt/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-10-04 02:31+0000\n" +"Last-Translator: Vytenis Viruišis " +"\n" +"Language-Team: Lithuanian \n" +"Language: 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" +"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" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -35,6 +40,13 @@ msgstr "Įvykis kalendoriuje" msgid "Calendar Event: Reminder" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Rodomas pavadinimas" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -48,17 +60,23 @@ msgstr "Renginio signalo valdytojas" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" + #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" @@ -75,17 +93,12 @@ msgid "SMS Text Message" msgstr "SMS žinutė" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Siųsti SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Siųsti SMS žinutę" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -99,11 +112,13 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" -msgstr "Šiuose susitikimuose nėra dalyvių." +msgstr "Šiuose susitikimuose nėra dalyvių" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Tipas" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Siųsti SMS žinutę" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lv.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lv.po index cf6e9d4..7fe3c46 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lv.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/lv.po @@ -1,24 +1,26 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Anzelika Adejanova, 2022 -# ievaputnina , 2023 -# +# +# Weblate , 2025. msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server 15.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: ievaputnina , 2023\n" -"Language-Team: Latvian (https://app.transifex.com/odoo/teams/41243/lv/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:33+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Latvian \n" +"Language: 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" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -30,6 +32,13 @@ msgstr "Kalendāra notikums" msgid "Calendar Event: Reminder" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -38,20 +47,26 @@ msgstr "Notikuma atgadinājums" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager msgid "Event Alarm Manager" -msgstr "Notikumu atgādinājumu pārvaldnieks" +msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" msgstr "" #. module: calendar_sms @@ -69,17 +84,12 @@ msgstr "" msgid "SMS Text Message" msgstr "" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "Sūtīt SMS" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "Nosūtīt īsziņu" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -94,9 +104,8 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" -msgstr "Šajos pasākumos nav neviena dalībnieka" +msgstr "" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/mk.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/mk.po index 78eee30..ed88c39 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/mk.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/mk.po @@ -1,48 +1,101 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 10.saas~18+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" "PO-Revision-Date: 2017-09-20 11:33+0000\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" -"Language: mk\n" "Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" +msgid "Calendar Event" +msgstr "" + +#. module: calendar_sms +#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder +msgid "Calendar Event: Reminder" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" +msgid "Event Alarm" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" +msgid "Event Alarm Manager" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Event reminder: %(name)s, %(time)s." +msgstr "" + +#. module: calendar_sms +#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "SMS" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +msgid "SMS Template" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms +msgid "SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Send SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "Send SMS to attendees" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +msgid "Template used to render SMS reminder content." +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "There are no attendees on these events" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type +msgid "Type" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ml.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ml.po deleted file mode 100644 index 4bb467d..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ml.po +++ /dev/null @@ -1,104 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -# Translators: -# Niyas Raphy, 2023 -# Nikhil Krishnan, 2023 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Nikhil Krishnan, 2023\n" -"Language-Team: Malayalam (https://app.transifex.com/odoo/teams/41243/ml/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: ml\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Calendar Event" -msgstr "കലണ്ടർ ഇവന്റ്" - -#. module: calendar_sms -#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder -msgid "Calendar Event: Reminder" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event Alarm" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "Event Alarm Manager" -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Event reminder: %(name)s, %(time)s." -msgstr "" - -#. module: calendar_sms -#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder -msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited -msgid "SMS" -msgstr "എസ്എംഎസ്" - -#. module: calendar_sms -#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id -msgid "SMS Template" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms -msgid "SMS Text Message" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "SMS അയയ്ക്കുക" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id -msgid "Template used to render SMS reminder content." -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "There are no attendees on these events" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type -msgid "Type" -msgstr "ടൈപ്പ്" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/mn.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/mn.po index 517aa79..eed85c6 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/mn.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/mn.po @@ -1,26 +1,28 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # tserendavaa tsogtoo , 2022 # Uuganbayar Batbaatar , 2022 # Martin Trigaux, 2022 # Batmunkh Ganbat , 2022 -# +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Batmunkh Ganbat , 2022\n" -"Language-Team: Mongolian (https://app.transifex.com/odoo/teams/41243/mn/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:07+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Mongolian \n" +"Language: mn\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: mn\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -32,6 +34,13 @@ msgstr "Календарийн арга хэмжээ" msgid "Calendar Event: Reminder" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Дэлгэрэнгүй нэр" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -45,17 +54,23 @@ msgstr "Арга хэмжээний анхааруулгын Менежер" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" + #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" @@ -72,17 +87,12 @@ msgid "SMS Text Message" msgstr "SMS Текст мессеж" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "SMS илгээх" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -96,7 +106,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ms.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/my.po similarity index 65% rename from odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ms.po rename to odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/my.po index 2ef2bcf..9730eca 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ms.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/my.po @@ -1,23 +1,23 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# -# Translators: -# Niyas Raphy, 2022 -# +# * calendar_sms +# +# Weblate , 2025. msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server 18.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Niyas Raphy, 2022\n" -"Language-Team: Malay (https://app.transifex.com/odoo/teams/41243/ms/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:13+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Burmese \n" +"Language: my\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: ms\n" "Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -29,6 +29,13 @@ msgstr "" msgid "Calendar Event: Reminder" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "ပြသသော အမည်" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -42,17 +49,23 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "နံပါတ်" + #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" @@ -68,17 +81,12 @@ msgstr "" msgid "SMS Text Message" msgstr "" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "SMS ပို့" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -93,11 +101,10 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" -msgstr "taip" +msgstr "အမျိုးအစား" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/nb.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/nb.po index 95ff534..ca3a328 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/nb.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/nb.po @@ -1,26 +1,28 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Jorunn D. Newth, 2022 # Marius Stedjan , 2022 # Martin Trigaux, 2022 # Henning Fyllingsnes, 2023 -# +# "Tiffany Chang (tic)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Henning Fyllingsnes, 2023\n" -"Language-Team: Norwegian Bokmål (https://app.transifex.com/odoo/teams/41243/nb/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-16 18:29+0000\n" +"Last-Translator: \"Tiffany Chang (tic)\" \n" +"Language-Team: Norwegian Bokmål \n" +"Language: nb\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: nb\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -32,6 +34,13 @@ msgstr "Kalenderhendelse" msgid "Calendar Event: Reminder" msgstr "Kalenderhendelse: påminnelse" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Visningsnavn" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -45,17 +54,23 @@ msgstr "Hendelse Alarm Styring" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" + #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" @@ -71,16 +86,11 @@ msgstr "SMS-mal" msgid "SMS Text Message" msgstr "SMS-melding" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "Send SMS" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" msgstr "Send SMS" #. module: calendar_sms @@ -96,7 +106,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" @@ -104,3 +113,6 @@ msgstr "" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Type" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Send SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ne.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ne.po index a68dba8..cfb430c 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ne.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ne.po @@ -1,48 +1,101 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 10.saas~18+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" +"POT-Creation-Date: 2023-05-16 13:49+0000\n" "PO-Revision-Date: 2017-09-20 11:33+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" -"Language: ne\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" +msgid "Calendar Event" +msgstr "" + +#. module: calendar_sms +#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder +msgid "Calendar Event: Reminder" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" +msgid "Event Alarm" msgstr "" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" +msgid "Event Alarm Manager" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Event reminder: %(name)s, %(time)s." +msgstr "" + +#. module: calendar_sms +#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +msgid "Event reminder: {{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_alarm.py:0 +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_notify_responsible +msgid "Notify Responsible" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "SMS" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +msgid "SMS Template" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms +msgid "SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Send SMS Text Message" +msgstr "" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "Send SMS to attendees" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +msgid "Template used to render SMS reminder content." +msgstr "" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "There are no attendees on these events" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type +msgid "Type" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/nl.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/nl.po index 595ab1b..9ee06c9 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/nl.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/nl.po @@ -1,24 +1,28 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 -# Jolien De Paepe, 2023 -# +# Jolien De Paepe, 2022 +# +# "Dylan Kiss (dyki)" , 2025. +# Bren Driesen , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Jolien De Paepe, 2023\n" -"Language-Team: Dutch (https://app.transifex.com/odoo/teams/41243/nl/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-13 14:24+0000\n" +"Last-Translator: Bren Driesen \n" +"Language-Team: Dutch \n" +"Language: 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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -30,6 +34,13 @@ msgstr "Agenda-afspraak" msgid "Calendar Event: Reminder" msgstr "Agenda-afspraak: Herinnering" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Weergavenaam" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -43,18 +54,24 @@ msgstr "Afspraak alarm manager" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Afspraak-herinnering: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Herinnering evenement: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Herinnering afspraak: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -69,19 +86,14 @@ msgstr "SMS-sjabloon" #. module: calendar_sms #: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms msgid "SMS Text Message" -msgstr "SMS bericht" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "SMS verzenden" +msgstr "SMS-bericht" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "SMS verzenden" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "Sms verzenden" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -96,7 +108,6 @@ msgstr "Sjabloon gebruikt om sms-herinneringsinhoud weer te geven." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Er zijn geen deelnemers bij deze afspraak" @@ -104,3 +115,6 @@ msgstr "Er zijn geen deelnemers bij deze afspraak" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Soort" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Verzend SMS tekstbericht" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/no.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/no.po deleted file mode 100644 index 8398e3b..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/no.po +++ /dev/null @@ -1,99 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Language-Team: Norwegian (https://app.transifex.com/odoo/teams/41243/no/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: no\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Calendar Event" -msgstr "" - -#. module: calendar_sms -#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder -msgid "Calendar Event: Reminder" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event Alarm" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "Event Alarm Manager" -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Event reminder: %(name)s, %(time)s." -msgstr "" - -#. module: calendar_sms -#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder -msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited -msgid "SMS" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id -msgid "SMS Template" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms -msgid "SMS Text Message" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id -msgid "Template used to render SMS reminder content." -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "There are no attendees on these events" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type -msgid "Type" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/pl.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/pl.po index 3a327a8..9344499 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/pl.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/pl.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Natalia Gros , 2022 # Tomasz Leppich , 2022 @@ -12,20 +12,25 @@ # Wojciech Warczakowski , 2022 # Dawid Prus, 2022 # Tadeusz Karpiński , 2023 -# +# "Dylan Kiss (dyki)" , 2025. +# Mateusz Lorczak , 2026. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Tadeusz Karpiński , 2023\n" -"Language-Team: Polish (https://app.transifex.com/odoo/teams/41243/pl/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2026-02-25 14:42+0000\n" +"Last-Translator: Mateusz Lorczak \n" +"Language-Team: Polish \n" +"Language: 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" +"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" +"X-Generator: Weblate 5.14.3\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -37,6 +42,13 @@ msgstr "Wydarzenie w kalendarzu" msgid "Calendar Event: Reminder" msgstr "Wydarzenie w kalendarzu: Przypomnienie" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Nazwa wyświetlana" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -50,18 +62,24 @@ msgstr "Zarządzanie alarmem wydarzenia" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Przypomnienie o wydarzeniu: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Przypomnienie o wydarzeniu: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Przypomnienie o wydarzeniu: {{ object.name }}, {{ object.get_display_time_tz" +"(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -79,17 +97,12 @@ msgid "SMS Text Message" msgstr "Wiadomość SMS" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Wyślij SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Wyślij wiadomość SMS" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -103,7 +116,6 @@ msgstr "Szablon używany do renderowania treści przypomnienia SMS" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Nie ma uczestników w tych wydarzeniach" @@ -111,3 +123,6 @@ msgstr "Nie ma uczestników w tych wydarzeniach" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Typ" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Wyślij wiadomość SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/pt.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/pt.po index f939fc6..2effd11 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/pt.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/pt.po @@ -1,26 +1,29 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Reinaldo Ramos , 2022 # Pedro Filipe , 2022 # Manuela Silva , 2022 -# Rita Bastos, 2024 -# +# "Tiffany Chang (tic)" , 2025. +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Rita Bastos, 2024\n" -"Language-Team: Portuguese (https://app.transifex.com/odoo/teams/41243/pt/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:07+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Portuguese \n" +"Language: 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" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : ((n != 0 && n % " +"1000000 == 0) ? 1 : 2);\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -32,6 +35,13 @@ msgstr "Evento do Calendário" msgid "Calendar Event: Reminder" msgstr "Lembrete do Evento do Calendário" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Nome" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -45,16 +55,24 @@ msgstr "Gestor de Alarmes do Evento" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." -msgstr "" +msgstr "Lembrete de evento: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +"Lembrete do evento: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -72,17 +90,12 @@ msgid "SMS Text Message" msgstr "Mensagem de Texto SMS" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Enviar SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Enviar Mensagem de Texto SMS" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -91,14 +104,13 @@ msgstr "Enviar SMS aos participantes" #. module: calendar_sms #: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id msgid "Template used to render SMS reminder content." -msgstr "" +msgstr "Modelo usado para renderizar o conteúdo do lembrete de SMS." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" -msgstr "" +msgstr "Não há participantes nesses eventos" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/pt_BR.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/pt_BR.po index f4e1275..d187ba8 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/pt_BR.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/pt_BR.po @@ -1,60 +1,76 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 -# Kevilyn Rosa, 2023 -# +# +# "Dylan Kiss (dyki)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Kevilyn Rosa, 2023\n" -"Language-Team: Portuguese (Brazil) (https://app.transifex.com/odoo/teams/41243/pt_BR/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-16 10:03+0000\n" +"Last-Translator: \"Dylan Kiss (dyki)\" \n" +"Language-Team: Portuguese (Brazil) \n" +"Language: 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" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : ((n != 0 && n % " +"1000000 == 0) ? 1 : 2);\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event msgid "Calendar Event" -msgstr "Evento Calendário" +msgstr "Evento do calendário" #. module: calendar_sms #: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder msgid "Calendar Event: Reminder" -msgstr "Evento Calendário: Lembrete" +msgstr "Evento do calendário: lembrete" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Nome exibido" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" -msgstr "Alarme do Evento" +msgstr "Alarme do evento" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager msgid "Event Alarm Manager" -msgstr "Gerenciamento de Alarme do Evento" +msgstr "Gerenciamento de alarme do evento" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Lembrete de evento: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Lembrete do evento: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Lembrete do evento: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -72,17 +88,12 @@ msgid "SMS Text Message" msgstr "Enviar SMS" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Enviar SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Enviar mensagem de texto SMS" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -96,7 +107,6 @@ msgstr "Modelo usado para renderizar o conteúdo do lembrete de SMS." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Não há participantes nesses eventos" @@ -104,3 +114,6 @@ msgstr "Não há participantes nesses eventos" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Tipo" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Enviar mensagem de texto SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ro.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ro.po index bb090eb..84c1efe 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ro.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ro.po @@ -1,27 +1,31 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 # Fekete Mihai , 2022 # Dorin Hongu , 2022 # Foldi Robert , 2022 # Claudia Baisan, 2023 -# +# Weblate , 2025. +# Ciocoiu Beatrice-Flavia , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Claudia Baisan, 2023\n" -"Language-Team: Romanian (https://app.transifex.com/odoo/teams/41243/ro/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-17 17:21+0000\n" +"Last-Translator: Ciocoiu Beatrice-Flavia \n" +"Language-Team: Romanian \n" +"Language: ro\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: ro\n" -"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " +"20)) ? 1 : 2;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -33,6 +37,13 @@ msgstr "Eveniment Calendar" msgid "Calendar Event: Reminder" msgstr "Eveniment Calendar: Memento" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Nume afișat" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -46,18 +57,24 @@ msgstr "Manager alarmă eveniment" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Memento eveniment: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Memento eveniment: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Memento eveniment: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -75,21 +92,16 @@ msgid "SMS Text Message" msgstr "Mesaj text SMS" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Trimite SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Trimiteți mesaj SMS" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" -msgstr "Trimite SMS la participanți" +msgstr "Trimite SMS la participanți" #. module: calendar_sms #: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id @@ -99,7 +111,6 @@ msgstr "Șablon utilizat pentru a genera conținutul memento-ului SMS." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Nu există participanți la aceste evenimente" @@ -107,3 +118,6 @@ msgstr "Nu există participanți la aceste evenimente" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Tip" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Trimiteți mesaj SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ru.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ru.po index d978a3d..6a5fb34 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ru.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ru.po @@ -1,108 +1,122 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: -# Ivan Kropotkin , 2022 -# Evgeniia Kotova, 2022 -# Сергей Шебанин , 2022 -# alenafairy, 2023 -# +# "Anastasiia Koroleva (koan)" , 2025. +# Weblate , 2025. msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server 17.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: alenafairy, 2023\n" -"Language-Team: Russian (https://app.transifex.com/odoo/teams/41243/ru/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-12-03 08:31+0000\n" +"Last-Translator: \"Anastasiia Koroleva (koan)\" \n" +"Language-Team: Russian \n" +"Language: 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" +"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" +"X-Generator: Weblate 5.14.3\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event msgid "Calendar Event" -msgstr "Встреча в календаре" +msgstr "Календарное событие" #. module: calendar_sms #: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder msgid "Calendar Event: Reminder" -msgstr "Событие в календаре: Напоминание" +msgstr "Событие календаря: Напоминание" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Display Name" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" -msgstr "Уведомление о встрече" +msgstr "Сигнал тревоги" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager msgid "Event Alarm Manager" -msgstr "Менеджер оповещения о встречах" +msgstr "Менеджер аварийных сигналов событий" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." -msgstr "Напоминание о событии: %(name)s, %(time)s." +msgstr "Напоминание о событиях: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Напоминание о событии: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Напоминание о событии: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "SMS" -msgstr "SMS" +msgstr "СМС" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id msgid "SMS Template" -msgstr "Шаблон SMS" +msgstr "SMS шаблон" #. module: calendar_sms #: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms msgid "SMS Text Message" -msgstr "SMS" +msgstr "Текстовое сообщение SMS" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Отправить SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Отправить SMS сообщение" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" -msgstr "Отправьте SMS участникам" +msgstr "Отправка SMS участникам" #. module: calendar_sms #: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id msgid "Template used to render SMS reminder content." -msgstr "Шаблон для генерации SMS напоминания" +msgstr "Шаблон для генерации SMS напоминания." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" -msgstr "В этих событиях никто не участвует" +msgstr "На этих мероприятиях нет посетителей" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" -msgstr "Раздел" +msgstr "Тип" + +#~ msgid "Notify Responsible" +#~ msgstr "Уведомить ответственного" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Отправить текстовое сообщение SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sk.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sk.po index 450484b..11052ce 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sk.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sk.po @@ -1,24 +1,25 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# # Translators: # Jan Prokop, 2022 # Martin Trigaux, 2022 -# +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" "PO-Revision-Date: 2022-09-22 05:45+0000\n" "Last-Translator: Martin Trigaux, 2022\n" "Language-Team: Slovak (https://app.transifex.com/odoo/teams/41243/sk/)\n" +"Language: 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" +"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: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -30,6 +31,13 @@ msgstr "Udalosť kalendára" msgid "Calendar Event: Reminder" msgstr "Udalosť kalendára: Pripomenutie" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -43,15 +51,21 @@ msgstr "Správca upozornenia udalostí" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Pripomienka udalosti:%(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" msgstr "" #. module: calendar_sms @@ -70,17 +84,12 @@ msgid "SMS Text Message" msgstr "Text SMS" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Zaslať SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Poslať SMS textovú správu" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -94,7 +103,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" @@ -102,3 +110,6 @@ msgstr "" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Typ" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Poslať SMS textovú správu" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sl.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sl.po index bb70ffe..c36265c 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sl.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sl.po @@ -1,37 +1,45 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Jasmina Macur , 2022 # matjaz k , 2022 -# Neun Pro, 2023 -# Aleš Pipan, 2025 -# +# "Tiffany Chang (tic)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Aleš Pipan, 2025\n" -"Language-Team: Slovenian (https://app.transifex.com/odoo/teams/41243/sl/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-16 21:15+0000\n" +"Last-Translator: \"Tiffany Chang (tic)\" \n" +"Language-Team: Slovenian \n" +"Language: 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" +"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || " +"n%100==4 ? 2 : 3;\n" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event msgid "Calendar Event" -msgstr "Calendar Event" +msgstr "Dogodek v koledarju" #. module: calendar_sms #: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder msgid "Calendar Event: Reminder" msgstr "Dogodek v koledarju: Opomnik" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -45,18 +53,24 @@ msgstr "Upravitelj alarmov dogodkov" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Opomnik na dogodek: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Opomnik na dogodek: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Opomnik na dogodek: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -74,17 +88,12 @@ msgid "SMS Text Message" msgstr "SMS Besedilno sporočilo" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "Pošlji SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -98,7 +107,6 @@ msgstr "Predloga, ki se uporablja za upodabljanje vsebine opomnikov SMS." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Na teh dogodkih ni udeležencev" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sq.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sq.po index e33fcbd..57182fb 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sq.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sq.po @@ -1,19 +1,20 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server 19.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Language-Team: Albanian (https://app.transifex.com/odoo/teams/41243/sq/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-12-30 18:36+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: sq\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: \n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -25,6 +26,13 @@ msgstr "" msgid "Calendar Event: Reminder" msgstr "" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -38,15 +46,21 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" +msgstr "" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" msgstr "" #. module: calendar_sms @@ -64,16 +78,11 @@ msgstr "" msgid "SMS Text Message" msgstr "" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" msgstr "" #. module: calendar_sms @@ -89,7 +98,6 @@ msgstr "" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sr@latin.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sr@latin.po index e59ed5b..e809c70 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sr@latin.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sr@latin.po @@ -1,51 +1,118 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# +# # Translators: -# Martin Trigaux , 2017 +# Dragan Vukosavljevic , 2022 +# Weblate , 2025. msgid "" msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18+e\n" +"Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-20 11:33+0000\n" -"PO-Revision-Date: 2017-09-20 11:33+0000\n" -"Last-Translator: Martin Trigaux , 2017\n" -"Language-Team: Serbian (Latin) (https://www.transifex.com/odoo/teams/41243/sr%40latin/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:07+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Serbian (Latin script) \n" +"Language: sr@latin\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: sr@latin\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" +"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" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Event" -msgstr "Događaj" +msgid "Calendar Event" +msgstr "Događaj kalendara" + +#. module: calendar_sms +#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder +msgid "Calendar Event: Reminder" +msgstr "Događaj kalendara: podsetnik" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Naziv za prikaz" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event alarm" -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:23 -#, python-format -msgid "Event reminder: %s on %s." -msgstr "" - -#. module: calendar_sms -#: code:addons/calendar_sms/models/calendar.py:24 -#, python-format -msgid "SMS text message reminder sent !" -msgstr "" - -#. module: calendar_sms -#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli -msgid "Send SMS to attendees" -msgstr "" +msgid "Event Alarm" +msgstr "Alarm događaja" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "calendar.alarm_manager" +msgid "Event Alarm Manager" +msgstr "Menadžer alarma događaja" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "Event reminder: %(name)s, %(time)s." +msgstr "Podsetnik događaja: %(name)s, %(time)s." + +#. module: calendar_sms +#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +msgid "" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" +"Podsetnik na događaj: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "SMS" +msgstr "SMS" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +msgid "SMS Template" +msgstr "SMS šablon" + +#. module: calendar_sms +#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms +msgid "SMS Text Message" +msgstr "SMS poruka" + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "Šalji SMS" + +#. module: calendar_sms +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +msgid "Send SMS to attendees" +msgstr "Šalji SMS učesnicima" + +#. module: calendar_sms +#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +msgid "Template used to render SMS reminder content." +msgstr "Šablon koji se koristi za prikazivanje sadržaja SMS podsetnika." + +#. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 +msgid "There are no attendees on these events" +msgstr "Na ovim događajima nema učesnika" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type +msgid "Type" +msgstr "Vrsta" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Pošalji SMS poruku" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sv.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sv.po index a7707a5..b15259f 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sv.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sv.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Han Wong , 2022 # Jakob Krabbe , 2022 @@ -10,21 +10,23 @@ # Martin Trigaux, 2022 # Chrille Hedberg , 2022 # Kim Asplund , 2022 -# Larissa Manderfeld, 2024 -# +# "Tiffany Chang (tic)" , 2025. +# Hanna Kharraziha , 2026. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Larissa Manderfeld, 2024\n" -"Language-Team: Swedish (https://app.transifex.com/odoo/teams/41243/sv/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2026-03-06 17:44+0000\n" +"Last-Translator: Hanna Kharraziha \n" +"Language-Team: Swedish \n" +"Language: 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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.16.1\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -36,31 +38,44 @@ msgstr "Kalenderhändelse" msgid "Calendar Event: Reminder" msgstr "Kalenderhändelse: påminnelse" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Visningsnamn" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" -msgstr "Händelsealarm" +msgstr "Påminnelse" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager msgid "Event Alarm Manager" -msgstr "Hanterare för händelsealarm" +msgstr "Hantera påminnelser" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." -msgstr "Kalenderpåminnelse: %(name)s, %(time)s." +msgstr "Påminnelse: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Påminnelse om evenemang: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Påminnelse om händelse: {{ object.name }}, {{ object.get_display_time_tz" +"(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -77,16 +92,11 @@ msgstr "SMS-mall" msgid "SMS Text Message" msgstr "SMS textmeddelande" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "Skicka SMS" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" msgstr "Skicka SMS" #. module: calendar_sms @@ -102,11 +112,13 @@ msgstr "Mall som används för att återge innehåll på SMS-påminnelser." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" -msgstr "Det finns inga deltagare på dessa evenemang" +msgstr "Det finns inga deltagare på dessa händelser" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Typ" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Skicka SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sw.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sw.po deleted file mode 100644 index daca3c3..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sw.po +++ /dev/null @@ -1,99 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Language-Team: Swahili (https://app.transifex.com/odoo/teams/41243/sw/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: sw\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Calendar Event" -msgstr "" - -#. module: calendar_sms -#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder -msgid "Calendar Event: Reminder" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event Alarm" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "Event Alarm Manager" -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Event reminder: %(name)s, %(time)s." -msgstr "" - -#. module: calendar_sms -#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder -msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited -msgid "SMS" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id -msgid "SMS Template" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms -msgid "SMS Text Message" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id -msgid "Template used to render SMS reminder content." -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "There are no attendees on these events" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type -msgid "Type" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ta.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ta.po deleted file mode 100644 index 7fcd039..0000000 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/ta.po +++ /dev/null @@ -1,99 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * calendar_sms -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Language-Team: Tamil (https://app.transifex.com/odoo/teams/41243/ta/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: ta\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_event -msgid "Calendar Event" -msgstr "" - -#. module: calendar_sms -#: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder -msgid "Calendar Event: Reminder" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm -msgid "Event Alarm" -msgstr "" - -#. module: calendar_sms -#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager -msgid "Event Alarm Manager" -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Event reminder: %(name)s, %(time)s." -msgstr "" - -#. module: calendar_sms -#: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder -msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited -msgid "SMS" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id -msgid "SMS Template" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms -msgid "SMS Text Message" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited -msgid "Send SMS to attendees" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id -msgid "Template used to render SMS reminder content." -msgstr "" - -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "There are no attendees on these events" -msgstr "" - -#. module: calendar_sms -#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type -msgid "Type" -msgstr "" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/th.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/th.po index 02beaef..b91cea7 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/th.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/th.po @@ -1,25 +1,27 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 # Wichanon Jamwutthipreecha, 2022 -# Rasareeyar Lappiam, 2023 -# +# +# "Tiffany Chang (tic)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Rasareeyar Lappiam, 2023\n" -"Language-Team: Thai (https://app.transifex.com/odoo/teams/41243/th/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-16 21:32+0000\n" +"Last-Translator: \"Tiffany Chang (tic)\" \n" +"Language-Team: Thai \n" +"Language: 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" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -31,6 +33,13 @@ msgstr "ปฎิทินอีเวนต์" msgid "Calendar Event: Reminder" msgstr "ปฎิทินอีเวนต์: เตือนความจำ" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "แสดงชื่อ" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -44,18 +53,24 @@ msgstr "ตัวจัดการการเตือนอีเวนต์ #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "เตือนความจำอีเวนต์: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"การแจ้งเตือนกิจกรรม: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"การแจ้งเตือนกิจกรรม: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ไอดี" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -73,17 +88,12 @@ msgid "SMS Text Message" msgstr "SMS ข้อความ" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "ส่ง SMS" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "ส่งข้อความ SMS" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -97,7 +107,6 @@ msgstr "เทมเพลตที่ใช้แสดงเนื้อหา #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "ไม่มีผู้เข้าร่วมในอีเวนต์เหล่านี้" @@ -105,3 +114,6 @@ msgstr "ไม่มีผู้เข้าร่วมในอีเวนต #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "ประเภท" + +#~ msgid "Send SMS Text Message" +#~ msgstr "ส่งข้อความ SMS" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/tr.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/tr.po index 06a96f7..3a06eef 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/tr.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/tr.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Nadir Gazioglu , 2022 # Ediz Duman , 2022 @@ -12,20 +12,23 @@ # Umur Akın , 2022 # Yedigen, 2022 # Ertuğrul Güreş , 2023 -# +# "Dylan Kiss (dyki)" , 2025. +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Ertuğrul Güreş , 2023\n" -"Language-Team: Turkish (https://app.transifex.com/odoo/teams/41243/tr/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-08 01:50+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Turkish \n" +"Language: 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" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -37,6 +40,13 @@ msgstr "Takvim Etkinliği" msgid "Calendar Event: Reminder" msgstr "Takvim Etkinliği: Hatırlatma" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "İsim Göster" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -50,18 +60,24 @@ msgstr "Etkinlik Alarmı Yöneticisi" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Event reminder: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Etkinlik hatırlatıcısı: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Etkinlik hatırlatıcısı: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -79,17 +95,12 @@ msgid "SMS Text Message" msgstr "SMS Metin Mesajı" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "SMS Gönder" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "SMS Metin Mesajı Gönderin" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -103,11 +114,13 @@ msgstr "SMS hatırlatıcı içeriği oluşturmak için kullanılan şablon." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Bu etkinliklerde katılımcı yok" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" -msgstr "Tür" +msgstr "Tip" + +#~ msgid "Send SMS Text Message" +#~ msgstr "SMS Metin Mesajı Gönderin" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/uk.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/uk.po index 715adde..e0ac499 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/uk.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/uk.po @@ -1,24 +1,30 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 # Alina Lisnenko , 2022 -# +# "Tiffany Chang (tic)" , 2025. +# Weblate , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Alina Lisnenko , 2022\n" -"Language-Team: Ukrainian (https://app.transifex.com/odoo/teams/41243/uk/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-11-16 15:07+0000\n" +"Last-Translator: Weblate \n" +"Language-Team: Ukrainian \n" +"Language: 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" +"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" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -30,6 +36,13 @@ msgstr "Календар подій" msgid "Calendar Event: Reminder" msgstr "Календар подій: Нагадування" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Назва для відображення" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -43,18 +56,24 @@ msgstr "Менеджер сповіщення про подію" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Нагадування про подію: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Нагадування про подію: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Нагадування про подію: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -69,19 +88,14 @@ msgstr "Шаблон SMS" #. module: calendar_sms #: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms msgid "SMS Text Message" -msgstr "Текст SMS-повідомлення " - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "Надішліть SMS" +msgstr "Текст SMS-повідомлення" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Надіслати SMS-повідомлення" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "Надішліть SMS" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -96,7 +110,6 @@ msgstr "Шаблон, який використовується для відт #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Немає учасників на цих подіях" @@ -104,3 +117,6 @@ msgstr "Немає учасників на цих подіях" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Тип" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Надіслати SMS-повідомлення" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sr.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/uz.po similarity index 53% rename from odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sr.po rename to odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/uz.po index 3a4f39f..a0e0ef0 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/sr.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/uz.po @@ -1,106 +1,133 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * calendar_sms -# -# Translators: -# Dragan Vukosavljevic , 2022 -# コフスタジオ, 2024 -# +# +# +# Translated by: +# Deepvision - info@deepvision.uz | +998 77-093-0007 +# Amon Olimov - amon.bars@gmail.com +# Jonibek Yorqulov - j.yorqulov@deepvision.uz +# Mirzohidkhon Ulugkhujaev ulugkhujayevmirzohidxon@gmail.com +# +# msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server 19.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: コフスタジオ, 2024\n" -"Language-Team: Serbian (https://app.transifex.com/odoo/teams/41243/sr/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-10-08 18:37+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: uz\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" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event +#, fuzzy msgid "Calendar Event" -msgstr "Događaj kalendara" +msgstr "Taqvim hodisasi" #. module: calendar_sms #: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder +#, fuzzy msgid "Calendar Event: Reminder" -msgstr "Događaj kalendara: podsetnik" +msgstr "Taqvim hodisasi: Eslatma" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +#, fuzzy +msgid "Display Name" +msgstr "Ko‘rsatiladigan nom" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm +#, fuzzy msgid "Event Alarm" -msgstr "Alarm događaja" +msgstr "Hodisa signali" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm_manager +#, fuzzy msgid "Event Alarm Manager" -msgstr "Menadžer alarma događaja" +msgstr "Hodisa signallari boshqaruvchisi" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format +#, fuzzy msgid "Event reminder: %(name)s, %(time)s." -msgstr "Podsetnik događaja: %(name)s, %(time)s." +msgstr "Hodisa eslatmasi: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder +#, fuzzy msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Podsetnik za događaj: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Hodisa eslatmasi: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +#, fuzzy +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +#, fuzzy msgid "SMS" msgstr "SMS" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__sms_template_id +#, fuzzy msgid "SMS Template" -msgstr "SMS šablon" +msgstr "SMS shabloni" #. module: calendar_sms #: model:ir.model.fields.selection,name:calendar_sms.selection__calendar_alarm__alarm_type__sms +#, fuzzy msgid "SMS Text Message" -msgstr "SMS poruka" - -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "Šalji SMS" +msgstr "SMS xabari" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Pošalji SMS poruku" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +#, fuzzy +msgid "Send SMS" +msgstr "SMS yuborish" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited +#, fuzzy msgid "Send SMS to attendees" -msgstr "Šalji SMS učesnicima" +msgstr "Ishtirokchilarga SMS yuborish" #. module: calendar_sms #: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id +#, fuzzy msgid "Template used to render SMS reminder content." -msgstr "Šablon koji se koristi za prikazivanje sadržaja SMS podsetnika." +msgstr "SMS eslatma mazmunini ko‘rsatish uchun ishlatiladigan shablon." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format +#, fuzzy msgid "There are no attendees on these events" -msgstr "Na ovim događajima nema učesnika" +msgstr "Bu hodisolarda hech qanday ishtirokchi yo‘q" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type +#, fuzzy msgid "Type" -msgstr "Vrsta" +msgstr "Tur" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/vi.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/vi.po index d669602..63c4c67 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/vi.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/vi.po @@ -1,34 +1,43 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 -# Thi Huong Nguyen, 2024 -# +# +# "Dylan Kiss (dyki)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Thi Huong Nguyen, 2024\n" -"Language-Team: Vietnamese (https://app.transifex.com/odoo/teams/41243/vi/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-16 02:30+0000\n" +"Last-Translator: \"Dylan Kiss (dyki)\" \n" +"Language-Team: Vietnamese \n" +"Language: 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" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event msgid "Calendar Event" -msgstr "Lịch sự kiện" +msgstr "Sự kiện trên lịch" #. module: calendar_sms #: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder msgid "Calendar Event: Reminder" -msgstr "Calendar Event: Reminder" +msgstr "Sự kiện trên lịch: Nhắc nhở" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "Tên hiển thị" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm @@ -43,18 +52,24 @@ msgstr "Quản lý cảnh báo sự kiện" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "Event reminder: %(name)s, %(time)s." #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"Nhắc nhở sự kiện: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Nhắc nhở sự kiện: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -71,17 +86,12 @@ msgstr "Mẫu SMS" msgid "SMS Text Message" msgstr "Thông điệp SMS" -#. module: calendar_sms -#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited -msgid "Send SMS" -msgstr "Send SMS" - #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "Send SMS Text Message" +#: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited +msgid "Send SMS" +msgstr "Gửi SMS" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -91,12 +101,11 @@ msgstr "Gửi SMS tới người tham dự" #. module: calendar_sms #: model:ir.model.fields,help:calendar_sms.field_calendar_alarm__sms_template_id msgid "Template used to render SMS reminder content." -msgstr "Mẫu được dùng để tạo nội dung SMS nhắc nhở. " +msgstr "Mẫu được dùng để tạo nội dung SMS nhắc nhở." #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "Những sự kiện này không có người tham dự" @@ -104,3 +113,6 @@ msgstr "Những sự kiện này không có người tham dự" #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "Loại" + +#~ msgid "Send SMS Text Message" +#~ msgstr "Send SMS Text Message" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/zh_CN.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/zh_CN.po index 33fdf99..9da0111 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/zh_CN.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/zh_CN.po @@ -1,36 +1,45 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: # Martin Trigaux, 2022 # Jeffery CHEN , 2022 # Emily Jia , 2023 -# +# "Tiffany Chang (tic)" , 2025. msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Emily Jia , 2023\n" -"Language-Team: Chinese (China) (https://app.transifex.com/odoo/teams/41243/zh_CN/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-16 15:21+0000\n" +"Last-Translator: \"Tiffany Chang (tic)\" \n" +"Language-Team: Chinese (Simplified Han script) \n" +"Language: 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" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event msgid "Calendar Event" -msgstr "日历事件" +msgstr "日历活动" #. module: calendar_sms #: model:sms.template,name:calendar_sms.sms_template_data_calendar_reminder msgid "Calendar Event: Reminder" msgstr "日历活动:提醒" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "显示名称" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -44,18 +53,24 @@ msgstr "活动提醒管理" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." -msgstr "‎事件提醒:‎%(name)s,%(time)s。" +msgstr "活动提醒:‎%(name)s,%(time)s。" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"活动提醒:{{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) " -"}}" +"活动提醒:{{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "ID" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -73,17 +88,12 @@ msgid "SMS Text Message" msgstr "短信息" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "发送短信息" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "发送文本短信息" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -97,11 +107,13 @@ msgstr "用来呈现短信息提醒内容的模板。" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" -msgstr "这些活动中没有人参加" +msgstr "这些活动没有参会者" #. module: calendar_sms #: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__alarm_type msgid "Type" msgstr "类型" + +#~ msgid "Send SMS Text Message" +#~ msgstr "发送文本短信息" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/zh_TW.po b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/zh_TW.po index 613d9d5..dabaf27 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/zh_TW.po +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/i18n/zh_TW.po @@ -1,24 +1,26 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * calendar_sms -# +# * calendar_sms +# # Translators: -# Martin Trigaux, 2022 -# Tony Ng, 2025 -# +# Wil Odoo, 2025 +# +# "Dylan Kiss (dyki)" , 2025. msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0+e\n" +"Project-Id-Version: Odoo Server saas~18.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-15 12:50+0000\n" -"PO-Revision-Date: 2022-09-22 05:45+0000\n" -"Last-Translator: Tony Ng, 2025\n" -"Language-Team: Chinese (Taiwan) (https://app.transifex.com/odoo/teams/41243/zh_TW/)\n" +"POT-Creation-Date: 2026-01-25 18:36+0000\n" +"PO-Revision-Date: 2025-09-16 08:10+0000\n" +"Last-Translator: \"Dylan Kiss (dyki)\" \n" +"Language-Team: Chinese (Traditional Han script) \n" +"Language: 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" +"X-Generator: Weblate 5.12.2\n" #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_event @@ -30,6 +32,13 @@ msgstr "日曆活動" msgid "Calendar Event: Reminder" msgstr "日曆:活動提醒" +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__display_name +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__display_name +msgid "Display Name" +msgstr "顯示名稱" + #. module: calendar_sms #: model:ir.model,name:calendar_sms.model_calendar_alarm msgid "Event Alarm" @@ -43,18 +52,24 @@ msgstr "活動提醒管理" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "Event reminder: %(name)s, %(time)s." msgstr "活動提醒:%(name)s、%(time)s。" #. module: calendar_sms #: model:sms.template,body:calendar_sms.sms_template_data_calendar_reminder msgid "" -"Event reminder: {{ object.name }}, {{ " -"object.get_display_time_tz(object.partner_id.tz) }}" +"Event reminder: {{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" msgstr "" -"活動提醒:{{ object.name }}, {{ object.get_display_time_tz(object.partner_id.tz) " -"}}" +"活動提醒:{{ object.name }}, " +"{{ object.get_display_time_tz(object.partner_id.tz) }}" + +#. module: calendar_sms +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_alarm_manager__id +#: model:ir.model.fields,field_description:calendar_sms.field_calendar_event__id +msgid "ID" +msgstr "識別號" #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited @@ -72,17 +87,12 @@ msgid "SMS Text Message" msgstr "簡訊" #. module: calendar_sms +#. odoo-python +#: code:addons/calendar_sms/models/calendar_event.py:0 #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_tree_inherited msgid "Send SMS" msgstr "發送簡訊" -#. module: calendar_sms -#. odoo-python -#: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format -msgid "Send SMS Text Message" -msgstr "傳送簡訊" - #. module: calendar_sms #: model_terms:ir.ui.view,arch_db:calendar_sms.view_calendar_event_form_inherited msgid "Send SMS to attendees" @@ -96,7 +106,6 @@ msgstr "用於繪製短訊提醒內容的範本。" #. module: calendar_sms #. odoo-python #: code:addons/calendar_sms/models/calendar_event.py:0 -#, python-format msgid "There are no attendees on these events" msgstr "這些活動沒有參加者" diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/models/calendar_alarm_manager.py b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/models/calendar_alarm_manager.py index bfc84b3..59b9500 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/models/calendar_alarm_manager.py +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/models/calendar_alarm_manager.py @@ -4,7 +4,7 @@ from odoo import api, models -class AlarmManager(models.AbstractModel): +class CalendarAlarm_Manager(models.AbstractModel): _inherit = 'calendar.alarm_manager' @api.model @@ -21,3 +21,4 @@ class AlarmManager(models.AbstractModel): alarm = self.env['calendar.alarm'].browse(alarm_id).with_prefetch(list(events_by_alarm.keys())) events = self.env['calendar.event'].browse(event_ids).with_prefetch(all_events_ids) events._do_sms_reminder(alarm) + events._setup_event_recurrent_alarms(events_by_alarm) diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/models/calendar_event.py b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/models/calendar_event.py index 97a356f..aaa1846 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/models/calendar_event.py +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/models/calendar_event.py @@ -4,31 +4,33 @@ from odoo import models, _ from odoo.exceptions import UserError + class CalendarEvent(models.Model): _inherit = 'calendar.event' - def _sms_get_default_partners(self): - """ Method overridden from mail.thread (defined in the sms module). - SMS text messages will be sent to attendees that haven't declined the event(s). - """ - return self.mapped('attendee_ids').filtered(lambda att: att.state != 'declined' and att.partner_id.phone_sanitized).mapped('partner_id') - - def _do_sms_reminder(self, alarm): + def _do_sms_reminder(self, alarms): """ Send an SMS text reminder to attendees that haven't declined the event """ for event in self: - event._message_sms_with_template( - template=alarm.sms_template_id, - template_fallback=_("Event reminder: %(name)s, %(time)s.", name=event.name, time=event.display_time), - partner_ids=event._sms_get_default_partners().ids, - put_in_queue=False - ) + declined_partners = event.attendee_ids.filtered_domain([('state', '=', 'declined')]).partner_id + for alarm in alarms: + partners = event._mail_get_partners()[event.id].filtered( + lambda partner: partner.phone_sanitized and partner not in declined_partners + ) + if event.user_id and not alarm.notify_responsible: + partners -= event.user_id.partner_id + event._message_sms_with_template( + template=alarm.sms_template_id, + template_fallback=_("Event reminder: %(name)s, %(time)s.", name=event.name, time=event.display_time), + partner_ids=partners.ids, + put_in_queue=False + ) def action_send_sms(self): if not self.partner_ids: raise UserError(_("There are no attendees on these events")) return { 'type': 'ir.actions.act_window', - 'name': _("Send SMS Text Message"), + 'name': _("Send SMS"), 'res_model': 'sms.composer', 'view_mode': 'form', 'target': 'new', diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/tests/test_calendar_sms.py b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/tests/test_calendar_sms.py index c3f6443..e6b5cd2 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/tests/test_calendar_sms.py +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/tests/test_calendar_sms.py @@ -3,24 +3,24 @@ from datetime import datetime, timedelta from odoo import fields -from odoo.addons.sms.tests.common import SMSCase -from odoo.tests.common import TransactionCase +from odoo.addons.sms.tests.common import SMSCommon +from odoo.tests import tagged -class TestCalendarSms(TransactionCase, SMSCase): +@tagged('sms') +class TestCalendarSms(SMSCommon): @classmethod def setUpClass(cls): super(TestCalendarSms, cls).setUpClass() - now = fields.datetime.now() + now = fields.Datetime.now() cls.partner_phone = cls.env['res.partner'].create({ 'name': 'Partner With Phone Number', 'phone': '0477777777', 'country_id': cls.env.ref('base.be').id, }) - cls.partner_phone_2 = cls.env['res.partner'].create({ 'name': 'Partner With Phone Number', 'phone': '0488888888', @@ -37,7 +37,16 @@ class TestCalendarSms(TransactionCase, SMSCase): 'name': 'Partner With No Phone Number', 'country_id': cls.env.ref('base.be').id, }) - + cls.event = cls.env['calendar.event'].create({ + 'alarm_ids': [(0, 0, { + 'alarm_type': 'sms', + 'name': 'SMS Reminder', + })], + 'name': "Boostrap vs Foundation", + 'partner_ids': [(6, 0, [cls.partner_phone.id, cls.partner_no_phone.id])], + 'start': datetime(2022, 1, 1, 11, 11), + 'stop': datetime(2022, 2, 2, 22, 22), + }) cls.alarm_1h = cls.env['calendar.alarm'].create({ 'name': 'Reminder 1 Hour', 'duration': 1, @@ -53,26 +62,26 @@ class TestCalendarSms(TransactionCase, SMSCase): cls.event_1h = cls.env['calendar.event'].create({ 'name': 'Event in 1h', - 'start': now + timedelta(hours=1), - 'stop': now + timedelta(hours=2), + 'start': now + timedelta(minutes=30), # Start in 30 minutes + 'stop': now + timedelta(hours=1, minutes=30), 'alarm_ids': [(4, cls.alarm_1h.id), (4, cls.alarm_24h.id)], - 'attendee_ids': [(0, 0, {'partner_id': cls.partner_phone.id})], + 'partner_ids': [(6, 0, [cls.partner_phone.id])], }) - cls.event_1h_dup = cls.env['calendar.event'].create({ - 'name': 'Event in 1h', - 'start': now + timedelta(hours=1), - 'stop': now + timedelta(hours=2), + cls.event_1h_dup = cls.event_1h.copy(default={ 'alarm_ids': [(4, cls.alarm_1h.id), (4, cls.alarm_24h.id)], - 'attendee_ids': [(0, 0, {'partner_id': cls.partner_phone_3.id})], + 'partner_ids': [(6, 0, [cls.partner_phone_3.id])], }) + # for some reason the above is not sufficient + cls.event_1h_dup.partner_ids = cls.partner_phone_3 + # Adjust event_24h so that the 24-hour alarm falls within the last hour cls.event_24h = cls.env['calendar.event'].create({ 'name': 'Event in 24h', - 'start': now + timedelta(hours=24), - 'stop': now + timedelta(hours=25), + 'start': now + timedelta(hours=23, minutes=30), # Start in 23 hours 30 minutes + 'stop': now + timedelta(hours=24, minutes=30), 'alarm_ids': [(4, cls.alarm_1h.id), (4, cls.alarm_24h.id)], - 'attendee_ids': [(0, 0, {'partner_id': cls.partner_phone_2.id})], + 'partner_ids': [(6, 0, [cls.partner_phone_2.id])], }) cls.sms_template_1h = cls.env['sms.template'].create({ @@ -92,13 +101,9 @@ class TestCalendarSms(TransactionCase, SMSCase): def test_attendees_with_number(self): """Test if only partners with sanitized number are returned.""" - attendees = self.env['calendar.event'].create({ - 'name': "Boostrap vs Foundation", - 'start': datetime(2022, 1, 1, 11, 11), - 'stop': datetime(2022, 2, 2, 22, 22), - 'partner_ids': [(6, 0, [self.partner_phone.id, self.partner_no_phone.id])], - })._sms_get_default_partners() - self.assertEqual(len(attendees), 1, "There should be only one partner retrieved") + with self.mockSMSGateway(): + self.event._do_sms_reminder(self.event.alarm_ids) + self.assertEqual(len(self._sms), 1, "There should be only one partner retrieved") def test_send_reminder_match_both_events(self): """ @@ -110,7 +115,9 @@ class TestCalendarSms(TransactionCase, SMSCase): self.env['calendar.alarm_manager'].with_context(lastcall=lastcall)._send_reminder() self.assertEqual(len(self._sms), 3) - self.assertSMS(self.partner_phone, self.partner_phone.phone_sanitized, 'sent', + self.assertSMS(self.partner_phone, self.partner_phone.phone_sanitized, 'pending', content=self.sms_template_1h.body) - self.assertSMS(self.partner_phone_2, self.partner_phone_2.phone_sanitized, 'sent', + self.assertSMS(self.partner_phone_3, self.partner_phone_3.phone_sanitized, 'pending', + content=self.sms_template_1h.body) + self.assertSMS(self.partner_phone_2, self.partner_phone_2.phone_sanitized, 'pending', content=self.sms_template_24h.body) diff --git a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/views/calendar_views.xml b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/views/calendar_views.xml index e94333e..f6bb98e 100644 --- a/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/views/calendar_views.xml +++ b/odoo-bringout-oca-ocb-calendar_sms/calendar_sms/views/calendar_views.xml @@ -7,14 +7,14 @@ - - calendar.event.tree.calendar_sms + calendar.event.list.calendar_sms calendar.event @@ -31,7 +31,7 @@ - + + + + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/data_response_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/data_response_model_patch.js new file mode 100644 index 0000000..53ed902 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/data_response_model_patch.js @@ -0,0 +1,11 @@ +import { DataResponse } from "@mail/core/common/data_response_model"; +import { fields } from "@mail/model/misc"; + +import { patch } from "@web/core/utils/patch"; + +patch(DataResponse.prototype, { + setup() { + super.setup(...arguments); + this.chatbot_step = fields.One("ChatbotStep"); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/expirable_storage.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/expirable_storage.js new file mode 100644 index 0000000..aa33d51 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/expirable_storage.js @@ -0,0 +1,79 @@ +import { EventBus } from "@odoo/owl"; +import { browser } from "@web/core/browser/browser"; + +const BASE_STORAGE_KEY = "EXPIRABLE_STORAGE_"; +const CLEAR_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + +function cleanupExpirableStorage() { + const now = Date.now(); + // Next line is for testing compatibility as for..in is not supported by + // the `MockStorage` class. + const keys = browser.localStorage.items?.keys() ?? Object.keys(browser.localStorage); + for (const key of keys) { + if (key.startsWith(BASE_STORAGE_KEY)) { + const item = JSON.parse(browser.localStorage.getItem(key)); + if (item.expires && item.expires < now) { + browser.localStorage.removeItem(key); + } + } + } +} + +const storageBus = new EventBus(); +const storageFnToWrapper = new Map(); +browser.addEventListener("storage", ({ key, newValue }) => { + if (key?.startsWith(BASE_STORAGE_KEY)) { + const actualKey = key.slice(BASE_STORAGE_KEY.length); + storageBus.trigger(actualKey, newValue ? JSON.parse(newValue).value : null); + } +}); + +export const expirableStorage = { + /** @param {string} key */ + getItem(key) { + cleanupExpirableStorage(); + const item = browser.localStorage.getItem(`${BASE_STORAGE_KEY}${key}`); + if (item) { + return JSON.parse(item).value; + } + return null; + }, + /** + * @param {string} key + * @param {string} value + * @param {number} ttl Number of seconds after which the item should expire. + */ + setItem(key, value, ttl) { + let expires; + if (ttl) { + expires = Date.now() + ttl * 1000; + } + browser.localStorage.setItem( + `${BASE_STORAGE_KEY}${key}`, + JSON.stringify({ value, expires }) + ); + }, + /** @param {string} key */ + removeItem(key) { + browser.localStorage.removeItem(`${BASE_STORAGE_KEY}${key}`); + }, + /** + * @param {string} key + * @param {(value: any) => void} fn + */ + onChange(key, fn) { + storageFnToWrapper.set(fn, ({ detail }) => fn(detail)); + storageBus.addEventListener(key, storageFnToWrapper.get(fn)); + }, + /** + * @param {string} key + * @param {(value: any) => void} fn + */ + offChange(key, fn) { + storageBus.removeEventListener(key, storageFnToWrapper.get(fn)); + storageFnToWrapper.delete(fn); + }, +}; + +cleanupExpirableStorage(); +setInterval(cleanupExpirableStorage, CLEAR_INTERVAL); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_channel_model.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_channel_model.js new file mode 100644 index 0000000..10048c7 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_channel_model.js @@ -0,0 +1,14 @@ +import { Record } from "@mail/core/common/record"; + +export class LivechatChannel extends Record { + static _name = "im_livechat.channel"; + static id = "id"; + + /** @type {boolean} */ + are_you_inside; + /** @type {number} */ + id; + /** @type {string} */ + name; +} +LivechatChannel.register(); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_channel_rule_model.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_channel_rule_model.js new file mode 100644 index 0000000..6f9e477 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_channel_rule_model.js @@ -0,0 +1,16 @@ +import { fields } from "@mail/model/misc"; +import { Record } from "@mail/model/record"; + +export class LivechatChannelRule extends Record { + static id = "id"; + static _name = "im_livechat.channel.rule"; + + /** @type {string} */ + action; + /** @type {number} */ + autopopup_timer; + chatbot_script_id = fields.One("chatbot.script"); + /** @type {number} */ + id; +} +LivechatChannelRule.register(); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_command_dialog.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_command_dialog.js new file mode 100644 index 0000000..b3df397 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_command_dialog.js @@ -0,0 +1,37 @@ +import { ActionPanel } from "@mail/discuss/core/common/action_panel"; + +import { Component, useState } from "@odoo/owl"; + +import { useAutofocus, useService } from "@web/core/utils/hooks"; +import { registry } from "@web/core/registry"; + +const commandRegistry = registry.category("discuss.channel_commands"); + +export class LivechatCommandDialog extends Component { + static template = "im_livechat.LivechatCommandDialog"; + static components = { ActionPanel }; + static props = ["thread", "close", "commandName", "placeholderText", "title", "icon"]; + + setup() { + this.state = useState({ inputText: "" }); + this.store = useService("mail.store"); + useAutofocus(); + } + + onKeydown(ev) { + if (ev.key === "Enter" && this.state.inputText.trim().length > 0) { + this.executeCommand(); + } + } + + executeCommand() { + const command = commandRegistry.get(this.props.commandName, false); + if (command) { + this.props.thread.executeCommand( + command, + `/${this.props.commandName} ${this.state.inputText}` + ); + this.props.close(); + } + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_command_dialog.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_command_dialog.scss new file mode 100644 index 0000000..94e91b7 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_command_dialog.scss @@ -0,0 +1,4 @@ +.o-livechat-LivechatCommandDialog-form:focus-within { + border-color: $input-focus-border-color; + color: $input-focus-color; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_command_dialog.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_command_dialog.xml new file mode 100644 index 0000000..7f0c574 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_command_dialog.xml @@ -0,0 +1,13 @@ + + + + +
+
+ +
+
+
+
+
diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_conversation_tag_model.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_conversation_tag_model.js new file mode 100644 index 0000000..41b441c --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_conversation_tag_model.js @@ -0,0 +1,14 @@ +import { Record } from "@mail/core/common/record"; + +export class LivechatConversationTag extends Record { + static _name = "im_livechat.conversation.tag"; + static id = "id"; + + /** @type {number} */ + id; + /** @type {string} */ + name; + /** @type {number} */ + color; +} +LivechatConversationTag.register(); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_expertise_model.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_expertise_model.js new file mode 100644 index 0000000..2aaaa2e --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/livechat_expertise_model.js @@ -0,0 +1,12 @@ +import { Record } from "@mail/core/common/record"; + +export class LivechatExpertise extends Record { + static id = "id"; + static _name = "im_livechat.expertise"; + + /** @type {number} */ + id; + /** @type {string} */ + name; +} +LivechatExpertise.register(); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/message_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/message_model_patch.js new file mode 100644 index 0000000..150da86 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/message_model_patch.js @@ -0,0 +1,27 @@ +import { Message } from "@mail/core/common/message_model"; +import { fields } from "@mail/core/common/record"; + +import { patch } from "@web/core/utils/patch"; + +/** @type {import("models").Message} */ +const messagePatch = { + setup() { + super.setup(...arguments); + this.chatbotStep = fields.One("ChatbotStep", { inverse: "message" }); + }, + canReplyTo(thread) { + return ( + super.canReplyTo(thread) && + (thread?.channel_type !== "livechat" || !thread.composerDisabled) + ); + }, + isTranslatable(thread) { + return ( + super.isTranslatable(thread) || + (this.store.hasMessageTranslationFeature && + thread?.channel_type === "livechat" && + this.store.self?.main_user_id?.share === false) + ); + }, +}; +patch(Message.prototype, messagePatch); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/misc.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/misc.js new file mode 100644 index 0000000..2bdcec2 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/misc.js @@ -0,0 +1,6 @@ +export function isValidEmail(val) { + // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript + const re = + /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i; + return re.test(val); +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/res_partner_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/res_partner_model_patch.js new file mode 100644 index 0000000..492e6fe --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/res_partner_model_patch.js @@ -0,0 +1,21 @@ +import { ResPartner } from "@mail/core/common/res_partner_model"; + +import { patch } from "@web/core/utils/patch"; + +/** @type {import("models").Persona} */ +const resPartnerPatch = { + setup() { + super.setup(); + /** @type {String[]} */ + this.livechat_languages = []; + /** + * @deprecated Use `user.livechat_expertise_ids` instead. + * @type {String[]} + */ + this.livechat_expertise = []; + }, + _computeDisplayName() { + return super._computeDisplayName() || this.user_livechat_username; + }, +}; +patch(ResPartner.prototype, resPartnerPatch); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/res_users_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/res_users_model_patch.js new file mode 100644 index 0000000..494151c --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/res_users_model_patch.js @@ -0,0 +1,14 @@ +import { ResUsers } from "@mail/core/common/res_users_model"; +import { fields } from "@mail/model/misc"; + +import { patch } from "@web/core/utils/patch"; + +/** @type {import("models").ResUsers} */ +const resUsersPatch = { + setup() { + super.setup(...arguments); + this.is_livechat_manager = false; + this.livechat_expertise_ids = fields.Many("im_livechat.expertise"); + }, +}; +patch(ResUsers.prototype, resUsersPatch); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/thread_actions_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/thread_actions_patch.js new file mode 100644 index 0000000..383b321 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/thread_actions_patch.js @@ -0,0 +1,58 @@ +import { ThreadAction, threadActionsRegistry } from "@mail/core/common/thread_actions"; +import { patch } from "@web/core/utils/patch"; + +patch(ThreadAction.prototype, { + _condition({ action, store, thread }) { + const visitorActions = [ + "fold-chat-window", + "close", + "restart", + "call-settings", + "meeting-chat", + ]; + if ( + thread?.channel_type === "livechat" && + store.self_partner?.main_user_id?.share !== false && + !visitorActions.includes(action.id) + ) { + return false; + } + return super._condition(...arguments); + }, +}); + +patch(threadActionsRegistry.get("invite-people"), { + condition({ thread }) { + if (thread?.channel_type === "livechat") { + return super.condition(...arguments) && !thread.livechat_end_dt; + } + return super.condition(...arguments); + }, +}); + +patch(threadActionsRegistry.get("notification-settings"), { + condition({ thread }) { + if (thread?.channel_type === "livechat") { + return super.condition(...arguments) && !thread.livechat_end_dt; + } + return super.condition(...arguments); + }, +}); + +patch(threadActionsRegistry.get("camera-call"), { + condition({ thread }) { + if (thread?.channel_type === "livechat") { + return super.condition(...arguments) && !thread.livechat_end_dt; + } + return super.condition(...arguments); + }, +}); + +patch(threadActionsRegistry.get("call"), { + condition({ thread }) { + if (thread?.channel_type === "livechat") { + return super.condition(...arguments) && !thread.livechat_end_dt; + } + return super.condition(...arguments); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/thread_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/thread_model_patch.js new file mode 100644 index 0000000..9a2eaa9 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/thread_model_patch.js @@ -0,0 +1,99 @@ +import { fields } from "@mail/core/common/record"; +import { Thread } from "@mail/core/common/thread_model"; + +import { _t } from "@web/core/l10n/translation"; +import { patch } from "@web/core/utils/patch"; +import { url } from "@web/core/utils/urls"; + +patch(Thread.prototype, { + setup() { + super.setup(); + this.livechat_end_dt = fields.Datetime(); + this.livechat_lang_id = fields.One("res.lang"); + this.livechat_operator_id = fields.One("res.partner"); + this.livechat_conversation_tag_ids = fields.Many("im_livechat.conversation.tag"); + this.chatbot = fields.One("Chatbot"); + this.livechatVisitorMember = fields.One("discuss.channel.member", { + compute() { + if (this.channel_type !== "livechat") { + return; + } + // For livechat threads, the correspondent is the first + // channel member that is not the operator. + const orderedChannelMembers = [...this.channel_member_ids].sort( + (a, b) => a.id - b.id + ); + const isFirstMemberOperator = orderedChannelMembers[0]?.partner_id?.eq( + this.livechat_operator_id + ); + const visitor = isFirstMemberOperator + ? orderedChannelMembers[1] + : orderedChannelMembers[0]; + return visitor; + }, + }); + /** @type {true|undefined} */ + this.open_chat_window = fields.Attr(undefined, { + /** @this {import("models").Thread} */ + onUpdate() { + if (this.open_chat_window) { + this.open_chat_window = undefined; + this.openChatWindow({ focus: true }); + } + }, + }); + }, + get autoOpenChatWindowOnNewMessage() { + return ( + (this.channel_type === "livechat" && + !this.store.chatHub.compact && + this.self_member_id) || + super.autoOpenChatWindowOnNewMessage + ); + }, + get showCorrespondentCountry() { + if (this.channel_type === "livechat") { + return ( + this.correspondent?.livechat_member_type === "visitor" && + Boolean(this.correspondentCountry) + ); + } + return super.showCorrespondentCountry; + }, + get typesAllowingCalls() { + return super.typesAllowingCalls.concat(["livechat"]); + }, + + get isChatChannel() { + return this.channel_type === "livechat" || super.isChatChannel; + }, + + get allowDescription() { + return this.channel_type === "livechat" || super.allowDescription; + }, + + get composerDisabled() { + return this.channel_type === "livechat" && this.livechat_end_dt; + }, + + get composerDisabledText() { + return this.channel_type === "livechat" && this.livechat_end_dt + ? _t("This livechat conversation has ended") + : ""; + }, + + get transcriptUrl() { + return url(`/im_livechat/download_transcript/${this.id}`); + }, + + /** + * @override + * @param {import("models").Persona} persona + */ + getPersonaName(persona) { + if (this.channel_type === "livechat" && persona?.user_livechat_username) { + return persona.user_livechat_username; + } + return super.getPersonaName(persona); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/thread_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/thread_patch.js new file mode 100644 index 0000000..a7c180c --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/thread_patch.js @@ -0,0 +1,59 @@ +import { Thread } from "@mail/core/common/thread"; +import { useEffect } from "@odoo/owl"; +import { _t } from "@web/core/l10n/translation"; +import { user } from "@web/core/user"; +import { patch } from "@web/core/utils/patch"; + +const { DateTime } = luxon; + +patch(Thread.prototype, { + setup() { + super.setup(...arguments); + this.IM_STATUS_DELAY = 1500; + Object.assign(this.state, { isVisitorOffline: false }); // starting online avoids flickering + useEffect( + () => { + if (!this.props.thread.livechatVisitorMember?.im_status) { + return; + } + clearTimeout(this.imStatusTimeoutId); + if (this.props.thread.livechatVisitorMember.im_status.includes("offline")) { + this.imStatusTimeoutId = setTimeout( + () => (this.state.isVisitorOffline = true), + this.IM_STATUS_DELAY + ); + } else { + this.state.isVisitorOffline = false; + } + return () => clearTimeout(this.imStatusTimeoutId); + }, + () => [this.props.thread.livechatVisitorMember?.im_status] + ); + }, + get showVisitorDisconnected() { + return ( + this.store.self.notEq(this.props.thread.livechatVisitorMember?.persona) && + !this.props.thread.livechat_end_dt && + this.props.thread.livechatVisitorMember && + this.state.isVisitorOffline + ); + }, + get disconnectedText() { + const offlineSince = this.props.thread.livechatVisitorMember.persona.offline_since; + if (!offlineSince) { + return _t("Visitor is disconnected"); + } + const userLocale = { locale: user.lang }; + if (offlineSince.hasSame(DateTime.now(), "day")) { + return _t("Visitor is disconnected since %(time)s", { + time: offlineSince.toLocaleString(DateTime.TIME_SIMPLE, userLocale), + }); + } + if (offlineSince.hasSame(DateTime.now().minus({ day: 1 }), "day")) { + return _t("Visitor is disconnected since yesterday at %(time)s", { + time: offlineSince.toLocaleString(DateTime.TIME_SIMPLE, userLocale), + }); + } + return _t("Visitor is disconnected"); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/thread_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/thread_patch.xml new file mode 100644 index 0000000..392b37c --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/thread_patch.xml @@ -0,0 +1,8 @@ + + + + +
+ + + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/transcript_sender.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/transcript_sender.js new file mode 100644 index 0000000..6d4da4f --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/transcript_sender.js @@ -0,0 +1,82 @@ +import { isValidEmail } from "@im_livechat/core/common/misc"; +import { Component, onWillUpdateProps, useEffect, useState } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; +import { useService } from "@web/core/utils/hooks"; + +/** + * @typedef {Object} Props + * @property {import("models").Thread} + * @extends {Component} + */ +export class TranscriptSender extends Component { + static template = "im_livechat.TranscriptSender"; + static props = ["thread", "disableOnSend?"]; + + STATUS = Object.freeze({ + IDLE: "idle", + SENDING: "sending", + SENT: "sent", + FAILED: "failed", + }); + + setup() { + this.isValidEmail = isValidEmail; + this.state = useState({ + email: this.props.thread.livechatVisitorMember?.persona.email, + status: this.STATUS.IDLE, + }); + this.store = useService("mail.store"); + onWillUpdateProps((newProps) => { + if (this.props.thread?.notEq(newProps.thread)) { + this.state.email = newProps.thread.livechatVisitorMember?.persona.email; + this.state.status = this.STATUS.IDLE; + } + }); + useEffect( + () => { + this.state.status = this.STATUS.IDLE; + }, + () => [this.state.email] + ); + } + + get isButtonDisabled() { + return ( + [this.STATUS.SENDING, this.STATUS.SENT].includes(this.state.status) || + !this.isValidEmail(this.state.email) + ); + } + + get isInputDisabled() { + return ( + !(this.store.self_partner?.main_user_id?.share === false) || + this.state.status === this.STATUS.SENDING || + (this.props.disableOnSend && this.state.status === this.STATUS.SENT) + ); + } + + /** @param {KeyboardEvent} ev */ + onKeydown(ev) { + if (ev.key == "Enter" && !this.isButtonDisabled) { + this.onClickSend(); + } + } + + clear() { + this.state.status = this.STATUS.IDLE; + this.state.email = ""; + } + + async onClickSend() { + this.state.status = this.STATUS.SENDING; + try { + await rpc("/im_livechat/email_livechat_transcript", { + channel_id: this.props.thread.id, + email: this.state.email, + }); + this.state.status = this.STATUS.SENT; + } catch { + this.state.status = this.STATUS.FAILED; + } + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/transcript_sender.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/transcript_sender.xml new file mode 100644 index 0000000..2caaad4 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/common/transcript_sender.xml @@ -0,0 +1,22 @@ + + + +
+
+ + +
+
+ The conversation was sent. + An error occurred. Please try again. +
+
+
+
diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/@types/models.d.ts b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/@types/models.d.ts new file mode 100644 index 0000000..488169a --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/@types/models.d.ts @@ -0,0 +1,24 @@ +declare module "models" { + export interface DiscussApp { + defaultLivechatCategory: DiscussAppCategory; + lastThread: Thread; + livechatLookingForHelpCategory: DiscussAppCategory; + livechats: Thread[]; + } + export interface DiscussAppCategory { + livechat_channel_id: LivechatChannel; + } + export interface LivechatChannel { + appCategory: DiscussAppCategory; + threads: Thread[]; + } + export interface Thread { + appAsLivechats: DiscussApp; + country_id: Country; + livechat_channel_id: LivechatChannel; + livechat_expertise_ids: LivechatExpertise[]; + livechat_status: "in_progress"|"waiting"|"need_help"|undefined; + matchesSelfExpertise: Readonly; + shadowedBySelf: number; + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/chat_window_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/chat_window_model_patch.js new file mode 100644 index 0000000..7d33816 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/chat_window_model_patch.js @@ -0,0 +1,20 @@ +import { ChatWindow } from "@mail/core/common/chat_window_model"; +import { patch } from "@web/core/utils/patch"; + +patch(ChatWindow.prototype, { + _onClose(options = {}) { + if ( + this.thread?.channel_type === "livechat" && + this.thread.livechatVisitorMember?.persona?.notEq(this.store.self) + ) { + const thread = this.thread; // save ref before delete + super._onClose(...arguments); + this.delete(); + if (options.notifyState) { + thread.leaveChannel({ force: true }); + } + } else { + super._onClose(...arguments); + } + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_app_category_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_app_category_model_patch.js new file mode 100644 index 0000000..b02b295 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_app_category_model_patch.js @@ -0,0 +1,29 @@ +import { patch } from "@web/core/utils/patch"; +import { DiscussAppCategory } from "@mail/discuss/core/public_web/discuss_app_category_model"; +import { fields } from "@mail/core/common/record"; +import { compareDatetime } from "@mail/utils/common/misc"; + +patch(DiscussAppCategory.prototype, { + setup() { + super.setup(...arguments); + this.livechat_channel_id = fields.One("im_livechat.channel", { + inverse: "appCategory", + onDelete() { + this.delete(); + }, + }); + }, + /** + * @param {import("models").Thread} t1 + * @param {import("models").Thread} t2 + */ + sortThreads(t1, t2) { + if (this.eq(this.app?.livechatLookingForHelpCategory)) { + return t1.id - t2.id; + } + if (this.livechat_channel_id || this.eq(this.app?.defaultLivechatCategory)) { + return compareDatetime(t2.lastInterestDt, t1.lastInterestDt) || t2.id - t1.id; + } + return super.sortThreads(t1, t2); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_app_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_app_model_patch.js new file mode 100644 index 0000000..dfa2c65 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_app_model_patch.js @@ -0,0 +1,123 @@ +import { fields } from "@mail/core/common/record"; +import { DiscussApp } from "@mail/core/public_web/discuss_app_model"; +import { effectWithDebouncedCleanup } from "@mail/utils/common/misc"; + +import { browser } from "@web/core/browser/browser"; +import { _t } from "@web/core/l10n/translation"; +import { patch } from "@web/core/utils/patch"; + +// Looking for help subscription is triggered when the sidebar category is +// opened, and when the discuss app is active. To avoid unsubscribing right away +// when the user closes the sidebar or switches to another app, wait for 5 +// minutes before unsubscribing. +export const LFH_UNSUBSCRIBE_DELAY = 5 * 60 * 1000; +export const LIVECHAT_INFO_DEFAULT_OPEN_LS = "im_livechat.isInfoPanelOpenByDefault"; + +const discussAppStaticPatch = { + new() { + /** @type {import("models").DiscussApp} */ + const app = super.new(...arguments); + effectWithDebouncedCleanup({ + delay: LFH_UNSUBSCRIBE_DELAY, + dependencies: (app) => ({ + busService: app.store.env.services.bus_service, + category: app.livechatLookingForHelpCategory, + store: app.store, + }), + effect({ busService, category, store }) { + busService.addChannel("im_livechat.looking_for_help"); + store.fetchStoreData("/im_livechat/looking_for_help"); + return () => { + busService.deleteChannel("im_livechat.looking_for_help"); + if (!category.exists()) { + return; + } + category.threads + .filter((thread) => !thread.self_member_id && !thread.isLocallyPinned) + .forEach((thread) => thread.delete()); + }; + }, + predicate: (app) => + Boolean( + app.exists() && + app.livechatLookingForHelpCategory?.open && + !app.livechatLookingForHelpCategory.hidden && + app.isActive + ), + reactiveTargets: [app], + }); + return app; + }, +}; +patch(DiscussApp, discussAppStaticPatch); + +patch(DiscussApp.prototype, { + setup(env) { + super.setup(...arguments); + this.defaultLivechatCategory = fields.One("DiscussAppCategory", { + compute() { + return { + extraClass: "o-mail-DiscussSidebarCategory-livechat", + hideWhenEmpty: true, + icon: "fa fa-commenting-o", + id: `im_livechat.category_default`, + name: _t("Livechat"), + sequence: 21, + }; + }, + eager: true, + }); + this.livechatLookingForHelpCategory = fields.One("DiscussAppCategory", { + compute() { + if (!this.store.has_access_livechat) { + return null; + } + return { + extraClass: "o-mail-DiscussSidebarCategory-livechatNeedHelp", + icon: "fa fa-exclamation-circle", + id: `im_livechat.category_need_help`, + name: _t("Looking for help"), + sequence: 15, + }; + }, + eager: true, + }); + this.lastThread = fields.One("Thread"); + this.livechats = fields.Many("Thread", { inverse: "appAsLivechats" }); + this._recomputeIsLivechatInfoPanelOpenedByDefault = 0; + this.isLivechatInfoPanelOpenByDefault = fields.Attr(true, { + compute() { + void this._recomputeIsLivechatInfoPanelOpenedByDefault; + return browser.localStorage.getItem(LIVECHAT_INFO_DEFAULT_OPEN_LS) !== "false"; + }, + }); + }, + + shouldDisableMemberPanelAutoOpenFromClose(nextActiveAction) { + if (nextActiveAction?.id === "livechat-info") { + return false; + } + return super.shouldDisableMemberPanelAutoOpenFromClose(...arguments); + }, + + _threadOnUpdate() { + if ( + this.lastThread?.notEq(this.thread) && + (this.lastThread.livechat_status === "need_help" || this.lastThread.unpinOnThreadSwitch) + ) { + this.lastThread.isLocallyPinned = false; + } + if (this.thread?.livechat_status === "need_help" && !this.thread.self_member_id) { + this.thread.isLocallyPinned = true; + } + this.lastThread = this.thread; + super._threadOnUpdate(); + }, + + onStorage(ev) { + super.onStorage(ev); + if (ev.key === LIVECHAT_INFO_DEFAULT_OPEN_LS) { + this._recomputeIsLivechatInfoPanelOpenedByDefault++; + } + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_content_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_content_patch.xml new file mode 100644 index 0000000..bc7c904 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_content_patch.xml @@ -0,0 +1,9 @@ + + + + + + $0 + + + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_sidebar_categories_patch.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_sidebar_categories_patch.scss new file mode 100644 index 0000000..11438ed --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_sidebar_categories_patch.scss @@ -0,0 +1,4 @@ +.o-mail-DiscussSidebarCategory-livechatJoinedIndicatorCompact { + right: 11px; + bottom: 1px; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_sidebar_categories_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_sidebar_categories_patch.xml new file mode 100644 index 0000000..b8a20f5 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_sidebar_categories_patch.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + joined + + + + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_sidebar_channel_actions_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_sidebar_channel_actions_patch.xml new file mode 100644 index 0000000..183fed8 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/discuss_sidebar_channel_actions_patch.xml @@ -0,0 +1,12 @@ + + + + +
+ $0 + +
+ $0 +
+
+
diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/livechat_channel_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/livechat_channel_model_patch.js new file mode 100644 index 0000000..4a99714 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/livechat_channel_model_patch.js @@ -0,0 +1,26 @@ +import { LivechatChannel } from "@im_livechat/core/common/livechat_channel_model"; +import { fields } from "@mail/core/common/record"; + +import { patch } from "@web/core/utils/patch"; + +const livechatChannelPatch = { + setup() { + super.setup(...arguments); + this.appCategory = fields.One("DiscussAppCategory", { + compute() { + return { + extraClass: "o-mail-DiscussSidebarCategory-livechat", + hideWhenEmpty: !this.are_you_inside, + id: `im_livechat.category_${this.id}`, + icon: "fa fa-commenting-o", + name: this.name, + sequence: 22, + }; + }, + eager: true, + inverse: "livechat_channel_id", + }); + this.threads = fields.Many("Thread", { inverse: "livechat_channel_id" }); + }, +}; +patch(LivechatChannel.prototype, livechatChannelPatch); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/messaging_menu_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/messaging_menu_patch.js new file mode 100644 index 0000000..6ab72bf --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/messaging_menu_patch.js @@ -0,0 +1,31 @@ +import { MessagingMenu } from "@mail/core/public_web/messaging_menu"; + +import { _t } from "@web/core/l10n/translation"; +import { patch } from "@web/core/utils/patch"; + +patch(MessagingMenu.prototype, { + /** + * @override + */ + get _tabs() { + const items = super._tabs; + const hasLivechats = Object.values(this.store.Thread.records).some( + ({ channel_type }) => channel_type === "livechat" + ); + if (hasLivechats) { + items.push({ + counter: this.store.discuss.livechats.reduce( + (acc, channel) => + channel.self_member_id?.message_unread_counter > 0 ? acc + 1 : acc, + 0 + ), + id: "livechat", + icon: "fa fa-commenting-o", + activeIcon: "fa fa-commenting", + label: _t("Live Chats"), + sequence: 60, + }); + } + return items; + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/thread_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/thread_model_patch.js new file mode 100644 index 0000000..7b490f7 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/public_web/thread_model_patch.js @@ -0,0 +1,157 @@ +import { fields } from "@mail/core/common/record"; +import { Thread } from "@mail/core/common/thread_model"; +import { _t } from "@web/core/l10n/translation"; + +import { patch } from "@web/core/utils/patch"; + +patch(Thread.prototype, { + setup() { + super.setup(...arguments); + this.appAsLivechats = fields.One("DiscussApp", { + compute() { + return this.channel_type === "livechat" ? this.store.discuss : null; + }, + }); + this.country_id = fields.One("res.country"); + this.livechat_channel_id = fields.One("im_livechat.channel", { inverse: "threads" }); + this.livechat_expertise_ids = fields.Many("im_livechat.expertise"); + /** @type {"in_progress"|"waiting"|"need_help"|undefined} */ + this.livechat_status = fields.Attr(undefined, { + onUpdate() { + if (this.livechat_status === "need_help") { + this.wasLookingForHelp = true; + this.unpinOnThreadSwitch = false; + return; + } + if (this.wasLookingForHelp) { + this.wasLookingForHelp = false; + // Still the active thread; keep it pinned after leaving "need help" status. + // The agent may interact with the thread, keeping it pinned, or it will be + // unpinned on the next thread switch to avoid bloating the sidebar. + this.unpinOnThreadSwitch = this.eq(this.store.discuss?.thread); + } + }, + }); + this.shadowedBySelf = 0; + }, + get canLeave() { + const lookingForHelpCategory = this.store.discuss.livechatLookingForHelpCategory; + return ( + super.canLeave && + (!lookingForHelpCategory || + lookingForHelpCategory.notEq(this.discussAppCategory) || + this.self_member_id) + ); + }, + _computeDiscussAppCategory() { + if (this.channel_type !== "livechat") { + return super._computeDiscussAppCategory(); + } + if ( + this.livechat_status === "need_help" && + this.store.discuss.livechatLookingForHelpCategory + ) { + return this.store.discuss.livechatLookingForHelpCategory; + } + return ( + this.livechat_channel_id?.appCategory ?? this.appAsLivechats?.defaultLivechatCategory + ); + }, + get hasMemberList() { + return this.channel_type === "livechat" || super.hasMemberList; + }, + get allowedToLeaveChannelTypes() { + return [...super.allowedToLeaveChannelTypes, "livechat"]; + }, + get correspondents() { + return super.correspondents.filter( + (correspondent) => correspondent.livechat_member_type !== "bot" + ); + }, + + computeCorrespondent() { + const correspondent = super.computeCorrespondent(); + if (this.channel_type === "livechat" && !correspondent) { + return this.livechatVisitorMember; + } + return correspondent; + }, + + _computeDisplayInSidebar() { + return this.livechat_status === "need_help" || super._computeDisplayInSidebar(); + }, + + get displayName() { + if ( + this.channel_type !== "livechat" || + !this.correspondent || + this.self_member_id?.custom_channel_name + ) { + return super.displayName; + } + if (!this.correspondent.persona.is_public && this.correspondent.persona.country) { + return `${this.correspondent.name} (${this.correspondent.persona.country.name})`; + } + if (this.country_id) { + return `${this.correspondent.name} (${this.country_id.name})`; + } + return this.correspondent.name; + }, + + get avatarUrl() { + if (this.channel_type === "livechat" && this.correspondent) { + return this.correspondent.avatarUrl; + } + return super.avatarUrl; + }, + + get inChathubOnNewMessage() { + if (this.channel_type === "livechat") { + return Boolean(this.self_member_id); + } + return super.inChathubOnNewMessage; + }, + get notifyWhenOutOfFocus() { + if (this.channel_type === "livechat") { + return ( + this.self_member_id || this.shadowedBySelf || this.eq(this.store.discuss?.thread) + ); + } + return super.notifyWhenOutOfFocus; + }, + get matchesSelfExpertise() { + return ( + this.store.self_partner?.main_user_id && + this.livechat_expertise_ids.some((expertise) => + expertise.in(this.store.self_partner.main_user_id.livechat_expertise_ids) + ) + ); + }, + /** + * @override + * @param {boolean} pushState + */ + setAsDiscussThread(pushState) { + super.setAsDiscussThread(pushState); + if (this.store.env.services.ui.isSmall && this.channel_type === "livechat") { + this.store.discuss.activeTab = "livechat"; + } + }, + get shouldSubscribeToBusChannel() { + return super.shouldSubscribeToBusChannel || Boolean(this.shadowedBySelf); + }, + async leaveChannel({ force = false } = {}) { + if ( + this.channel_type === "livechat" && + this.channel_member_ids.length <= 2 && + this.self_member_id && + !this.livechat_end_dt && + !force + ) { + await this.askLeaveConfirmation( + _t("Leaving will end the live chat. Do you want to proceed?") + ); + } + super.leaveChannel(...arguments); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/@types/models.d.ts b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/@types/models.d.ts new file mode 100644 index 0000000..8143058 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/@types/models.d.ts @@ -0,0 +1,22 @@ +declare module "models" { + export interface LivechatChannel { + join: (param0: { notify: boolean }) => Promise; + joinTitle: Readonly; + leave: (param0: { notify: boolean }) => Promise; + leaveTitle: Readonly; + } + export interface Store { + goToOldestUnreadLivechatThread: () => boolean; + has_access_livechat: boolean; + livechatChannels: ReturnType; + livechatStatusButtons: Readonly; + } + export interface Thread { + hasFetchedLivechatSessionData: boolean; + livechat_note: ReturnType|string; + livechat_outcome: "no_answer"|"no_agent"|"no_failure"|"escalated"|undefined; + livechatNoteText: string|undefined; + livechatStatusLabel: Readonly; + updateLivechatStatus: (status: "in_progress"|"waiting"|"need_help") => void; + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_commands_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_commands_patch.js new file mode 100644 index 0000000..59d9a27 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_commands_patch.js @@ -0,0 +1,9 @@ +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; + +registry.category("discuss.channel_commands").add("history", { + condition: ({ store }) => store.has_access_livechat, + channel_types: ["livechat"], + help: _t("See 15 last visited pages"), + methodName: "execute_command_history", +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_invitation_patch.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_invitation_patch.scss new file mode 100644 index 0000000..bdad520 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_invitation_patch.scss @@ -0,0 +1,3 @@ +.o-discuss-ChannelInvitation-inCallTextColor { + color: lighten($o-action, 5%); +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_invitation_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_invitation_patch.xml new file mode 100644 index 0000000..615ef19 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_invitation_patch.xml @@ -0,0 +1,28 @@ + + + + + + + + in a call + + + +
+ + + + + + + + + + + + +
+
+
+
diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_member_list_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_member_list_patch.js new file mode 100644 index 0000000..2ec1632 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_member_list_patch.js @@ -0,0 +1,12 @@ +import { ChannelMemberList } from "@mail/discuss/core/common/channel_member_list"; +import { patch } from "@web/core/utils/patch"; + +patch(ChannelMemberList.prototype, { + canOpenChatWith(member) { + return ( + super.canOpenChatWith(member) && + !member.partner_id?.is_public && + member.livechat_member_type !== "bot" + ); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_member_list_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_member_list_patch.xml new file mode 100644 index 0000000..9b312a8 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/channel_member_list_patch.xml @@ -0,0 +1,17 @@ + + + + +
+ + + + + + + + +
+
+
+
diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/chat_bubble_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/chat_bubble_patch.xml new file mode 100644 index 0000000..9287598 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/chat_bubble_patch.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/chat_window_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/chat_window_patch.xml new file mode 100644 index 0000000..395af32 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/chat_window_patch.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/command_palette.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/command_palette.xml new file mode 100644 index 0000000..6c17b4b --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/command_palette.xml @@ -0,0 +1,11 @@ + + + +
+ + + + +
+
+
diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/composer_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/composer_patch.js new file mode 100644 index 0000000..ff43834 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/composer_patch.js @@ -0,0 +1,37 @@ +import { Composer } from "@mail/core/common/composer"; +import { _t } from "@web/core/l10n/translation"; + +import { patch } from "@web/core/utils/patch"; + +patch(Composer.prototype, { + onKeydown(ev) { + super.onKeydown(ev); + if ( + ev.key === "Tab" && + this.thread?.channel_type === "livechat" && + !this.props.composer.composerText + ) { + const threadChanged = this.store.goToOldestUnreadLivechatThread(); + if (threadChanged) { + // prevent chat window from switching to the next thread: as + // we want to go to the oldest unread thread, not the next + // one. + ev.stopPropagation(); + } + } + }, + get placeholder() { + if (this.displayNextLivechatHint() && this.props.composer.isFocused) { + return _t("Tab to next livechat"); + } + return super.placeholder; + }, + displayNextLivechatHint() { + return ( + this.thread?.channel_type === "livechat" && + this.store.discuss.livechats.some( + (thread) => thread.notEq(this.thread) && thread.isUnread + ) + ); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_client_action_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_client_action_patch.js new file mode 100644 index 0000000..903d45f --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_client_action_patch.js @@ -0,0 +1,13 @@ +import { DiscussClientAction } from "@mail/core/public_web/discuss_client_action"; + +import { patch } from "@web/core/utils/patch"; + +patch(DiscussClientAction.prototype, { + async restoreDiscussThread() { + if (this.store.has_access_livechat) { + this.store.livechatChannels.fetch(); + this.store.livechatSelfExpertises.fetch(); + } + return super.restoreDiscussThread(...arguments); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_content_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_content_patch.js new file mode 100644 index 0000000..915beba --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_content_patch.js @@ -0,0 +1,14 @@ +import { DiscussContent } from "@mail/core/public_web/discuss_content"; + +import { patch } from "@web/core/utils/patch"; + +patch(DiscussContent.prototype, { + actionPanelAutoOpenFn() { + const livechatInfoAction = this.threadActions.actions.find((a) => a.id === "livechat-info"); + if (livechatInfoAction && this.store.discuss.isLivechatInfoPanelOpenByDefault) { + livechatInfoAction.open(); + } else { + super.actionPanelAutoOpenFn(); + } + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_sidebar_category_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_sidebar_category_patch.js new file mode 100644 index 0000000..4efc290 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_sidebar_category_patch.js @@ -0,0 +1,59 @@ +import { + DiscussSidebarCategory, + DiscussSidebarChannel, +} from "@mail/discuss/core/public_web/discuss_sidebar_categories"; +import { patch } from "@web/core/utils/patch"; + +/** @type {import("@mail/discuss/core/public_web/discuss_sidebar_categories").DiscussSidebarCategory} */ +const DiscussSidebarCategoryPatch = { + get actions() { + const actions = super.actions; + if ( + this.store.has_access_livechat && + this.category.livechat_channel_id && + this.category.open + ) { + actions.push({ + onSelect: () => { + if (this.category.livechat_channel_id.are_you_inside) { + this.category.livechat_channel_id.leave({ notify: false }); + } else { + this.category.livechat_channel_id.join({ notify: false }); + } + }, + label: this.category.livechat_channel_id.are_you_inside + ? this.category.livechat_channel_id.leaveTitle + : this.category.livechat_channel_id.joinTitle, + icon: this.category.livechat_channel_id.are_you_inside + ? "fa fa-sign-out fa-rotate-180 text-danger" + : "fa fa-sign-in text-success", + }); + } + return actions; + }, +}; + +/** @type {import("@mail/discuss/core/public_web/discuss_sidebar_categories").DiscussSidebarChannel} */ +const DiscussSidebarChannelPatch = { + get attClassContainer() { + return { + ...super.attClassContainer, + "bg-100": this.thread.livechat_end_dt, + }; + }, + get itemNameAttClass() { + return { + ...super.itemNameAttClass, + "fst-italic text-muted fw-normal": this.thread.livechat_end_dt, + }; + }, + get threadAvatarAttClass() { + return { + ...super.threadAvatarAttClass, + "o-opacity-65": this.thread.livechat_end_dt, + }; + }, +}; + +patch(DiscussSidebarCategory.prototype, DiscussSidebarCategoryPatch); +patch(DiscussSidebarChannel.prototype, DiscussSidebarChannelPatch); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_sidebar_category_patch.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_sidebar_category_patch.scss new file mode 100644 index 0000000..e67b02a --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_sidebar_category_patch.scss @@ -0,0 +1,38 @@ +.o-livechat-LivechatStatusLabel-Sidebar { + --bg-opacity: .1; + --border-color: transparent; + + &.bg-info { + --background-color: RGBA(#{to-rgb($purple)}, var(--bg-opacity)); + } + + .o-mail-DiscussSidebarChannel.o-active &, + .o-mail-DiscussSidebarChannel &:hover, + .o-mail-DiscussSidebarChannel &:focus-visible { + --bg-opacity: 1; + } + + .o-mail-DiscussSidebarChannel.o-active & { + --border-opacity: .75; + } + + .o-mail-DiscussSidebarChannel:hover &.bg-warning { + --background-color: #{mix($o-webclient-background-color, $warning, 80%)}; + } + .o-mail-DiscussSidebarChannel:hover &.bg-info { + --background-color: #{mix($o-webclient-background-color, $purple, 80%)}; + } + + .o-mail-DiscussSidebarChannel.o-active &.bg-warning { + --background-color: #{mix($o-webclient-background-color, $warning, 70%)}; + --border-color: #{rgba($warning, 0.75)}; + } + .o-mail-DiscussSidebarChannel.o-active &.bg-info { + --background-color: #{mix($o-webclient-background-color, $purple, 90%)}; + --border-color: #{rgba($indigo, 0.75)}; + } +} + +.o-livechat-LivechatStatusLabel-icon.o-inDiscussSidebar { + background-color: $white; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_sidebar_category_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_sidebar_category_patch.xml new file mode 100644 index 0000000..d064da0 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_sidebar_category_patch.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_sidebar_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_sidebar_patch.xml new file mode 100644 index 0000000..6bb5210 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/discuss_sidebar_patch.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/expertise_tags_autocomplete.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/expertise_tags_autocomplete.js new file mode 100644 index 0000000..74ec22f --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/expertise_tags_autocomplete.js @@ -0,0 +1,85 @@ +import { Component } from "@odoo/owl"; + +import { _t } from "@web/core/l10n/translation"; +import { rpc } from "@web/core/network/rpc"; +import { x2ManyCommands } from "@web/core/orm_service"; +import { useTagNavigation } from "@web/core/record_selectors/tag_navigation_hook"; +import { TagsList } from "@web/core/tags_list/tags_list"; +import { useService } from "@web/core/utils/hooks"; +import { Many2XAutocomplete } from "@web/views/fields/relational_utils"; + +/** + * @typedef {Object} Props + * @property {import("models").Thread} channel + * @extends {Component} + */ +export class ExpertiseTagsAutocomplete extends Component { + static template = "im_livechat.ExpertiseTagsAutocomplete"; + static props = ["channel", "disabled?"]; + static components = { TagsList, Many2XAutocomplete }; + + setup() { + super.setup(...arguments); + this.orm = useService("orm"); + this.store = useService("mail.store"); + useTagNavigation("root", { + delete: (index) => { + const expertise = this.props.channel.livechat_expertise_ids[index]; + if (expertise) { + this.writeExpertises([x2ManyCommands.unlink(expertise.id)]); + } + }, + }); + } + + /** @param {(ReturnType|ReturnType)[]} ormCommands */ + writeExpertises(ormCommands) { + rpc("/im_livechat/conversation/write_expertises", { + channel_id: this.props.channel.id, + orm_commands: ormCommands, + }); + } + + /** @param {string} name */ + createAndLinkExpertise(name) { + if ( + this.props.channel.livechat_expertise_ids.some( + (expertise) => expertise.name === name.trim() + ) + ) { + return; + } + rpc("/im_livechat/conversation/create_and_link_expertise", { + channel_id: this.props.channel.id, + expertise_name: name, + }); + } + + /** @param {{id: number, display_name: string}} expertises */ + addExpertises(expertises) { + const toAdd = expertises.filter((expertise) => !this.isSelected(expertise.id)); + if (!toAdd.length) { + return; + } + this.writeExpertises(toAdd.map((expertise) => x2ManyCommands.link(expertise.id))); + } + + get placeholder() { + if (this.props.channel.livechat_expertise_ids.length === 0) { + return _t("Add expertise"); + } + return ""; + } + + get tags() { + return this.props.channel.livechat_expertise_ids.map((expertise) => ({ + id: expertise.id, + onDelete: () => this.writeExpertises([x2ManyCommands.unlink(expertise.id)]), + text: expertise.name, + })); + } + + isSelected(expertiseId) { + return this.props.channel.livechat_expertise_ids.some((e) => e.id === expertiseId); + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/expertise_tags_autocomplete.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/expertise_tags_autocomplete.xml new file mode 100644 index 0000000..f76b350 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/expertise_tags_autocomplete.xml @@ -0,0 +1,30 @@ + + + +
+ +
+ + + + + + + +
+
+
+
diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_command_provider.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_command_provider.js new file mode 100644 index 0000000..e835b6d --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_command_provider.js @@ -0,0 +1,44 @@ +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; + +class LivechatChannelCommand extends Component { + static template = "im_livechat.LivechatChannelCommand"; + static props = { + executeCommand: Function, + iconClass: String, + name: String, + searchValue: String, + slots: Object, + }; +} + +registry.category("command_provider").add("im_livechat.channel_join_leave", { + /** + * @param {import("@web/env").OdooEnv} env + */ + async provide(env) { + const store = env.services["mail.store"]; + if (!store?.has_access_livechat) { + return []; + } + await store.livechatChannels.fetch(); + const activeChannels = new Set( + Object.values(store["im_livechat.channel"].records) + .filter((c) => c.threads.length > 0) + .map((c) => c.id) + ); + // Show live chat channels with ongoing conversations first + return Object.values(store["im_livechat.channel"].records) + .sort((c) => (activeChannels.has(c.id) ? -1 : 1)) + .map((c) => ({ + action: c.are_you_inside ? c.leave.bind(c) : c.join.bind(c), + Component: LivechatChannelCommand, + name: c.are_you_inside ? c.leaveTitle : c.joinTitle, + props: { + iconClass: c.are_you_inside + ? "fa fa-sign-out text-danger" + : "fa fa-sign-in text-success", + }, + })); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_info_list.dark.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_info_list.dark.scss new file mode 100644 index 0000000..4360f00 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_info_list.dark.scss @@ -0,0 +1,4 @@ +.o-livechat-LivechatStatusLabel.o-help { + --livechat-LivechatStatusLabel-helpColor: #{lighten($purple, 15%)}; + text-shadow: 0px 0px 3px black +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_info_list.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_info_list.js new file mode 100644 index 0000000..ce20f48 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_info_list.js @@ -0,0 +1,107 @@ +import { TranscriptSender } from "@im_livechat/core/common/transcript_sender"; +import { ExpertiseTagsAutocomplete } from "@im_livechat/core/web/expertise_tags_autocomplete"; +import { ConversationTagEdit } from "@im_livechat/core/web/livechat_conversation_tag_edit"; + +import { ActionPanel } from "@mail/discuss/core/common/action_panel"; +import { prettifyMessageContent } from "@mail/utils/common/format"; + +import { Component, useEffect, useRef, useSubEnv } from "@odoo/owl"; + +import { startUrl } from "@web/core/browser/router"; +import { rpc } from "@web/core/network/rpc"; +import { usePopover } from "@web/core/popover/popover_hook"; +import { useService } from "@web/core/utils/hooks"; +import { url } from "@web/core/utils/urls"; +import { TagsList } from "@web/core/tags_list/tags_list"; + +export class LivechatChannelInfoList extends Component { + static components = { ActionPanel, TagsList, ExpertiseTagsAutocomplete, TranscriptSender }; + static template = "im_livechat.LivechatChannelInfoList"; + static props = ["thread"]; + + setup() { + super.setup(); + this.actionService = useService("action"); + this.store = useService("mail.store"); + this.ui = useService("ui"); + this.tagEditPopover = usePopover(ConversationTagEdit, { + closeOnClickAway: true, + position: "left", + useBottomSheet: this.ui.isSmall, + }); + this.tagsContainer = useRef("tagsContainer"); + useSubEnv({ inLivechatInfoPanel: true }); + useEffect( + () => { + if (this.props.thread.hasFetchedLivechatSessionData) { + return; + } + this.store.fetchStoreData("/im_livechat/session/data", { + channel_id: this.props.thread.id, + }); + this.props.thread.hasFetchedLivechatSessionData = true; + }, + () => [this.props.thread.id, this.props.thread.hasFetchedLivechatSessionData] + ); + } + + get conversationTags() { + return this.props.thread.livechat_conversation_tag_ids.map((tag) => ({ + id: tag.id, + text: tag.name, + colorIndex: tag.color, + className: "me-1 mb-1", + })); + } + + get expectAnswerSteps() { + return this.props.thread.messages + .filter((m) => m.chatbotStep?.expectAnswer && m.chatbotStep.answer) + .map((m) => m.chatbotStep); + } + + /** @deprecated */ + get expertiseTags() { + return this.props.thread.livechat_expertise_ids.map((expertise) => ({ + id: expertise.id, + text: expertise.name, + colorIndex: 0, + className: "me-1 mb-1", + })); + } + + onBlurNote() { + prettifyMessageContent(this.props.thread.livechatNoteText).then((note) => { + rpc("/im_livechat/session/update_note", { channel_id: this.props.thread.id, note }); + }); + } + + onClickEditTags(ev) { + this.tagEditPopover.open(this.tagsContainer.el, { + thread: this.props.thread, + }); + } + + openVisitorProfile() { + if (this.ui.isSmall) { + this.store.ChatWindow.get({ thread: this.props.thread })?.fold(); + } else { + this.props.thread.openChatWindow({ focus: true }); + } + this.actionService.doAction({ + type: "ir.actions.act_window", + res_model: "res.partner", + res_id: this.props.thread.livechatVisitorMember.partner_id.id, + views: [[false, "form"]], + target: "current", + }); + } + + get visitorProfileURL() { + const visitorMember = this.props.thread?.livechatVisitorMember; + if (visitorMember?.partner_id) { + return url(`/${startUrl()}/res.partner/${visitorMember.partner_id.id}`); + } + return undefined; + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_info_list.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_info_list.scss new file mode 100644 index 0000000..2e08811 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_info_list.scss @@ -0,0 +1,101 @@ +.o-livechat-LivechatStatusLabel.o-help { + color: var(--livechat-LivechatStatusLabel-helpColor, #{$purple}); +} + +.o-livechat-LivechatStatusSelection { + --list-group-border-radius: var(--border-radius); + + @keyframes shakeX { + from, to {transform: translate3d(0, 0, 0);} + 20%, 60% {transform: translate3d(-.15em, 0, 0);} + 40%, 80% {transform: translate3d(.15em, 0, 0);} + } + + button:where(:not(.active)) .o-livechat-LivechatStatusLabel { + visibility: hidden; + animation: none; + } + + .o-livechat-LivechatStatusLabel { + &.o-waiting { + transform: rotate(180deg); + transition: transform 1s; + } + &.o-help { + animation: none; + } + } + + button.active .o-livechat-LivechatStatusLabel { + &.o-waiting { + transform: none; + } + &.o-help { + animation: shakeX .5s; + } + } + + button.active.o-inProgress { + --list-group-active-bg: #{rgba($success, .2)}; + --list-group-active-color: #{$success}; + --list-group-active-border-color: #{$success}; + + .o-livechat-LivechatStatusSelection-Label { + color: var(--list-group-color); + } + &.o-livechat-LivechatStatusLabel-icon { + color: $success; + } + + .form-check-input { + background-color: $success; + border-color: $success; + } + } + + button.active.o-need-help { + --list-group-active-bg: #{rgba($purple, .1)}; + --list-group-active-color: var(--body-color); + --list-group-active-border-color: var(--purple); + + + &.o-livechat-LivechatStatusLabel-icon { + color: var(--purple); + } + + .form-check-input { + background-color: var(--purple); + border-color: var(--purple); + } + } + + button.active.o-waiting { + --list-group-active-color: var(--body-color); + --list-group-active-border-color: map-get($o-theme-text-colors, 'warning'); + + .form-check-input { + background-color: map-get($o-theme-text-colors, 'warning'); + border-color: map-get($o-theme-text-colors, 'warning'); + } + } + + // Dropdown + .o_popover & { + --list-group-border-radius: 0; + + .list-group-item { + border-width: 0; + margin: 0; + } + } +} + +.o-livechat-LivechatStatusLabel.position-absolute { + background-color: rgba($white, var(--bg-opacity, 1)); + border-radius: 50%; +} + +.o_country_flag { + width:24px; + height: 16px; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_info_list.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_info_list.xml new file mode 100644 index 0000000..730b216 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_info_list.xml @@ -0,0 +1,84 @@ + + + + + + View Contact + +
+
Status
+ + + +
+
+
Outcome
+
+ Never Answered + No one Available + Success + Escalated +
+
+
+
Notes
+ +
+
+
+ Tags + +
+
+ + +
+
+
+
Chatbot answers
+ +
+ + +
+
+
+
+
Expertise
+ +
+
+
Country & Language
+
+ + +
+
+ +
+
Send conversation
+ + Download +
+
+
+ + +
+
+ + + + + + +
+
+
diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_model_patch.js new file mode 100644 index 0000000..1c058cf --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_channel_model_patch.js @@ -0,0 +1,40 @@ +import { LivechatChannel } from "@im_livechat/core/common/livechat_channel_model"; + +import { useSequential } from "@mail/utils/common/hooks"; + +import { _t } from "@web/core/l10n/translation"; +import { patch } from "@web/core/utils/patch"; + +const sequential = useSequential(); + +const livechatChannelPatch = { + async join({ notify = true } = {}) { + this.are_you_inside = true; + if (notify) { + this.store.env.services.notification.add(_t("You joined %s.", this.name), { + type: "info", + }); + } + await sequential(() => + this.store.env.services.orm.call("im_livechat.channel", "action_join", [this.id]) + ); + }, + get joinTitle() { + return _t("Join %s", this.name); + }, + async leave({ notify = true } = {}) { + this.are_you_inside = false; + if (notify) { + this.store.env.services.notification.add(_t("You left %s.", this.name), { + type: "info", + }); + } + await sequential(() => + this.store.env.services.orm.call("im_livechat.channel", "action_quit", [this.id]) + ); + }, + get leaveTitle() { + return _t("Leave %s", this.name); + }, +}; +patch(LivechatChannel.prototype, livechatChannelPatch); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_conversation_tag_edit.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_conversation_tag_edit.js new file mode 100644 index 0000000..ac77572 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_conversation_tag_edit.js @@ -0,0 +1,135 @@ +import { Component, onWillStart, useEffect, useState, xml } from "@odoo/owl"; + +import { useAutofocus, useService } from "@web/core/utils/hooks"; +import { useSequential } from "@mail/utils/common/hooks"; +import { highlightText } from "@web/core/utils/html"; +import { useDebounced } from "@web/core/utils/timing"; +import { escapeRegExp } from "@web/core/utils/strings"; +import { rpc } from "@web/core/network/rpc"; +import { NavigableList } from "@mail/core/common/navigable_list"; + +export class ConversationTagEdit extends Component { + static components = { NavigableList }; + static props = ["thread", "autofocus?", "close?"]; + static template = "im_livechat.ConversationTagEdit"; + + setup() { + super.setup(); + this.orm = useService("orm"); + this.store = useService("mail.store"); + this.inputRef = useAutofocus(); + this.sequential = useSequential(); + this.state = useState({ + selectableTags: [], + searchStr: "", + }); + this.debouncedFetchConversationTags = useDebounced( + this.fetchConversationTags.bind(this), + 250 + ); + onWillStart(() => { + this.fetchConversationTags(); + }); + useEffect( + () => { + this.debouncedFetchConversationTags(); + }, + () => [this.state.searchStr] + ); + } + + get allSelectableTagNames() { + return this.state.selectableTags.map((tag) => tag.name); + } + + get allSelectedTagNames() { + return this.props.thread.livechat_conversation_tag_ids.map((tag) => tag.name); + } + + get remainingSelectableTags() { + return this.state.selectableTags.filter( + (tag) => !tag.in(this.props.thread.livechat_conversation_tag_ids) + ); + } + + get navigableListProps() { + return { + onSelect: (ev, option) => { + this.toggleSelectedTag(option.tag); + this.state.searchStr = ""; + }, + optionTemplate: xml``, + options: this.remainingSelectableTags.map((tag) => ({ + tag, + label: highlightText(this.state.searchStr.trim(), tag.name, "text-primary"), + buttonClass: "btn", + })), + }; + } + + async toggleSelectedTag(tag) { + await rpc("/im_livechat/conversation/update_tags", { + channel_id: this.props.thread.id, + tag_ids: [tag.id], + method: this.props.thread.livechat_conversation_tag_ids.includes(tag) + ? "DELETE" + : "ADD", + }); + } + + async fetchConversationTags() { + const results = await this.sequential(() => + this.orm.searchRead( + "im_livechat.conversation.tag", + [["name", "ilike", this.state.searchStr]], + ["id", "name"], + { limit: 15 } + ) + ); + if (!results) { + return; + } + const result = this.store["im_livechat.conversation.tag"].insert(results); + this.state.selectableTags = [...result]; + } + + onKeydownSearchInput(ev) { + if (ev.key === "Enter") { + if (!this.state.searchStr.trim()) { + return; + } + ev.preventDefault(); + this.onClickCreateToggle(); + } + } + + async onClickFooterSelectedTag(tag) { + this.toggleSelectedTag(tag); + } + + async onClickCreateToggle() { + const tagName = this.state.searchStr.trim(); + const existingSelectableTag = this.state.selectableTags.find((tag) => tag.name === tagName); + if (this.props.thread.livechat_conversation_tag_ids.includes(existingSelectableTag)) { + return; + } + if ( + existingSelectableTag && + !this.props.thread.livechat_conversation_tag_ids.includes(existingSelectableTag) + ) { + this.toggleSelectedTag(existingSelectableTag); + this.state.searchStr = ""; + return; + } + const [tagId] = await this.orm.create("im_livechat.conversation.tag", [ + { name: escapeRegExp(tagName) }, + ]); + const newTag = this.store["im_livechat.conversation.tag"].insert({ + id: tagId, + name: tagName, + }); + this.state.selectableTags = [newTag, ...this.state.selectableTags]; + this.toggleSelectedTag(newTag); + this.state.searchStr = ""; + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_conversation_tag_edit.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_conversation_tag_edit.scss new file mode 100644 index 0000000..f24d74b --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_conversation_tag_edit.scss @@ -0,0 +1,30 @@ +.o-livechat-ConversationTagEdit { + min-height: 0; + width: 300px; +} + +.o-livechat-ConversationTagEdit .o-mail-NavigableList-item { + &:nth-child(odd) { + background-color: $gray-100 !important;; + } + &:hover { + background-color: mix($gray-100, $gray-200) !important; + } +} + +.o-livechat-ConversationTagEdit-selectedList { + max-height: 100px; + + button { + background-color: mix($o-view-background-color, $o-action, 85%); + + &:not(:hover) .oi-close { + border-color: transparent !important; + } + + &:hover .oi-close { + color: $danger; + border-color: $danger !important; + } + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_conversation_tag_edit.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_conversation_tag_edit.xml new file mode 100644 index 0000000..f5dd1b2 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/livechat_conversation_tag_edit.xml @@ -0,0 +1,33 @@ + + + + +
+
+ + +
+
    +
    + Tag has already been selected +
    +
    + No tags found +
    + +
+
+
+ + + +
+
+
+
+ +
diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/message.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/message.scss new file mode 100644 index 0000000..d3deeb8 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/message.scss @@ -0,0 +1,3 @@ +.o_livechat_emoji_rating { + width: 1.5rem; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/notification_item_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/notification_item_patch.xml new file mode 100644 index 0000000..6f3d525 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/notification_item_patch.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/partner_compare.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/partner_compare.js new file mode 100644 index 0000000..ec84415 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/partner_compare.js @@ -0,0 +1,24 @@ +import { partnerCompareRegistry } from "@mail/core/common/partner_compare"; + +partnerCompareRegistry.add( + "im_livechat.available", + (p1, p2, { thread }) => { + if (thread?.channel_type === "livechat" && p1.is_available !== p2.is_available) { + return p1.is_available ? -1 : 1; + } + }, + { sequence: 15 } +); + +partnerCompareRegistry.add( + "im_livechat.invite-count", + (p1, p2, { thread }) => { + if ( + thread?.channel_type === "livechat" && + p1.invite_by_self_count !== p2.invite_by_self_count + ) { + return p2.invite_by_self_count - p1.invite_by_self_count; + } + }, + { sequence: 20 } +); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/store_service_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/store_service_patch.js new file mode 100644 index 0000000..48417cb --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/store_service_patch.js @@ -0,0 +1,81 @@ +import { Store } from "@mail/core/common/store_service"; +import { compareDatetime } from "@mail/utils/common/misc"; +import { _t } from "@web/core/l10n/translation"; + +import { patch } from "@web/core/utils/patch"; + +/** @type {import("models").Store} */ +const storePatch = { + setup() { + super.setup(...arguments); + this.livechatChannels = this.makeCachedFetchData("im_livechat.channel"); + this.livechatSelfExpertises = this.makeCachedFetchData("/im_livechat/fetch_self_expertise"); + this.has_access_livechat = false; + }, + /** + * @override + */ + onStarted() { + super.onStarted(...arguments); + if (this.discuss.isActive && this.has_access_livechat) { + this.livechatChannels.fetch(); + this.livechatSelfExpertises.fetch(); + } + }, + /** @returns {boolean} Whether the livechat thread changed. */ + goToOldestUnreadLivechatThread() { + const [oldestUnreadThread] = this.discuss.livechats + .filter((thread) => thread.isUnread) + .sort( + (t1, t2) => + !t2.livechat_end_dt - !t1.livechat_end_dt || + compareDatetime(t1.lastInterestDt, t2.lastInterestDt) || + t1.id - t2.id + ); + if (!oldestUnreadThread) { + return false; + } + if (this.discuss.isActive) { + oldestUnreadThread.setAsDiscussThread(); + return true; + } + this.store.chatHub.initPromise.then(() => { + const chatWindow = this.ChatWindow.insert({ thread: oldestUnreadThread }); + chatWindow.open({ focus: true, jumpToNewMessage: true }); + }); + return true; + }, + get livechatStatusButtons() { + return [ + { + label: _t("In progress"), + status: "in_progress", + icon: "fa fa-comments", + }, + { + label: _t("Waiting for customer"), + status: "waiting", + icon: "fa fa-hourglass-start", + }, + { + label: _t("Looking for help"), + status: "need_help", + icon: "fa fa-lg fa-exclamation-circle", + }, + ]; + }, + /** + * @override + */ + tabToThreadType(tab) { + const threadTypes = super.tabToThreadType(tab); + if (tab === "chat" && !this.env.services.ui.isSmall) { + threadTypes.push("livechat"); + } + if (tab === "livechat") { + threadTypes.push("livechat"); + } + return threadTypes; + }, +}; +patch(Store.prototype, storePatch); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/suggestion_service_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/suggestion_service_patch.js new file mode 100644 index 0000000..8c8eba5 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/suggestion_service_patch.js @@ -0,0 +1,13 @@ +import { SuggestionService } from "@mail/core/common/suggestion_service"; + +import { patch } from "@web/core/utils/patch"; + +patch(SuggestionService.prototype, { + /** @override */ + getSupportedDelimiters(thread, env) { + const res = super.getSupportedDelimiters(...arguments); + return thread.channel_type === "livechat" + ? res.filter((delimiter) => delimiter.at(0) !== "#") + : res; + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/thread_actions.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/thread_actions.js new file mode 100644 index 0000000..5d91bc8 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/thread_actions.js @@ -0,0 +1,83 @@ +import { registerThreadAction } from "@mail/core/common/thread_actions"; + +import { LIVECHAT_INFO_DEFAULT_OPEN_LS } from "@im_livechat/core/public_web/discuss_app_model_patch"; +import { LivechatChannelInfoList } from "@im_livechat/core/web/livechat_channel_info_list"; + +import { _t } from "@web/core/l10n/translation"; + +registerThreadAction("livechat-info", { + actionPanelComponent: LivechatChannelInfoList, + condition: ({ owner, store, thread }) => + thread?.channel_type === "livechat" && + store.self_partner?.main_user_id?.share === false && + !owner.isDiscussSidebarChannelActions, + panelOuterClass: "o-livechat-ChannelInfoList bg-inherit", + icon: "fa fa-fw fa-info", + name: _t("Information"), + open: ({ store }) => { + store.discuss.isLivechatInfoPanelOpenByDefault = true; + localStorage.removeItem(LIVECHAT_INFO_DEFAULT_OPEN_LS); + }, + close: ({ action, store }) => { + if (action.condition) { + store.discuss.isLivechatInfoPanelOpenByDefault = false; + localStorage.setItem(LIVECHAT_INFO_DEFAULT_OPEN_LS, "false"); + } + }, + sequence: 10, + sequenceGroup: 7, + toggle: true, +}); +registerThreadAction("livechat-status", { + actionPanelComponent: LivechatChannelInfoList, + condition: ({ owner, store, thread }) => + thread?.channel_type === "livechat" && + store.has_access_livechat && + !thread.livechat_end_dt && + !owner.isDiscussContent, + dropdown: true, + dropdownMenuClass: "p-0", + dropdownTemplate: "im_livechat.LivechatStatusSelection", + dropdownTemplateParams: ({ thread }) => ({ livechatThread: thread }), + panelOuterClass: "o-livechat-ChannelInfoList bg-inherit", + icon: ({ store, thread }) => { + const btn = store.livechatStatusButtons.find( + (btn) => btn.status === thread.livechat_status + ); + if (!btn) { + return undefined; + } + return { + template: "im_livechat.LivechatStatusLabel", + params: { btn, inThreadActions: true }, + }; + }, + name: ({ thread }) => thread.livechatStatusLabel, + nameClass: "fst-italic small", + sequence: ({ owner }) => (owner.isDiscussSidebarChannelActions ? 10 : 5), + sequenceGroup: ({ owner }) => (owner.isDiscussSidebarChannelActions ? 5 : 7), + toggle: true, +}); +registerThreadAction("join-livechat-needing-help", { + condition: ({ owner, thread }) => + thread?.livechat_status === "need_help" && + !thread?.self_member_id && + !owner.isDiscussSidebarChannelActions, + icon: "fa fa-fw fa-sign-in", + name: _t("Join Chat"), + nameClass: "text-success", + open: async ({ store, thread }) => { + const hasJoined = await store.env.services.orm.call( + "discuss.channel", + "livechat_join_channel_needing_help", + [[thread.id]] + ); + if (!hasJoined && thread.isDisplayed) { + store.env.services.notification.add( + _t("Someone has already joined this conversation"), + { type: "warning" } + ); + } + }, + sequence: 5, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/thread_icon_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/thread_icon_patch.js new file mode 100644 index 0000000..459485d --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/thread_icon_patch.js @@ -0,0 +1,13 @@ +import { ThreadIcon } from "@mail/core/common/thread_icon"; +import { _t } from "@web/core/l10n/translation"; + +import { patch } from "@web/core/utils/patch"; + +patch(ThreadIcon.prototype, { + get defaultChatIcon() { + if (this.props.thread.channel_type === "livechat") { + return { class: "fa fa-comments opacity-75", title: _t("Livechat") }; + } + return super.defaultChatIcon; + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/thread_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/thread_model_patch.js new file mode 100644 index 0000000..ef414b2 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/core/web/thread_model_patch.js @@ -0,0 +1,45 @@ +import { Thread } from "@mail/core/common/thread_model"; +import { fields } from "@mail/model/misc"; +import { convertBrToLineBreak } from "@mail/utils/common/format"; +import { _t } from "@web/core/l10n/translation"; +import { rpc } from "@web/core/network/rpc"; + +import { patch } from "@web/core/utils/patch"; + +patch(Thread.prototype, { + setup() { + super.setup(); + this.hasFetchedLivechatSessionData = false; + this.livechat_note = fields.Html(); + /** @type {string|undefined} */ + this.livechatNoteText = fields.Attr(undefined, { + compute() { + if (this.livechat_note !== undefined) { + return convertBrToLineBreak(this.livechat_note || ""); + } + return this.livechatNoteText; + }, + }); + /** @type {"no_answer"|"no_agent"|"no_failure"|"escalated"|undefined} */ + this.livechat_outcome = undefined; + }, + get livechatStatusLabel() { + if (this.livechat_end_dt) { + return _t("Conversation has ended"); + } + const status = this.livechat_status; + if (status === "waiting") { + return _t("Waiting for customer"); + } else if (status === "need_help") { + return _t("Looking for help"); + } + return _t("In progress"); + }, + /** @param {"in_progress"|"waiting"|"need_help"} status */ + updateLivechatStatus(status) { + if (this.livechat_status === status) { + return; + } + rpc("/im_livechat/session/update_status", { channel_id: this.id, livechat_status: status }); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/@types/models.d.ts b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/@types/models.d.ts new file mode 100644 index 0000000..5ab3d85 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/@types/models.d.ts @@ -0,0 +1,23 @@ +declare module "models" { + export interface Message { + disableChatbotAnswers: boolean; + } + export interface Store { + activeLivechats: Thread[]; + guest_token: null; + livechat_available: boolean; + livechat_rule: LivechatChannelRule; + } + export interface Thread { + _toggleChatbot: boolean; + chatbot: Chatbot; + chatbotTypingMessage: Message; + hasWelcomeMessage: Readonly; + isLastMessageFromCustomer: Readonly; + livechat_operator_id: ResPartner; + livechatWelcomeMessage: Message; + readyToSwapDeferred: Deferred; + requested_by_operator: boolean; + storeAsActiveLivechats: Store; + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/attachment_upload_service_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/attachment_upload_service_patch.js new file mode 100644 index 0000000..c2a9d18 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/attachment_upload_service_patch.js @@ -0,0 +1,17 @@ +import { AttachmentUploadService } from "@mail/core/common/attachment_upload_service"; + +import { patch } from "@web/core/utils/patch"; + +patch(AttachmentUploadService.prototype, { + async upload(thread, composer, file, options) { + if (thread.channel_type === "livechat" && thread.isTransient) { + thread = await this.env.services["im_livechat.livechat"].persist(thread); + if (!thread) { + return; + } + thread.readyToSwapDeferred.resolve(); + composer = thread.composer; + } + return super.upload(thread, composer, file, options); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/autopopup_service.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/autopopup_service.js new file mode 100644 index 0000000..4be7b09 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/autopopup_service.js @@ -0,0 +1,50 @@ +import { expirableStorage } from "@im_livechat/core/common/expirable_storage"; +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; + +export class AutopopupService { + static STORAGE_KEY = "im_livechat_auto_popup"; + + /** + * @param {import("@web/env").OdooEnv} env + * @param {{ + * "im_livechat.livechat": import("@im_livechat/embed/common/livechat_service").LivechatService, + * "mail.store": import("@mail/core/common/store_service").Store, + * ui: typeof import("@web/core/ui/ui_service").uiService.start, + * }} services + */ + constructor(env, { "im_livechat.livechat": livechatService, "mail.store": storeService, ui }) { + this.storeService = storeService; + this.livechatService = livechatService; + this.ui = ui; + + storeService.isReady.then(() => { + browser.setTimeout(async () => { + await storeService.chatHub.initPromise; + if (this.allowAutoPopup) { + expirableStorage.setItem(AutopopupService.STORAGE_KEY, true); + livechatService.open(); + } + }, storeService.livechat_rule?.auto_popup_timer * 1000); + }); + } + + get allowAutoPopup() { + return Boolean( + !expirableStorage.getItem(AutopopupService.STORAGE_KEY) && + !this.ui.isSmall && + this.storeService.livechat_rule?.action === "auto_popup" && + this.storeService.livechat_available && + this.storeService.activeLivechats.length === 0 + ); + } +} + +export const autoPopupService = { + dependencies: ["im_livechat.livechat", "mail.store", "ui"], + + start(env, services) { + return new AutopopupService(env, services); + }, +}; +registry.category("services").add("im_livechat.autopopup", autoPopupService); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/boot_helpers.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/boot_helpers.js new file mode 100644 index 0000000..d25639a --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/boot_helpers.js @@ -0,0 +1,82 @@ +import { url } from "@web/core/utils/urls"; + +async function loadFont(name, url, targetDocument) { + await targetDocument.fonts.ready; + if ([...targetDocument.fonts].some(({ family }) => family === name)) { + // Font already loaded. + return; + } + const link = document.createElement("link"); + link.rel = "preload"; + link.as = "font"; + link.href = url; + link.crossOrigin = ""; + const style = document.createElement("style"); + style.appendChild( + document.createTextNode(` + @font-face { + font-family: ${name}; + src: url('${url}') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: block; + } + `) + ); + const loadPromise = new Promise((res, rej) => { + link.addEventListener("load", res); + link.addEventListener("error", rej); + }); + targetDocument.head.appendChild(link); + targetDocument.head.appendChild(style); + return loadPromise; +} + +function loadStyle(target) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = url("/im_livechat/assets_embed.css"); + const stylesLoadedPromise = new Promise((res, rej) => { + link.addEventListener("load", res); + link.addEventListener("error", rej); + }); + target.appendChild(link); + return stylesLoadedPromise; +} + +/** + * @param {HTMLElement} target + * @returns {HTMLDivElement} + */ +export function makeRoot(target) { + const root = document.createElement("div"); + root.classList.add("o-livechat-root"); + root.setAttribute("id", `o-livechat-root-${luxon.DateTime.now().ts + Math.random()}`); + root.style.zIndex = "calc(9e999)"; + root.style.position = "relative"; + root.style.display = "block"; + target.appendChild(root); + return root; +} + +export async function loadAssets(styleTarget) { + const document = styleTarget.ownerDocument; + await Promise.all([ + loadStyle(styleTarget), + loadFont("FontAwesome", url("/im_livechat/font-awesome"), document), + loadFont("odoo_ui_icons", url("/im_livechat/odoo_ui_icons"), document), + ]); +} + +/** + * Initialize the livechat container by loading the styles and + * the fonts. + * + * @param {HTMLElement} root + * @returns {ShadowRoot} + */ +export async function makeShadow(root) { + const shadow = root.attachShadow({ mode: "open" }); + await loadAssets(shadow); + return shadow; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_hub_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_hub_patch.js new file mode 100644 index 0000000..b31dbc0 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_hub_patch.js @@ -0,0 +1,23 @@ +import { LivechatButton } from "@im_livechat/embed/common/livechat_button"; +import { ChatHub } from "@mail/core/common/chat_hub"; +import { useExternalListener } from "@odoo/owl"; +import { patch } from "@web/core/utils/patch"; + +ChatHub.components = { ...ChatHub.components, LivechatButton }; + +patch(ChatHub.prototype, { + setup() { + super.setup(...arguments); + useExternalListener(document, "scroll", this._onScroll); + }, + _onScroll(ev) { + if (this.position.dragged) { + return; + } + const container = document.querySelector("html"); + this.position.bottom = + container.scrollHeight - container.scrollTop === container.clientHeight + ? `${this.chatHub.BUBBLE_OUTER * 5}px` + : `${this.chatHub.BUBBLE_OUTER}px`; + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_hub_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_hub_patch.xml new file mode 100644 index 0000000..22a4d53 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_hub_patch.xml @@ -0,0 +1,11 @@ + + + + + ChatHub + + + + + + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_window_model_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_window_model_patch.js new file mode 100644 index 0000000..0b2ff50 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_window_model_patch.js @@ -0,0 +1,13 @@ +import { patch } from "@web/core/utils/patch"; +import { ChatWindow } from "@mail/core/common/chat_window_model"; +import { CW_LIVECHAT_STEP } from "@im_livechat/core/common/chat_window_model_patch"; + +patch(ChatWindow.prototype, { + close() { + super.close(...arguments); + if (this.livechatStep === CW_LIVECHAT_STEP.FEEDBACK) { + this.store.env.services["im_livechat.livechat"].leave(this.thread); + this.thread.chatbot?.stop(); + } + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_window_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_window_patch.js new file mode 100644 index 0000000..76db48b --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_window_patch.js @@ -0,0 +1,33 @@ +import { CW_LIVECHAT_STEP } from "@im_livechat/core/common/chat_window_model_patch"; +import { FeedbackPanel } from "@im_livechat/embed/common/feedback_panel/feedback_panel"; + +import { ChatWindow } from "@mail/core/common/chat_window"; +import { useState } from "@odoo/owl"; + +import { useService } from "@web/core/utils/hooks"; +import { patch } from "@web/core/utils/patch"; + +Object.assign(ChatWindow.components, { FeedbackPanel }); + +patch(ChatWindow.prototype, { + setup() { + super.setup(...arguments); + this.livechatService = useService("im_livechat.livechat"); + this.livechatState = useState({ showCloseConfirmation: false }); + }, + async onClickNewSession() { + await this.close(); + await this.livechatService.open(); + }, + onClickFeedback() { + this.props.chatWindow.livechatStep = CW_LIVECHAT_STEP.CONFIRM_CLOSE; // Skip the confirmation step. + this.close(); + }, + get showGiveFeedbackBtn() { + const thread = this.props.chatWindow.thread; + if (thread?.channel_type !== "livechat") { + return false; + } + return thread.chatbot?.completed || thread.livechat_end_dt; + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_window_patch.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_window_patch.scss new file mode 100644 index 0000000..8c1053f --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_window_patch.scss @@ -0,0 +1,17 @@ +.o-mail-ChatWindow-moreActions { + &:hover, &.o-active, &.o-hover { + color: inherit !important; + } +} + +.o-mail-ChatWindow-header .o-mail-ActionList { + button { + --btn-color: var(--o-mail-livechat-btn-color); + --btn-hover-color: var(--o-mail-livechat-btn-color); + --btn-active-color: var(--o-mail-livechat-btn-color); + + &:not(:hover, &:focus-visible, &:active) { + --o-mail-ActionList-Button-opacity: 1; + } + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_window_patch.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_window_patch.xml new file mode 100644 index 0000000..db65fca --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/chat_window_patch.xml @@ -0,0 +1,23 @@ + + + + + + + $0 + + + + $0 + + + + + + + + + + + + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/composer.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/composer.scss new file mode 100644 index 0000000..91d0264 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/composer.scss @@ -0,0 +1,5 @@ +.o-mail-Composer-input, .o-mail-Composer-fake { + &.o-mobile { + font-size: 16px; + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/composer_patch.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/composer_patch.js new file mode 100644 index 0000000..35a7402 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/composer_patch.js @@ -0,0 +1,13 @@ +import { Composer } from "@mail/core/common/composer"; + +import { _t } from "@web/core/l10n/translation"; +import { patch } from "@web/core/utils/patch"; + +patch(Composer.prototype, { + get placeholder() { + if (this.thread?.channel_type !== "livechat") { + return super.placeholder; + } + return _t("Say something..."); + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/disabled_features.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/disabled_features.js new file mode 100644 index 0000000..f933f81 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/disabled_features.js @@ -0,0 +1,12 @@ +import { Thread } from "@mail/core/common/thread_model"; + +import { patch } from "@web/core/utils/patch"; + +patch(Thread.prototype, { + get hasMemberList() { + return false; + }, + get hasAttachmentPanel() { + return this.channel_type !== "livechat" && super.hasAttachmentPanel; + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/emoji_picker.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/emoji_picker.scss new file mode 100644 index 0000000..70697db --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/emoji_picker.scss @@ -0,0 +1,3 @@ +.o-EmojiPicker { + font-size: 1.2em; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/feedback_panel/feedback_panel.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/feedback_panel/feedback_panel.js new file mode 100644 index 0000000..0f1693f --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/feedback_panel/feedback_panel.js @@ -0,0 +1,71 @@ +import { RATING } from "@im_livechat/embed/common/livechat_service"; +import { TranscriptSender } from "@im_livechat/core/common/transcript_sender"; + +import { Component, useState } from "@odoo/owl"; + +import { useService } from "@web/core/utils/hooks"; +import { session } from "@web/session"; +import { url } from "@web/core/utils/urls"; +import { rpc } from "@web/core/network/rpc"; + +/** + * @typedef {Object} Props + * @property {Function} [onClickClose] + * @property {import("models").Thread} + * @extends {Component} + */ +export class FeedbackPanel extends Component { + static template = "im_livechat.FeedbackPanel"; + static props = ["onClickClose?", "onClickNewSession", "thread"]; + static components = { TranscriptSender }; + + STEP = Object.freeze({ + RATING: "rating", + THANKS: "thanks", + }); + RATING = RATING; + + setup() { + this.session = session; + this.livechatService = useService("im_livechat.livechat"); + this.store = useService("mail.store"); + this.state = useState({ + step: this.STEP.RATING, + rating: null, + feedback: "", + }); + this.url = url; + } + + /** + * @param {number} rating + */ + select(rating) { + this.state.rating = rating; + } + + get allowNewSession() { + return ( + this.store.livechat_rule?.action !== "hide_button" && + this.livechatService.options.channel_id + ); + } + + /** @deprecated use `thread.transcriptUrl` instead */ + get transcriptUrl() { + return this.props.thread.transcriptUrl; + } + + onClickSendFeedback() { + rpc("/im_livechat/feedback", { + reason: this.state.feedback, + rate: this.state.rating, + channel_id: this.props.thread.id, + }); + this.state.step = this.STEP.THANKS; + const link = this.livechatService.options.review_link; + if (this.state.rating === this.RATING.GOOD && link) { + window.open(link, "_blank", "noopener"); + } + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/feedback_panel/feedback_panel.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/feedback_panel/feedback_panel.xml new file mode 100644 index 0000000..70104c7 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/embed/common/feedback_panel/feedback_panel.xml @@ -0,0 +1,36 @@ + + + + +
+
+
+ +

Did we correctly answer your question?

+
+ + + +
+
+ +

Thank you for your feedback

+
+
+
+ -
- -
-
-
- - Receive a copy of this conversation -
- - -
-
- -
- Close conversation -
-
- - diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/livechat_button.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/livechat_button.js deleted file mode 100644 index 002ce0e..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/livechat_button.js +++ /dev/null @@ -1,84 +0,0 @@ -/** @odoo-module **/ - -import time from 'web.time'; -import {getCookie} from 'web.utils.cookies'; -import Widget from 'web.Widget'; - -const LivechatButton = Widget.extend({ - className: 'openerp o_livechat_button d-print-none', - events: { - 'click': '_onClick' - }, - init(parent, messaging) { - this._super(parent); - this.messaging = messaging; - }, - start() { - this.messaging.publicLivechatGlobal.livechatButtonView.start(); - return this._super(); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Will try to get a previous operator for this visitor. - * If the visitor already had visitor A, it's better for his user experience - * to get operator A again. - * - * The information is stored in the 'im_livechat_previous_operator_pid' cookie. - * - * @private - * @return {integer} operator_id.partner_id.id if the cookie is set - */ - _get_previous_operator_id() { - const cookie = getCookie('im_livechat_previous_operator_pid'); - if (cookie) { - return cookie; - } - - return null; - }, - /** - * @private - */ - _prepareGetSessionParameters() { - return { - channel_id: this.messaging.publicLivechatGlobal.channelId, - anonymous_name: this.messaging.publicLivechatGlobal.livechatButtonView.defaultUsername, - previous_operator_id: this._get_previous_operator_id(), - }; - }, - /** - * @private - */ - _sendWelcomeMessage() { - if (this.messaging.publicLivechatGlobal.livechatButtonView.defaultMessage) { - this.messaging.publicLivechatGlobal.livechatButtonView.addMessage({ - id: '_welcome', - author: { - id: this.messaging.publicLivechatGlobal.publicLivechat.operator.id, - name: this.messaging.publicLivechatGlobal.publicLivechat.operator.name, - }, - body: this.messaging.publicLivechatGlobal.livechatButtonView.defaultMessage, - date: time.datetime_to_str(new Date()), - model: "mail.channel", - res_id: this.messaging.publicLivechatGlobal.publicLivechat.id, - }, { prepend: true }); - } - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onClick() { - this.messaging.publicLivechatGlobal.livechatButtonView.openChat(); - }, -}); - -export default LivechatButton; diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.js deleted file mode 100644 index 6df9b8a..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.js +++ /dev/null @@ -1,451 +0,0 @@ -/** @odoo-module **/ - -import * as mailUtils from '@mail/js/utils'; - -import core from 'web.core'; -import time from 'web.time'; -import Widget from 'web.Widget'; - -const QWeb = core.qweb; -const _lt = core._lt; - -const ORDER = { - ASC: 1, // visually, ascending order of message IDs (from top to bottom) - DESC: -1, // visually, descending order of message IDs (from top to bottom) -}; - -const READ_MORE = _lt("read more"); -const READ_LESS = _lt("read less"); - -/** - * This is a generic widget to render a thread. - * Any thread that extends mail.model.AbstractThread can be used with this - * widget. - */ -const PublicLivechatView = Widget.extend({ - className: 'o_mail_thread', - - events: { - 'click a': '_onClickRedirect', - 'click img': '_onClickRedirect', - 'click strong': '_onClickRedirect', - 'click .o_thread_show_more': '_onClickShowMore', - 'click .o_thread_message_needaction': '_onClickMessageNeedaction', - 'click .o_thread_message_star': '_onClickMessageStar', - 'click .o_thread_message_reply': '_onClickMessageReply', - 'click .oe_mail_expand': '_onClickMailExpand', - 'click .o_thread_message': '_onClickMessage', - 'click': '_onClick', - 'click .o_thread_message_notification_error': '_onClickMessageNotificationError', - }, - - /** - * @override - * @param {widget} parent - * @param {Messaging} messaging - * @param {Object} options - */ - init(parent, messaging, options) { - this._super(...arguments); - this.messaging = messaging; - // options when the thread is enabled (e.g. can send message, - // interact on messages, etc.) - this._enabledOptions = _.defaults(options || {}, { - displayOrder: ORDER.ASC, - displayMarkAsRead: true, - displayDocumentLinks: true, - displayAvatars: true, - squashCloseMessages: true, - loadMoreOnScroll: false, - }); - this._selectedMessageID = null; - this._currentThreadID = null; - }, - /** - * The message mail popover may still be shown at this moment. If we do not - * remove it, it stays visible on the page until a page reload. - * - * @override - */ - destroy() { - clearInterval(this._updateTimestampsInterval); - this._super(); - }, - /** - * @param {Object} [options] - * @param {integer} [options.displayOrder=ORDER.ASC] order of displaying - * messages in the thread: - * - ORDER.ASC: last message is at the bottom of the thread - * - ORDER.DESC: last message is at the top of the thread - * @param {boolean} [options.displayLoadMore] - * @param {Array} [options.domain=[]] the domain for the messages in the - * thread. - * @param {boolean} [options.scrollToBottom=false] - * @param {boolean} [options.squashCloseMessages] - */ - render(options) { - let shouldScrollToBottomAfterRendering = false; - if (this._currentThreadID === this.messaging.publicLivechatGlobal.publicLivechat.id && this.isAtBottom()) { - shouldScrollToBottomAfterRendering = true; - } - this._currentThreadID = this.messaging.publicLivechatGlobal.publicLivechat.id; - - // copy so that reverse do not alter order in the thread object - const messages = _.clone(this.messaging.publicLivechatGlobal.publicLivechat.widget.getMessages()); - - const modeOptions = this._enabledOptions; - - options = Object.assign({}, modeOptions, options, { - selectedMessageID: this._selectedMessageID, - }); - - // dict where key is message ID, and value is whether it should display - // the author of message or not visually - const displayAuthorMessages = {}; - - // Hide avatar and info of a message if that message and the previous - // one are both comments wrote by the same author at the same minute - // and in the same document (users can now post message in documents - // directly from a channel that follows it) - let prevMessage; - for (let message of messages) { - if ( - // is first message of thread - !prevMessage || - // more than 1 min. elasped - (Math.abs(message.getDate().diff(prevMessage.getDate())) > 60000) || - prevMessage.getType() !== 'comment' || - message.getType() !== 'comment' || - // from a different author - prevMessage.getAuthorID() !== message.getAuthorID() - ) { - displayAuthorMessages[message.getID()] = true; - } else { - displayAuthorMessages[message.getID()] = !options.squashCloseMessages; - } - prevMessage = message; - } - - if (modeOptions.displayOrder === ORDER.DESC) { - messages.reverse(); - } - - this.$el.html(QWeb.render('im_livechat.legacy.mail.widget.Thread', { - displayAuthorMessages, - options, - ORDER, - dateFormat: time.getLangDatetimeFormat(), - widget: this, - })); - - for (let message of messages) { - const $message = this.$('.o_thread_message[data-message-id="' + message.getID() + '"]'); - $message.find('.o_mail_timestamp').data('date', message.getDate()); - - this._insertReadMore($message); - } - - if (shouldScrollToBottomAfterRendering) { - this.scrollToBottom(); - } - - if (!this._updateTimestampsInterval) { - this.updateTimestampsInterval = setInterval(() => { - this._updateTimestamps(); - }, 1000 * 60); - } - }, - - /** - * Render thread widget when loading, i.e. when messaging is not yet ready. - * @see /mail/init_messaging - */ - renderLoading() { - this.$el.html(QWeb.render('im_livechat.legacy.mail.widget.ThreadLoading')); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - getScrolltop() { - return this.$el.scrollTop(); - }, - /** - * State whether the bottom of the thread is visible or not, - * with a tolerance of 5 pixels - * - * @return {boolean} - */ - isAtBottom() { - const fullHeight = this.el.scrollHeight; - const topHiddenHeight = this.$el.scrollTop(); - const visibleHeight = this.$el.outerHeight(); - const bottomHiddenHeight = fullHeight - topHiddenHeight - visibleHeight; - return bottomHiddenHeight < 5; - }, - /** - * Scroll to the bottom of the thread - */ - scrollToBottom() { - this.$el.scrollTop(this.el.scrollHeight); - }, - /** - * Scrolls the thread to a given message - * - * @param {integer} options.msgID the ID of the message to scroll to - * @param {integer} [options.duration] - * @param {boolean} [options.onlyIfNecessary] - */ - scrollToMessage(options) { - const $target = this.$('.o_thread_message[data-message-id="' + options.messageID + '"]'); - if (options.onlyIfNecessary) { - const delta = $target.parent().height() - $target.height(); - let offset = delta < 0 ? - 0 : - delta - ($target.offset().top - $target.offsetParent().offset().top); - offset = - Math.min(offset, 0); - this.$el.scrollTo("+=" + offset + "px", options.duration); - } else if ($target.length) { - this.$el.scrollTo($target); - } - }, - /** - * Scroll to the specific position in pixel - * - * If no position is provided, scroll to the bottom of the thread - * - * @param {integer} [position] distance from top to position in pixels. - * If not provided, scroll to the bottom. - */ - scrollToPosition(position) { - if (position) { - this.$el.scrollTop(position); - } else { - this.scrollToBottom(); - } - }, - /** - * Unselect the selected message - */ - unselectMessage() { - this.$('.o_thread_message').removeClass('o_thread_selected_message'); - this._selectedMessageID = null; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Modifies $element to add the 'read more/read less' functionality - * All element nodes with 'data-o-mail-quote' attribute are concerned. - * All text nodes after a ``#stopSpelling`` element are concerned. - * Those text nodes need to be wrapped in a span (toggle functionality). - * All consecutive elements are joined in one 'read more/read less'. - * - * @private - * @param {jQuery} $element - */ - _insertReadMore($element) { - - const groups = []; - let readMoreNodes; - - // nodeType 1: element_node - // nodeType 3: text_node - const $children = $element.contents() - .filter(function () { - return this.nodeType === 1 || - this.nodeType === 3 && - this.nodeValue.trim(); - }); - - for (let child of $children) { - let $child = $(child); - - // Hide Text nodes if "stopSpelling" - if ( - child.nodeType === 3 && - $child.prevAll('[id*="stopSpelling"]').length > 0 - ) { - // Convert Text nodes to Element nodes - $child = $('', { - text: child.textContent, - 'data-o-mail-quote': '1', - }); - child.parentNode.replaceChild($child[0], child); - } - - // Create array for each 'read more' with nodes to toggle - if ( - $child.attr('data-o-mail-quote') || - ( - $child.get(0).nodeName === 'BR' && - $child.prev('[data-o-mail-quote="1"]').length > 0 - ) - ) { - if (!readMoreNodes) { - readMoreNodes = []; - groups.push(readMoreNodes); - } - $child.hide(); - readMoreNodes.push($child); - } else { - readMoreNodes = undefined; - this._insertReadMore($child); - } - } - - for (let group of groups) { - // Insert link just before the first node - const $readMore = $('', { - class: 'o_mail_read_more', - href: '#', - text: READ_MORE, - }).insertBefore(group[0]); - - // Toggle All next nodes - let isReadMore = true; - $readMore.click(function (e) { - e.preventDefault(); - isReadMore = !isReadMore; - for (let $child of group) { - $child.hide(); - $child.toggle(!isReadMore); - } - $readMore.text(isReadMore ? READ_MORE : READ_LESS); - }); - } - }, - /** - * @private - * @param {Object} options - * @param {integer} [options.channelID] - * @param {string} options.model - * @param {integer} options.id - */ - _redirect: _.debounce(function (options) { - if ('channelID' in options) { - this.trigger('redirect_to_channel', options.channelID); - } else { - this.trigger('redirect', options.model, options.id); - } - }, 500, true), - /** - * @private - */ - _updateTimestamps() { - const isAtBottom = this.isAtBottom(); - this.$('.o_mail_timestamp').each(function () { - const date = $(this).data('date'); - $(this).text(mailUtils.timeFromNow(date)); - }); - if (isAtBottom && !this.isAtBottom()) { - this.scrollToBottom(); - } - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onClick() { - if (this._selectedMessageID) { - this.unselectMessage(); - this.trigger('unselect_message'); - } - }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickMailExpand(ev) { - ev.preventDefault(); - }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickMessage(ev) { - $(ev.currentTarget).toggleClass('o_thread_selected_message'); - }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickMessageNeedaction(ev) { - const messageID = $(ev.currentTarget).data('message-id'); - this.trigger('mark_as_read', messageID); - }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickMessageNotificationError(ev) { - const messageID = $(ev.currentTarget).data('message-id'); - this.do_action('mail.mail_resend_message_action', { - additional_context: { - mail_message_to_resend: messageID, - } - }); - }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickMessageReply(ev) { - this._selectedMessageID = $(ev.currentTarget).data('message-id'); - this.$('.o_thread_message').removeClass('o_thread_selected_message'); - this.$('.o_thread_message[data-message-id="' + this._selectedMessageID + '"]') - .addClass('o_thread_selected_message'); - this.trigger('select_message', this._selectedMessageID); - ev.stopPropagation(); - }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickMessageStar(ev) { - const messageID = $(ev.currentTarget).data('message-id'); - this.trigger('toggle_star_status', messageID); - }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickRedirect(ev) { - // ignore inherited branding - if ($(ev.target).data('oe-field') !== undefined) { - return; - } - const id = $(ev.target).data('oe-id'); - if (id) { - ev.preventDefault(); - const model = $(ev.target).data('oe-model'); - let options; - if (model && (model !== 'mail.channel')) { - options = { - model, - id - }; - } else { - options = { channelID: id }; - } - this._redirect(options); - } - }, - /** - * @private - */ - _onClickShowMore() { - this.trigger('load_more_messages'); - }, -}); - -PublicLivechatView.ORDER = ORDER; - -export default PublicLivechatView; diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.xml deleted file mode 100644 index 1ac00cb..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.xml +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - - - - - - -
- - Please wait... -
- - - - - - - - - - -
- - - -
- -
-
- - - -
- - - - -
-
- - - - - -
- - - - -
- - - -
- - - -
-
- -
- - - - -
-
- - - - - - -
-
-

- - Note by - - - - - - - - - - - - - - -

-
-
-
-
- -
-
-
-
-
- - - - - - - -
- - Loading older messages... - - - - -
-
- - - - - - - -
- New messages -
-
- \ No newline at end of file diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js deleted file mode 100644 index 9aea23e..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js +++ /dev/null @@ -1,344 +0,0 @@ -/** @odoo-module **/ - -import config from 'web.config'; -import { _t, qweb } from 'web.core'; -import Widget from 'web.Widget'; - -import {unaccent} from 'web.utils'; -import {setCookie} from 'web.utils.cookies'; - -/** - * This is the widget that represent windows of livechat in the frontend. - * - * @see @im_livechat/legacy/widgets/public_livechat_window/public_livechat_window for more information - */ -const PublicLivechatWindow = Widget.extend({ - FOLD_ANIMATION_DURATION: 200, // duration in ms for (un)fold transition - HEIGHT_OPEN: '400px', // height in px of thread window when open - HEIGHT_FOLDED: '34px', // height, in px, of thread window when folded - template: 'im_livechat.legacy.PublicLivechatWindow', - events: { - 'click .o_thread_window_close': '_onClickClose', - 'click .o_thread_window_header': '_onClickFold', - 'click .o_composer_text_field': '_onComposerClick', - 'click .o_mail_thread': '_onThreadWindowClicked', - 'keydown .o_composer_text_field': '_onKeydown', - 'keypress .o_composer_text_field': '_onKeypress', - 'input .o_composer_text_field': '_onInput', - }, - /** - * @param {Widget} parent - * @param {Messaging} messaging - * @param {@im_livechat/legacy/models/public_livechat} thread - */ - init(parent, messaging, thread) { - this._super(parent); - this.messaging = messaging; - - this._debouncedOnScroll = _.debounce(this._onScroll.bind(this), 100); - }, - /** - * @override - * @return {Promise} - */ - async start() { - this.$input = this.$('.o_composer_text_field'); - this.$header = this.$('.o_thread_window_header'); - - // animate the (un)folding of thread windows - this.$el.css({ transition: 'height ' + this.FOLD_ANIMATION_DURATION + 'ms linear' }); - if (this.messaging.publicLivechatGlobal.publicLivechat.isFolded) { - this.$el.css('height', this.HEIGHT_FOLDED); - } else { - this._focusInput(); - } - const def = this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.replace(this.$('.o_thread_window_content')).then(() => { - this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.$el.on('scroll', this, this._debouncedOnScroll); - }); - await Promise.all([this._super(), def]); - if (this.messaging.publicLivechatGlobal.livechatButtonView.headerBackgroundColor) { - this.$('.o_thread_window_header').css('background-color', this.messaging.publicLivechatGlobal.livechatButtonView.headerBackgroundColor); - } - if (this.messaging.publicLivechatGlobal.livechatButtonView.titleColor) { - this.$('.o_thread_window_header').css('color', this.messaging.publicLivechatGlobal.livechatButtonView.titleColor); - } - }, - - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - close() { - const isComposerDisabled = this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_thread_composer input').prop('disabled'); - const shouldAskFeedback = !isComposerDisabled && this.messaging.publicLivechatGlobal.messages.find(function (message) { - return message.id !== '_welcome'; - }); - if (shouldAskFeedback) { - this.messaging.publicLivechatGlobal.chatWindow.widget.toggleFold(false); - this.messaging.publicLivechatGlobal.livechatButtonView.askFeedback(); - } else { - this.messaging.publicLivechatGlobal.livechatButtonView.closeChat(); - } - this.messaging.publicLivechatGlobal.leaveSession(); - }, - /** - * States whether the current environment is in mobile or not. This is - * useful in order to customize the template rendering for mobile view. - * - * @returns {boolean} - */ - isMobile() { - return config.device.isMobile; - }, - /** - * Render the thread window - */ - render() { - this.renderHeader(); - this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.render({ displayLoadMore: false }); - }, - /** - * Render the header of this thread window. - * This is useful when some information on the header have be updated such - * as the status or the title of the thread that have changed. - * - * @private - */ - renderHeader() { - this.$header.html(qweb.render('im_livechat.legacy.PublicLivechatWindow.HeaderContent', { widget: this })); - }, - - /** - * Render the chat window itself. - */ - renderChatWindow() { - this.renderElement(); - this.adjustPosition(); - }, - - /** - * Compute position of this chat window and apply corresponding styles to - * the underlying widget. - */ - adjustPosition() { - const cssProps = { bottom: 0 }; - cssProps[this.messaging.locale.textDirection === 'rtl' ? 'left' : 'right'] = 0; - if (!config.device.isMobile) { - const margin_dir = _t.database.parameters.direction === "rtl" ? "margin-left" : "margin-right"; - cssProps[margin_dir] = $.position.scrollbarWidth(); - } - this.$el.css(cssProps); - }, - - /** - * Replace the thread content with provided new content - * - * @param {$.Element} $element - */ - replaceContentWith($element) { - $element.replace(this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.$el); - }, - /** - * Toggle the fold state of this thread window. Also update the fold state - * of the thread model. If the boolean parameter `folded` is provided, it - * folds/unfolds the window when it is set/unset. - * - * Warn the parent widget (LivechatButton) - * - * @param {boolean} [folded] if not a boolean, toggle the fold state. - * Otherwise, fold/unfold the window if set/unset. - */ - toggleFold(folded) { - if (!_.isBoolean(folded)) { - folded = !this.messaging.publicLivechatGlobal.publicLivechat.isFolded; - } - this.messaging.publicLivechatGlobal.publicLivechat.update({ isFolded: folded }); - if (this.messaging.publicLivechatGlobal.publicLivechat.operator) { - setCookie('im_livechat_session', unaccent(JSON.stringify(this.messaging.publicLivechatGlobal.publicLivechat.widget.toData()), true), 60 * 60, 'required'); - } - this.updateVisualFoldState(); - }, - /** - * Update the visual state of the window so that it matched the internal - * fold state. This is useful in case the related thread has its fold state - * that has been changed. - */ - updateVisualFoldState() { - if (!this.messaging.publicLivechatGlobal.publicLivechat.isFolded) { - this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.scrollToBottom(); - this._focusInput(); - } - const height = this.messaging.publicLivechatGlobal.publicLivechat.isFolded ? this.HEIGHT_FOLDED : this.HEIGHT_OPEN; - this.$el.css({ height }); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Set the focus on the composer of the thread window. This operation is - * ignored in mobile context. - * - * @private - * Set the focus on the input of the window - */ - _focusInput() { - if ( - config.device.touch && - config.device.size_class <= config.device.SIZES.SM - ) { - return; - } - this.$input.focus(); - }, - /** - * Tells whether there is focus on this thread. Note that a thread that has - * the focus means the input has focus. - * - * @private - * @returns {boolean} - */ - _hasFocus() { - return this.$input.is(':focus'); - }, - /** - * Post a message on this thread window, and auto-scroll to the bottom of - * the thread. - * - * @private - * @param {Object} messageData - */ - async _postMessage(messageData) { - try { - await this.messaging.publicLivechatGlobal.livechatButtonView.sendMessage(messageData); - } catch (_err) { - await this.messaging.publicLivechatGlobal.livechatButtonView.sendMessage(messageData); // try again just in case - } - if (!this.messaging.publicLivechatGlobal.publicLivechat.operator) { - return; - } - this.messaging.publicLivechatGlobal.publicLivechat.widget.postMessage(messageData) - .then(() => { - this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.scrollToBottom(); - }); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Close the thread window. - * Mark the thread as read if the thread window was open. - * - * @private - * @param {MouseEvent} ev - */ - _onClickClose(ev) { - ev.stopPropagation(); - ev.preventDefault(); - if ( - this.messaging.publicLivechatGlobal.publicLivechat.unreadCounter > 0 && - !this.messaging.publicLivechatGlobal.publicLivechat.isFolded - ) { - this.messaging.publicLivechatGlobal.publicLivechat.widget.markAsRead(); - } - this.close(); - }, - /** - * Fold/unfold the thread window. - * Also mark the thread as read. - * - * @private - */ - _onClickFold() { - if (!config.device.isMobile) { - this.toggleFold(); - } - }, - /** - * Called when the composer is clicked -> forces focus on input even if - * jquery's blockUI is enabled. - * - * @private - * @param {Event} ev - */ - _onComposerClick(ev) { - if ($(ev.target).closest('a, button').length) { - return; - } - this._focusInput(); - }, - /** - * Called when the input in the composer changes - * - * @private - */ - _onInput() { - const isTyping = this.$input.val().length > 0; - this.messaging.publicLivechatGlobal.publicLivechat.widget.setMyselfTyping({ typing: isTyping }); - }, - /** - * Called when typing something on the composer of this thread window. - * - * @private - * @param {KeyboardEvent} ev - */ - _onKeydown(ev) { - ev.stopPropagation(); // to prevent jquery's blockUI to cancel event - // ENTER key (avoid requiring jquery ui for external livechat) - if (ev.which === 13) { - const content = _.str.trim(this.$input.val()); - const messageData = { - content, - attachment_ids: [], - partner_ids: [], - }; - this.$input.val(''); - if (content) { - this._postMessage(messageData); - } - } - }, - /** - * @private - * @param {KeyboardEvent} ev - */ - _onKeypress(ev) { - ev.stopPropagation(); // to prevent jquery's blockUI to cancel event - }, - /** - * @private - */ - _onScroll() { - if ( - !this.messaging.exists() || - !this.messaging.publicLivechatGlobal || - !this.messaging.publicLivechatGlobal.chatWindow - ) { - return; - } - if (this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.isAtBottom()) { - this.messaging.publicLivechatGlobal.publicLivechat.widget.markAsRead(); - } - }, - /** - * When a thread window is clicked on, we want to give the focus to the main - * input. An exception is made when the user is selecting something. - * - * @private - */ - _onThreadWindowClicked() { - const selectObj = window.getSelection(); - if (selectObj.anchorOffset === selectObj.focusOffset) { - this.$input.focus(); - } - }, -}); - -export default PublicLivechatWindow; diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml deleted file mode 100644 index a5a8b39..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - -
-
- - - - - - -
- -
-
-
- -
-
-
-

No operator available

-
-
-
- - - - - - () - - - -
- - - - - - - - - - - - - diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/channel.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/channel.js deleted file mode 100644 index cfd2899..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/channel.js +++ /dev/null @@ -1,37 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; -import { attr, one } from '@mail/model/model_field'; - -registerPatch({ - name: 'Channel', - fields: { - anonymous_country: one('Country'), - anonymous_name: attr(), - discussSidebarCategory: { - compute() { - if (this.channel_type === 'livechat') { - return this.messaging.discuss.categoryLivechat; - } - return this._super(); - }, - }, - displayName: { - compute() { - if (!this.thread) { - return; - } - if (this.channel_type === 'livechat' && this.correspondent) { - if (!this.correspondent.is_public && this.correspondent.country) { - return `${this.thread.getMemberName(this.correspondent.persona)} (${this.correspondent.country.name})`; - } - if (this.anonymous_country) { - return `${this.thread.getMemberName(this.correspondent.persona)} (${this.anonymous_country.name})`; - } - return this.thread.getMemberName(this.correspondent.persona); - } - return this._super(); - }, - }, - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/channel_preview_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/channel_preview_view.js deleted file mode 100644 index 7751035..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/channel_preview_view.js +++ /dev/null @@ -1,17 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; - -registerPatch({ - name: 'ChannelPreviewView', - fields: { - imageUrl: { - compute() { - if (this.channel.channel_type === 'livechat') { - return '/mail/static/src/img/smiley/avatar.jpg'; - } - return this._super(); - }, - }, - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/chat_window.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/chat_window.js deleted file mode 100644 index 414b7b7..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/chat_window.js +++ /dev/null @@ -1,25 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; - -registerPatch({ - name: 'ChatWindow', - recordMethods: { - /** - * @override - */ - close({ notifyServer } = {}) { - if ( - this.thread && - this.thread.channel && - this.thread.channel.channel_type === 'livechat' && - this.thread.cache.isLoaded && - this.thread.messages.length === 0 - ) { - notifyServer = true; - this.thread.unpin(); - } - this._super({ notifyServer }); - }, - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/composer_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/composer_view.js deleted file mode 100644 index d7ebe1d..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/composer_view.js +++ /dev/null @@ -1,19 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; -import { clear } from '@mail/model/model_field_command'; -import '@mail/models/composer_view'; - -registerPatch({ - name: 'ComposerView', - fields: { - dropZoneView: { - compute() { - if (this.composer.thread && this.composer.thread.channel && this.composer.thread.channel.channel_type === 'livechat') { - return clear(); - } - return this._super(); - }, - }, - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/discuss.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/discuss.js deleted file mode 100644 index 36c2e21..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/discuss.js +++ /dev/null @@ -1,28 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; -import { one } from '@mail/model/model_field'; - -registerPatch({ - name: 'Discuss', - recordMethods: { - /** - * @override - */ - onInputQuickSearch(value) { - if (!this.sidebarQuickSearchValue) { - this.categoryLivechat.open(); - } - return this._super(value); - }, - }, - fields: { - /** - * Discuss sidebar category for `livechat` channel threads. - */ - categoryLivechat: one('DiscussSidebarCategory', { - default: {}, - inverse: 'discussAsLivechat', - }), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/discuss_sidebar_category.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/discuss_sidebar_category.js deleted file mode 100644 index 31ce029..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/discuss_sidebar_category.js +++ /dev/null @@ -1,70 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; -import { one } from '@mail/model/model_field'; -import { clear } from '@mail/model/model_field_command'; - -registerPatch({ - name: 'DiscussSidebarCategory', - fields: { - categoryItemsOrderedByLastAction: { - compute() { - if (this.discussAsLivechat) { - return this.categoryItems; - } - return this._super(); - }, - }, - discussAsLivechat: one('Discuss', { - identifying: true, - inverse: 'categoryLivechat', - }), - isServerOpen: { - compute() { - // there is no server state for non-users (guests) - if (!this.messaging.currentUser) { - return clear(); - } - if (!this.messaging.currentUser.res_users_settings_id) { - return clear(); - } - if (this.discussAsLivechat) { - return this.messaging.currentUser.res_users_settings_id.is_discuss_sidebar_category_livechat_open; - } - return this._super(); - }, - }, - name: { - compute() { - if (this.discussAsLivechat) { - return this.env._t("Livechat"); - } - return this._super(); - }, - }, - orderedCategoryItems: { - compute() { - if (this.discussAsLivechat) { - return this.categoryItemsOrderedByLastAction; - } - return this._super(); - }, - }, - serverStateKey: { - compute() { - if (this.discussAsLivechat) { - return 'is_discuss_sidebar_category_livechat_open'; - } - return this._super(); - }, - }, - supportedChannelTypes: { - compute() { - if (this.discussAsLivechat) { - return ['livechat']; - } - return this._super(); - }, - }, - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/discuss_sidebar_category_item.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/discuss_sidebar_category_item.js deleted file mode 100644 index 1716a33..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/discuss_sidebar_category_item.js +++ /dev/null @@ -1,52 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; -import { clear } from '@mail/model/model_field_command'; - -registerPatch({ - name: 'DiscussSidebarCategoryItem', - fields: { - avatarUrl: { - compute() { - if (this.channel.channel_type === 'livechat') { - if (this.channel.correspondent && !this.channel.correspondent.is_public) { - return this.channel.correspondent.avatarUrl; - } - } - return this._super(); - }, - }, - categoryCounterContribution: { - compute() { - if (this.channel.channel_type === 'livechat') { - return this.channel.localMessageUnreadCounter > 0 ? 1 : 0; - } - return this._super(); - }, - }, - counter: { - compute() { - if (this.channel.channel_type === 'livechat') { - return this.channel.localMessageUnreadCounter; - } - return this._super(); - }, - }, - hasThreadIcon: { - compute() { - if (this.channel.channel_type === 'livechat') { - return clear(); - } - return this._super(); - }, - }, - hasUnpinCommand: { - compute() { - if (this.channel.channel_type === 'livechat') { - return !this.channel.localMessageUnreadCounter; - } - return this._super(); - }, - }, - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/message.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/message.js deleted file mode 100644 index 2c85025..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/message.js +++ /dev/null @@ -1,17 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; - -registerPatch({ - name: 'Message', - fields: { - hasReactionIcon: { - compute() { - if (this.originThread && this.originThread.channel && this.originThread.channel.channel_type === 'livechat') { - return false; - } - return this._super(); - }, - }, - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/message_action_list.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/message_action_list.js deleted file mode 100644 index 145da94..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/message_action_list.js +++ /dev/null @@ -1,23 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; -import { clear } from '@mail/model/model_field_command'; - -registerPatch({ - name: 'MessageActionList', - fields: { - actionReplyTo: { - compute() { - if ( - this.message && - this.message.originThread && - this.message.originThread.channel && - this.message.originThread.channel.channel_type === 'livechat' - ) { - return clear(); - } - return this._super(); - } - }, - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/message_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/message_view.js deleted file mode 100644 index 76b962b..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/message_view.js +++ /dev/null @@ -1,22 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from "@mail/model/model_core"; - -registerPatch({ - name: "MessageView", - fields: { - hasAuthorClickable: { - compute() { - if ( - this.message && - this.message.originThread && - this.message.originThread.channel && - this.message.originThread.channel.channel_type === "livechat" - ) { - return this.message.author === this.message.originThread.channel.correspondent; - } - return this._super(); - }, - }, - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/messaging.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/messaging.js deleted file mode 100644 index a6efa66..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/messaging.js +++ /dev/null @@ -1,17 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; -import { many } from '@mail/model/model_field'; - -registerPatch({ - name: 'Messaging', - fields: { - /** - * All pinned livechats that are known. - */ - pinnedLivechats: many('Thread', { - inverse: 'messagingAsPinnedLivechat', - readonly: true, - }), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/messaging_initializer.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/messaging_initializer.js deleted file mode 100644 index a8ffec9..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/messaging_initializer.js +++ /dev/null @@ -1,25 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; -import { insert } from '@mail/model/model_field_command'; - -registerPatch({ - name: 'MessagingInitializer', - recordMethods: { - /** - * @override - * @param {Object[]} [param0.channel_livechat=[]] - */ - _initCommands() { - this._super(); - this.messaging.update({ - commands: insert({ - channel_types: ['livechat'], - help: this.env._t("See 15 last visited pages"), - methodName: 'execute_command_history', - name: "history", - }), - }); - }, - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/mobile_messaging_navbar_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/mobile_messaging_navbar_view.js deleted file mode 100644 index e38468d..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/mobile_messaging_navbar_view.js +++ /dev/null @@ -1,22 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; - -registerPatch({ - name: 'MobileMessagingNavbarView', - fields: { - tabs: { - compute() { - const res = this._super(); - if (this.messaging.pinnedLivechats.length > 0) { - return [...res, { - icon: 'fa fa-comments', - id: 'livechat', - label: this.env._t("Livechat"), - }]; - } - return res; - }, - }, - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/notification_list_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/notification_list_view.js deleted file mode 100644 index 7525c26..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/notification_list_view.js +++ /dev/null @@ -1,20 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; - -registerPatch({ - name: 'NotificationListView', - fields: { - filteredChannels: { - compute() { - if (this.filter === 'livechat') { - return this.messaging.models['Channel'].all(channel => - channel.channel_type === 'livechat' && - channel.thread.isPinned - ); - } - return this._super(); - }, - }, - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/partner.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/partner.js deleted file mode 100644 index eb840c9..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/partner.js +++ /dev/null @@ -1,15 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; -import { attr } from '@mail/model/model_field'; - -registerPatch({ - name: 'Partner', - fields: { - /** - * States the specific name of this partner in the context of livechat. - * Either a string or undefined. - */ - user_livechat_username: attr(), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/res_users_settings.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/res_users_settings.js deleted file mode 100644 index b27c712..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/res_users_settings.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; -import { attr } from '@mail/model/model_field'; - -registerPatch({ - name: 'res.users.settings', - fields: { - is_discuss_sidebar_category_livechat_open: attr({ - default: true, - }), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/thread.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/thread.js deleted file mode 100644 index ce587d5..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/models/thread.js +++ /dev/null @@ -1,61 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; -import { one } from '@mail/model/model_field'; -import { clear } from '@mail/model/model_field_command'; - -registerPatch({ - name: 'Thread', - recordMethods: { - /** - * @override - */ - getMemberName(persona) { - if (this.channel && this.channel.channel_type === 'livechat' && persona.partner && persona.partner.user_livechat_username) { - return persona.partner.user_livechat_username; - } - if (this.channel && this.channel.channel_type === 'livechat' && persona.partner && persona.partner.is_public && this.channel.anonymous_name) { - return this.channel.anonymous_name; - } - return this._super(persona); - }, - }, - fields: { - hasInviteFeature: { - compute() { - if (this.channel && this.channel.channel_type === 'livechat') { - return true; - } - return this._super(); - }, - }, - hasMemberListFeature: { - compute() { - if (this.channel && this.channel.channel_type === 'livechat') { - return true; - } - return this._super(); - }, - }, - isChatChannel: { - compute() { - if (this.channel && this.channel.channel_type === 'livechat') { - return true; - } - return this._super(); - }, - }, - /** - * If set, current thread is a livechat. - */ - messagingAsPinnedLivechat: one('Messaging', { - compute() { - if (!this.messaging || !this.channel || this.channel.channel_type !== 'livechat' || !this.isPinned) { - return clear(); - } - return this.messaging; - }, - inverse: 'pinnedLivechats', - }), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public/main.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public/main.js deleted file mode 100644 index eb9ac3f..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public/main.js +++ /dev/null @@ -1,24 +0,0 @@ -/** @odoo-module **/ - -import { publicLivechatService } from '@im_livechat/services/public_livechat_service'; -import { isAvailable, options, serverUrl } from 'im_livechat.loaderData'; - -import { messagingService } from '@mail/services/messaging_service'; -import { makeMessagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env'; - -import { registry } from '@web/core/registry'; - -const messagingValuesService = { - start() { - return { - publicLivechatGlobal: { isAvailable, options, serverUrl }, - }; - } -}; - -const serviceRegistry = registry.category('services'); -serviceRegistry.add('messaging', messagingService); -serviceRegistry.add('messagingValues', messagingValuesService); -serviceRegistry.add('public_livechat_service', publicLivechatService); - -registry.category('wowlToLegacyServiceMappers').add('make_messaging_to_legacy_env', makeMessagingToLegacyEnv); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public/session.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public/session.js deleted file mode 100644 index f963661..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public/session.js +++ /dev/null @@ -1,7 +0,0 @@ -odoo.define('web.session', function (require) { - - const Session = require('web.Session'); - const { serverUrl } = require('im_livechat.loaderData'); - - return new Session(undefined, serverUrl, { use_cors: true }); -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/chatbot.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/chatbot.js deleted file mode 100644 index 91eb3e8..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/chatbot.js +++ /dev/null @@ -1,605 +0,0 @@ -/** @odoo-module **/ - -import { registerModel } from '@mail/model/model_core'; -import { attr, one } from '@mail/model/model_field'; -import { clear, increment } from '@mail/model/model_field_command'; - -import { qweb } from 'web.core'; -import { Markup } from 'web.utils'; - -registerModel({ - name: 'Chatbot', - recordMethods: { - /** - * Add message posted by the bot into the conversation. - * This allows not having to wait for the bus (since we run checks based on messages in the - * conversation, having the result be there immediately eases the process). - * - * It also helps while running test tours since those don't have the bus enabled. - */ - addMessage(message, options) { - message.body = Markup(message.body); - this.messaging.publicLivechatGlobal.livechatButtonView.addMessage(message, options); - if (this.messaging.publicLivechatGlobal.publicLivechat.isFolded || !this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.isAtBottom()) { - this.messaging.publicLivechatGlobal.publicLivechat.update({ unreadCounter: increment() }); - } - - if (!options || !options.skipRenderMessages) { - this.messaging.publicLivechatGlobal.chatWindow.renderMessages(); - } - }, - /** - * Once the script ends, adds a visual element at the end of the chat window allowing to restart - * the whole script. - */ - endScript() { - if ( - this.currentStep && - this.currentStep.data && - this.currentStep.data.conversation_closed - ) { - // don't touch anything if the user has closed the conversation, let the chat window - // handle the display - return; - } - this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_composer_text_field').addClass('d-none'); - this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_livechat_chatbot_main_restart').show(); - this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_livechat_chatbot_end').show(); - this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_livechat_chatbot_restart').one('click', this.messaging.publicLivechatGlobal.livechatButtonView.onChatbotRestartScript); - }, - onKeydownInput() { - if ( - this.currentStep && - this.currentStep.data && - this.currentStep.data.chatbot_step_type === 'free_input_multi' - ) { - this.debouncedAwaitUserInput(); - } - }, - /** - * When the user first interacts with the bot, we want to make sure to actually post the welcome - * messages into the conversation. - * - * Indeed, before that, they are 'virtual' messages that are not tied to mail.messages, see - * #_sendWelcomeChatbotMessage() for more information. - * - * Posting them as real messages allows to have a cleaner model and conversation, that will be - * kept intact when changing page on the website. - * - * It also allows tying any first response / question_selection choice to a chatbot.message - * that has a linked mail.message. - */ - async postWelcomeMessages() { - const welcomeMessages = this.messaging.publicLivechatGlobal.welcomeMessages; - - if (welcomeMessages.length === 0) { - // we already posted the welcome messages, nothing to do - return; - } - - const postedWelcomeMessages = await this.messaging.rpc({ - route: '/chatbot/post_welcome_steps', - params: { - channel_uuid: this.messaging.publicLivechatGlobal.publicLivechat.uuid, - chatbot_script_id: this.scriptId, - }, - }); - - const welcomeMessagesIds = welcomeMessages.map(welcomeMessage => welcomeMessage.id); - this.messaging.publicLivechatGlobal.update({ - messages: this.messaging.publicLivechatGlobal.messages.filter((message) => { - !welcomeMessagesIds.includes(message.id); - }), - }); - - postedWelcomeMessages.reverse(); - postedWelcomeMessages.forEach((message) => { - this.addMessage(message, { - prepend: true, - skipRenderMessages: true, - }); - }); - - this.messaging.publicLivechatGlobal.chatWindow.renderMessages(); - }, - /** - * Processes the step, depending on the current state of the script and the author of the last - * message that was typed into the conversation. - * - * This is a rather complicated process since we have many potential states to handle. - * Here are the detailed possible outcomes: - * - * - Check if the script is finished, and if so end it. - * - * - If a human operator has taken over the conversation - * -> enable the input and let the operator handle the visitor. - * - * - If the received step is of type expecting an input from the user - * - the last message if from the user (he has already answered) - * -> trigger the next step - * - otherwise - * -> enable the input and let the user type - * - * - Otherwise - * - if the step is of type 'question_selection' and we are still waiting for the user to - * select one of the options - * -> don't do anything, wait for the user to click one of the options - * - otherwise - * -> trigger the next step - */ - processStep() { - if (this.shouldEndScript) { - this.endScript(); - } else if ( - this.currentStep.data.chatbot_step_type === 'forward_operator' && - this.currentStep.data.chatbot_operator_found - ) { - this.messaging.publicLivechatGlobal.chatWindow.enableInput(); - } else if (this.isExpectingUserInput) { - if (this.messaging.publicLivechatGlobal.isLastMessageFromCustomer) { - // user has already typed a message in -> trigger next step - this.setIsTyping(); - this.update({ - nextStepTimeout: setTimeout( - this.triggerNextStep, - this.messageDelay, - ), - }); - } else { - this.messaging.publicLivechatGlobal.chatWindow.enableInput(); - } - } else { - let triggerNextStep = true; - if (this.currentStep.data.chatbot_step_type === 'question_selection') { - if (!this.messaging.publicLivechatGlobal.isLastMessageFromCustomer) { - // if there is no last message or if the last message is from the bot - // -> don't trigger the next step, we are waiting for the user to pick an option - triggerNextStep = false; - } - } - - if (triggerNextStep) { - let nextStepDelay = this.messageDelay; - if (this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_livechat_chatbot_typing').length !== 0) { - // special case where we already have a "is typing" message displayed - // can happen when the previous step did not trigger any message posted from the bot - // e.g: previous step was "forward_operator" and no-one is available - // -> in that case, don't wait and trigger the next step immediately - nextStepDelay = 0; - } else { - this.setIsTyping(); - } - - this.update({ - nextStepTimeout: setTimeout( - this.triggerNextStep, - nextStepDelay, - ), - }); - } - } - - if (!this.hasRestartButton) { - this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_livechat_chatbot_main_restart').hide(); - } - }, - /** - * See 'Chatbot/saveSession'. - * - * We retrieve the livechat uuid from the session cookie since the livechat Widget is not yet - * initialized when we restore the chatbot state. - * - * We also clear any older keys that store a previously saved chatbot session. - * (In that case we clear the actual browser's local storage, we don't use the localStorage - * object as it does not allow browsing existing keys, see 'local_storage.js'.) - */ - restoreSession() { - const browserLocalStorage = window.localStorage; - if (browserLocalStorage && browserLocalStorage.length) { - for (let i = 0; i < browserLocalStorage.length; i++) { - const key = browserLocalStorage.key(i); - if (key.startsWith('im_livechat.chatbot.state.uuid_') && key !== this.sessionCookieKey) { - browserLocalStorage.removeItem(key); - } - } - } - const chatbotState = localStorage.getItem(this.sessionCookieKey); - if (chatbotState) { - this.update({ currentStep: { data: this.localStorageState._chatbotCurrentStep } }); - } - }, - /** - * Register current chatbot step state into localStorage to be able to resume if the visitor - * goes to another website page or if he refreshes his page. - * - * (Will not work if the visitor switches browser but his livechat session will not be restored - * anyway in that case, since it's stored into a cookie). - */ - saveSession() { - localStorage.setItem('im_livechat.chatbot.state.uuid_' + this.messaging.publicLivechatGlobal.publicLivechat.uuid, JSON.stringify({ - '_chatbot': this.data, - '_chatbotCurrentStep': this.currentStep.data, - })); - }, - /** - * Adds a small "is typing" animation into the chat window. - * - * @param {boolean} [isWelcomeMessage=false] - */ - setIsTyping(isWelcomeMessage = false) { - if (this.messaging.publicLivechatGlobal.livechatButtonView.isTypingTimeout) { - clearTimeout(this.messaging.publicLivechatGlobal.livechatButtonView.isTypingTimeout); - } - this.messaging.publicLivechatGlobal.chatWindow.disableInput(''); - this.messaging.publicLivechatGlobal.livechatButtonView.update({ - isTypingTimeout: setTimeout( - () => { - if (!this.messaging.publicLivechatGlobal.chatWindow || !this.messaging.publicLivechatGlobal.chatWindow.exists()) { - return; - } - this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_mail_thread_content').append( - $(qweb.render('im_livechat.legacy.chatbot.is_typing_message', { - 'chatbotImageSrc': this.messaging.publicLivechatGlobal.serverUrl + `/im_livechat/operator/${ - this.messaging.publicLivechatGlobal.publicLivechat.operator.id - }/avatar`, - 'chatbotIsTypingImageSrc': this.messaging.publicLivechatGlobal.serverUrl + '/im_livechat/static/src/img/chatbot_is_typing.gif', - 'chatbotName': this.name, - 'isWelcomeMessage': isWelcomeMessage, - })) - ); - this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.scrollToBottom(); - }, - this.messageDelay / 3, - ), - }); - }, - /** - * Triggers the next step of the script by calling the associated route. - * This will receive the next step and call step processing. - */ - async triggerNextStep() { - if (!this.messaging.publicLivechatGlobal.chatWindow || !this.messaging.publicLivechatGlobal.chatWindow.exists()) { - return; - } - let triggerNextStep = true; - if ( - this.currentStep && - this.currentStep.data && - this.currentStep.data.chatbot_step_type === 'question_email' - ) { - triggerNextStep = await this.validateEmail(); - } - - if (!triggerNextStep) { - return; - } - - const nextStep = await this.messaging.rpc({ - route: '/chatbot/step/trigger', - params: { - channel_uuid: this.messaging.publicLivechatGlobal.publicLivechat.uuid, - chatbot_script_id: this.scriptId, - }, - }); - - if (nextStep) { - if (nextStep.chatbot_posted_message) { - this.addMessage(nextStep.chatbot_posted_message); - } - - this.update({ currentStep: { data: nextStep.chatbot_step } }); - - this.processStep(); - } else { - // did not find next step -> end the script - this.currentStep.data.chatbot_step_is_last = true; - this.messaging.publicLivechatGlobal.chatWindow.renderMessages(); - this.endScript(); - } - - this.saveSession(); - - return nextStep; - }, - /** - * A special case is handled for email steps, where we first validate the email (server side) - * and we allow the user to try again in case the format is incorrect. - * - * The validation is made server-side to have the same test when we validate here and when we - * register the answer, but also to easily post a message as the bot ("Sorry, try again..."). - * - * Returns a boolean stating whether the email was valid or not. - */ - async validateEmail() { - let emailValidResult = await this.messaging.rpc({ - route: '/chatbot/step/validate_email', - params: { channel_uuid: this.messaging.publicLivechatGlobal.publicLivechat.uuid }, - }); - - if (emailValidResult.success) { - this.currentStep.data.is_email_valid = true; - this.saveSession(); - - return true; - } else { - // email is not valid, let the user try again - this.messaging.publicLivechatGlobal.chatWindow.enableInput(); - if (emailValidResult.posted_message) { - this.addMessage(emailValidResult.posted_message); - } - - return false; - } - }, - /** - * This method will be transformed into a 'debounced' version (see init). - * - * The purpose is to handle steps of type 'free_input_multi', that will let the user type in - * multiple lines of text before the bot goes to the next step. - * - * Every time a 'keydown' is detected into the input, or every time a message is sent, we call - * this debounced method, which will give the user about 10 seconds to type more text before - * the next step is triggered. - * - * First we check if the last message was sent by the user, to make sure we always let him type - * at least one message before moving on. - */ - awaitUserInput() { - if (this.messaging.publicLivechatGlobal.isLastMessageFromCustomer) { - if (this.shouldEndScript) { - this.endScript(); - } else { - this.setIsTyping(); - this.update({ - nextStepTimeout: setTimeout( - this.triggerNextStep, - this.messageDelay, - ) - }); - } - } - }, - }, - fields: { - awaitUserInputDebounceTime: attr({ - compute() { - return 10000; - }, - }), - data: attr({ - compute() { - if (this.messaging.publicLivechatGlobal.isTestChatbot) { - return this.messaging.publicLivechatGlobal.testChatbotData; - } - if (this.state === 'init') { - return this.messaging.publicLivechatGlobal.rule.chatbot; - } - if (this.state === 'welcome') { - return this.messaging.publicLivechatGlobal.livechatInit.rule.chatbot; - } - if ( - this.state === 'restore_session' && - this.localStorageState - ) { - return this.localStorageState._chatbot; - } - return clear(); - }, - }), - currentStep: one('ChatbotStep', { - inverse: 'chabotOwner', - }), - debouncedAwaitUserInput: attr({ - compute() { - // debounced to let the user type several sentences, see 'Chatbot/awaitUserInput' for details - return _.debounce( - this.awaitUserInput, - this.awaitUserInputDebounceTime, - ); - }, - }), - hasRestartButton: attr({ - /** - * Will display a "Restart script" button in the conversation toolbar. - * - * Side-case: if the conversation has been forwarded to a human operator, we don't want to - * display that restart button. - */ - compute() { - const { publicLivechat } = this.messaging.publicLivechatGlobal; - if (publicLivechat && !publicLivechat.operator) { - return false; - } - if ( - !this.messaging.publicLivechatGlobal.publicLivechat || - !this.messaging.publicLivechatGlobal.publicLivechat.uuid - ) { - return false; - } - if (publicLivechat && !publicLivechat.data.chatbot_script_id) { - return false; - } - return Boolean( - !this.currentStep || - ( - this.currentStep.data.chatbot_step_type !== 'forward_operator' || - !this.currentStep.data.chatbot_operator_found - ) - ); - }, - default: false, - }), - isActive: attr({ - compute() { - if (this.messaging.publicLivechatGlobal.isTestChatbot) { - return true; - } - if (this.messaging.publicLivechatGlobal.rule && this.messaging.publicLivechatGlobal.rule.chatbot) { - return true; - } - if (this.messaging.publicLivechatGlobal.livechatInit && this.messaging.publicLivechatGlobal.livechatInit.rule.chatbot) { - return true; - } - if (this.state === 'welcome') { - return true; - } - if (this.localStorageState) { - return true; - } - return clear(); - }, - default: false, - }), - isExpectingUserInput: attr({ - compute() { - if (!this.currentStep) { - return clear(); - } - return [ - 'question_phone', - 'question_email', - 'free_input_single', - 'free_input_multi', - ].includes(this.currentStep.data.chatbot_step_type); - }, - default: false, - }), - isRedirecting: attr({ - default: false, - }), - lastWelcomeStep: attr({ - compute() { - if (!this.welcomeSteps) { - return clear(); - } - return this.welcomeSteps[this.welcomeSteps.length - 1]; - }, - }), - localStorageState: attr({ - compute() { - if (!this.messaging.publicLivechatGlobal.sessionCookie) { - return clear(); - } - const data = localStorage.getItem(this.sessionCookieKey); - if (!data) { - return clear(); - } - return JSON.parse(data); - }, - }), - name: attr({ - compute() { - if (!this.data) { - return clear(); - } - return this.data.name; - }, - }), - nextStepTimeout: attr(), - messageDelay: attr({ - compute() { - return clear(); - }, - default: 3500, // in milliseconds - }), - publicLivechatGlobalOwner: one('PublicLivechatGlobal', { - identifying: true, - inverse: 'chatbot', - }), - scriptId: attr({ - compute() { - if (!this.data) { - return clear(); - } - return this.data.chatbot_script_id; - }, - }), - serverUrl: attr(), - sessionCookieKey: attr({ - compute() { - if (!this.messaging.publicLivechatGlobal.sessionCookie) { - return clear(); - } - return 'im_livechat.chatbot.state.uuid_' + JSON.parse(this.messaging.publicLivechatGlobal.sessionCookie).uuid; - }, - }), - shouldEndScript: attr({ - /** - * Compute method that checks if the script should be ended or not. - * If the user has closed the conversation -> script has ended. - * - * Otherwise, there are 2 use cases where we want to end the script: - * - * If the current step is the last one AND the conversation was not taken over by a human operator - * 1. AND we expect a user input (or we are on a selection) - * AND the user has already answered - * 2. AND we don't expect a user input - */ - compute() { - if (!this.currentStep) { - return clear(); - } - if (this.currentStep.data.conversation_closed) { - return true; - } - if (this.currentStep.data.chatbot_step_is_last && - (this.currentStep.data.chatbot_step_type !== 'forward_operator' || - !this.currentStep.data.chatbot_operator_found) - ) { - if (this.currentStep.data.chatbot_step_type === 'question_email' - && !this.currentStep.data.is_email_valid - ) { - // email is not (yet) valid, let the user answer / try again - return false; - } else if ( - (this.isExpectingUserInput || - this.currentStep.data.chatbot_step_type === 'question_selection') && - this.messaging.publicLivechatGlobal.messages.length !== 0 - ) { - if (this.messaging.publicLivechatGlobal.lastMessage.authorId !== this.messaging.publicLivechatGlobal.publicLivechat.operator.id) { - // we are on the last step of the script, expect a user input and the user has - // already answered - // -> end the script - return true; - } - } else if (!this.isExpectingUserInput) { - // we are on the last step of the script and we do not expect a user input - // -> end the script - return true; - } - } - return false; - }, - default: false, - }), - state: attr({ - compute() { - if (this.messaging.publicLivechatGlobal.rule && !!this.messaging.publicLivechatGlobal.rule.chatbot) { - return 'init'; - } - if (this.messaging.publicLivechatGlobal.livechatInit && this.messaging.publicLivechatGlobal.livechatInit.rule.chatbot) { - return 'welcome'; - } - if ( - !this.messaging.publicLivechatGlobal.rule && - this.messaging.publicLivechatGlobal.history !== null && - this.messaging.publicLivechatGlobal.history.length !== 0 && - this.sessionCookieKey && - localStorage.getItem(this.sessionCookieKey) - ) { - return 'restore_session'; - } - return clear(); - }, - }), - welcomeMessageTimeout: attr(), - welcomeSteps: attr({ - compute() { - if (!this.data) { - return clear(); - } - return this.data.chatbot_welcome_steps; - }, - }), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/chatbot_step.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/chatbot_step.js deleted file mode 100644 index f759c4f..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/chatbot_step.js +++ /dev/null @@ -1,15 +0,0 @@ -/** @odoo-module **/ - -import { registerModel } from '@mail/model/model_core'; -import { attr, one } from '@mail/model/model_field'; - -registerModel({ - name: 'ChatbotStep', - fields: { - chabotOwner: one('Chatbot', { - identifying: true, - inverse: 'currentStep', - }), - data: attr(), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/livechat_button_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/livechat_button_view.js deleted file mode 100644 index 4fcc1b4..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/livechat_button_view.js +++ /dev/null @@ -1,450 +0,0 @@ -/** @odoo-module **/ - -import { registerModel } from '@mail/model/model_core'; -import { attr, one } from '@mail/model/model_field'; -import { clear } from '@mail/model/model_field_command'; - -import {getCookie, deleteCookie} from 'web.utils.cookies'; - -registerModel({ - name: 'LivechatButtonView', - lifecycleHooks: { - _created() { - this.update({ widget: this.env.services.public_livechat_service.mountLivechatButton() }); - }, - _willDelete() { - this.widget.destroy(); - }, - }, - recordMethods: { - /** - * @param {Object} data - * @param {Object} [options={}] - */ - addMessage(data, options) { - const hasAlreadyMessage = _.some(this.messaging.publicLivechatGlobal.messages, function (msg) { - return data.id === msg.id; - }); - if (hasAlreadyMessage) { - return; - } - const message = this.messaging.models['PublicLivechatMessage'].insert({ - data, - id: data.id, - }); - - if (this.messaging.publicLivechatGlobal.publicLivechat && this.messaging.publicLivechatGlobal.publicLivechat.widget) { - this.messaging.publicLivechatGlobal.publicLivechat.widget.addMessage(message.widget); - } - - if (options && options.prepend) { - this.messaging.publicLivechatGlobal.update({ - messages: [message, ...this.messaging.publicLivechatGlobal.messages], - }); - } else { - this.messaging.publicLivechatGlobal.update({ - messages: [...this.messaging.publicLivechatGlobal.messages, message], - }); - } - }, - askFeedback() { - this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_thread_composer input').prop('disabled', true); - this.messaging.publicLivechatGlobal.update({ feedbackView: {} }); - /** - * When we enter the "ask feedback" process of the chat, we hide some elements that become - * unnecessary and irrelevant (restart / end messages, any text field values, ...). - */ - if ( - this.messaging.publicLivechatGlobal.chatbot.currentStep && - this.messaging.publicLivechatGlobal.chatbot.currentStep.data - ) { - this.messaging.publicLivechatGlobal.chatbot.currentStep.data.conversation_closed = true; - this.messaging.publicLivechatGlobal.chatbot.saveSession(); - } - this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_livechat_chatbot_main_restart').hide(); - this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_livechat_chatbot_end').hide(); - this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_composer_text_field') - .removeClass('d-none') - .val(''); - }, - /** - * Restart the script and then trigger the "next step" (which will be the first of the script - * in this case). - */ - async onChatbotRestartScript(ev) { - this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_composer_text_field').removeClass('d-none'); - this.messaging.publicLivechatGlobal.chatWindow.widget.$('.o_livechat_chatbot_end').hide(); - - if (this.messaging.publicLivechatGlobal.chatbot.nextStepTimeout) { - clearTimeout(this.messaging.publicLivechatGlobal.chatbot.nextStepTimeout); - } - - if (this.messaging.publicLivechatGlobal.chatbot.welcomeMessageTimeout) { - clearTimeout(this.messaging.publicLivechatGlobal.chatbot.welcomeMessageTimeout); - } - if (this.messaging.publicLivechatGlobal.publicLivechat.uuid) { - const postedMessage = await this.messaging.rpc({ - route: '/chatbot/restart', - params: { - channel_uuid: this.messaging.publicLivechatGlobal.publicLivechat.uuid, - chatbot_script_id: this.messaging.publicLivechatGlobal.chatbot.scriptId, - }, - }); - - if (postedMessage) { - this.messaging.publicLivechatGlobal.chatbot.addMessage(postedMessage); - } - } - - this.messaging.publicLivechatGlobal.chatbot.update({ currentStep: clear() }); - this.messaging.publicLivechatGlobal.chatbot.setIsTyping(); - this.messaging.publicLivechatGlobal.chatbot.update({ - nextStepTimeout: setTimeout( - this.messaging.publicLivechatGlobal.chatbot.triggerNextStep, - this.messaging.publicLivechatGlobal.chatbot.messageDelay, - ), - }); - }, - closeChat() { - this.messaging.publicLivechatGlobal.update({ chatWindow: clear() }); - deleteCookie('im_livechat_session'); - }, - openChat() { - if (this.isOpenChatDebounced) { - this.openChatDebounced(); - } else { - this._openChat(); - } - }, - async openChatWindow() { - this.messaging.publicLivechatGlobal.update({ chatWindow: {} }); - await this.messaging.publicLivechatGlobal.chatWindow.widget.appendTo($('body')); - this.messaging.publicLivechatGlobal.chatWindow.widget.adjustPosition(); - this.widget.$el.hide(); - this._openChatWindowChatbot(); - }, - /** - * @param {Object} message - */ - async sendMessage(message) { - if (this.messaging.publicLivechatGlobal.publicLivechat.isTemporary) { - await this.messaging.publicLivechatGlobal.publicLivechat.createLivechatChannel(); - if (!this.messaging.publicLivechatGlobal.publicLivechat.operator) { - return; - } - } - await this._sendMessageChatbotBefore(); - await this._sendMessage(message); - this._sendMessageChatbotAfter(); - }, - async start() { - if (!this.messaging.publicLivechatGlobal.hasWebsiteLivechatFeature) { - this.widget.$el.text(this.buttonText); - } - this.update({ isWidgetMounted: true }); - if (this.messaging.publicLivechatGlobal.history) { - for (const m of this.messaging.publicLivechatGlobal.history) { - this.addMessage(m); - } - await this.openChat(); - } else if (!this.messaging.device.isSmall && this.messaging.publicLivechatGlobal.rule.action === 'auto_popup') { - const autoPopupCookie = getCookie('im_livechat_auto_popup'); - if (!autoPopupCookie || JSON.parse(autoPopupCookie)) { - this.update({ - autoOpenChatTimeout: setTimeout( - this.openChat, - this.messaging.publicLivechatGlobal.rule.auto_popup_timer * 1000, - ), - }); - } - } - if (this.buttonBackgroundColor) { - this.widget.$el.css('background-color', this.buttonBackgroundColor); - } - if (this.buttonTextColor) { - this.widget.$el.css('color', this.buttonTextColor); - } - // If website_event_track installed, put the livechat banner above the PWA banner. - const pwaBannerHeight = $('.o_pwa_install_banner').outerHeight(true); - if (pwaBannerHeight) { - this.widget.$el.css('bottom', pwaBannerHeight + 'px'); - } - }, - /** - * @private - */ - async _openChat() { - if (this.isOpeningChat) { - return; - } - const cookie = decodeURIComponent(getCookie('im_livechat_session')); - let def; - this.update({ isOpeningChat: true }); - clearTimeout(this.autoOpenChatTimeout); - if (cookie) { - def = Promise.resolve(JSON.parse(cookie)); - } else { - // re-initialize messages cache - this.messaging.publicLivechatGlobal.update({ messages: clear() }); - def = this.messaging.rpc({ - route: '/im_livechat/get_session', - params: { - ...this.widget._prepareGetSessionParameters(), - persisted: false, - }, - }, { silent: true }); - } - def.then((livechatData) => { - if (!livechatData || !livechatData.operator_pid) { - try { - this.widget.displayNotification({ - message: this.env._t("No available collaborator, please try again later."), - sticky: true, - }); - } catch (_err) { - /** - * Failure in displaying notification happens when - * notification service doesn't exist, which is the case in - * external lib. We don't want notifications in external - * lib at the moment because they use bootstrap toast and - * we don't want to include boostrap in external lib. - */ - console.warn(this.env._t("No available collaborator, please try again later.")); - } - } else { - this.messaging.publicLivechatGlobal.update({ - publicLivechat: { data: livechatData }, - }); - return this.openChatWindow().then(() => { - if (!this.messaging.publicLivechatGlobal.history) { - this.widget._sendWelcomeMessage(); - } - this.messaging.publicLivechatGlobal.chatWindow.renderMessages(); - this.messaging.publicLivechatGlobal.publicLivechat.updateSessionCookie(); - }); - } - }).then(() => { - this.update({ isOpeningChat: false }); - }).guardedCatch(() => { - this.update({ isOpeningChat: false }); - }); - }, - /** - * Resuming the chatbot script if we are currently running one. - * - * In addition, we register a resize event on the window object to scroll messages to bottom. - * This is done especially for mobile (Android) where the keyboard opens upon focusing the input - * field and shrinks the whole window size. - * Scrolling to the bottom allows the user to see the last messages properly when that happens. - * - * @private - * @override - */ - _openChatWindowChatbot() { - window.addEventListener('resize', () => { - if (this.messaging.publicLivechatGlobal.chatWindow) { - this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.scrollToBottom(); - } - }); - - if ( - this.messaging.publicLivechatGlobal.chatbot.currentStep && - this.messaging.publicLivechatGlobal.chatbot.currentStep.data && - this.messaging.publicLivechatGlobal.messages && - this.messaging.publicLivechatGlobal.messages.length !== 0 - ) { - this.messaging.publicLivechatGlobal.chatbot.processStep(); - } - }, - /** - * @private - * @param {Object} message - */ - async _sendMessage(message) { - this.messaging.publicLivechatGlobal.publicLivechat.widget._notifyMyselfTyping({ typing: false }); - const messageId = await this.messaging.rpc({ - route: '/mail/chat_post', - params: { uuid: this.messaging.publicLivechatGlobal.publicLivechat.uuid, message_content: message.content }, - }); - if (!messageId) { - try { - this.widget.displayNotification({ - message: this.env._t("Session expired... Please refresh and try again."), - sticky: true, - }); - } catch (_err) { - /** - * Failure in displaying notification happens when - * notification service doesn't exist, which is the case - * in external lib. We don't want notifications in - * external lib at the moment because they use bootstrap - * toast and we don't want to include boostrap in - * external lib. - */ - console.warn(this.env._t("Session expired... Please refresh and try again.")); - } - this.closeChat(); - } - this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.scrollToBottom(); - }, - /** - * @private - */ - _sendMessageChatbotAfter() { - if (this.messaging.publicLivechatGlobal.chatbot.isRedirecting) { - return; - } - if ( - this.messaging.publicLivechatGlobal.chatbot.isActive && - this.messaging.publicLivechatGlobal.chatbot.currentStep && - this.messaging.publicLivechatGlobal.chatbot.currentStep.data - ) { - if ( - this.messaging.publicLivechatGlobal.chatbot.currentStep.data.chatbot_step_type === 'forward_operator' && - this.messaging.publicLivechatGlobal.chatbot.currentStep.data.chatbot_operator_found - ) { - return; // operator has taken over the conversation, let them speak - } else if (this.messaging.publicLivechatGlobal.chatbot.currentStep.data.chatbot_step_type === 'free_input_multi') { - this.messaging.publicLivechatGlobal.chatbot.debouncedAwaitUserInput(); - } else if (!this.messaging.publicLivechatGlobal.chatbot.shouldEndScript) { - this.messaging.publicLivechatGlobal.chatbot.setIsTyping(); - this.messaging.publicLivechatGlobal.chatbot.update({ - nextStepTimeout: setTimeout( - this.messaging.publicLivechatGlobal.chatbot.triggerNextStep, - this.messaging.publicLivechatGlobal.chatbot.messageDelay, - ), - }); - } else { - this.messaging.publicLivechatGlobal.chatbot.endScript(); - } - this.messaging.publicLivechatGlobal.chatbot.saveSession(); - } - }, - /** - * When the Customer sends a message, we need to act depending on our current state: - * - If the conversation has been forwarded to an operator - * Then there is nothing to do, we let them speak - * - If we are currently on a 'free_input_multi' step - * Await more user input (see #Chatbot/awaitUserInput for details) - * - Otherwise we continue the script or end it if it's the last step - * - * We also save the current session state. - * Important as this may be the very first interaction with the bot, we need to save right away - * to correctly handle any page redirection / page refresh. - * - * Special side case: if we are currently redirecting to another page (see '_onChatbotOptionClicked') - * we shortcut the process as we are currently moving to a different URL. - * The script will be resumed on the new page (if in the same website domain). - * - * @private - */ - async _sendMessageChatbotBefore() { - if ( - this.messaging.publicLivechatGlobal.chatbot.isActive && - this.messaging.publicLivechatGlobal.chatbot.currentStep && - this.messaging.publicLivechatGlobal.chatbot.currentStep.data - ) { - await this.messaging.publicLivechatGlobal.chatbot.postWelcomeMessages(); - } - }, - }, - fields: { - autoOpenChatTimeout: attr(), - buttonBackgroundColor: attr({ - compute() { - return this.messaging.publicLivechatGlobal.options.button_background_color; - }, - }), - buttonText: attr({ - compute() { - if (this.messaging.publicLivechatGlobal.options.button_text) { - return this.messaging.publicLivechatGlobal.options.button_text; - } - return this.env._t("Chat with one of our collaborators"); - }, - }), - buttonTextColor: attr({ - compute() { - return this.messaging.publicLivechatGlobal.options.button_text_color; - }, - }), - chatbotNextStepTimeout: attr(), - chatbotWelcomeMessageTimeout: attr(), - currentPartnerId: attr({ - compute() { - if (!this.messaging.publicLivechatGlobal.isAvailable) { - return clear(); - } - return this.messaging.publicLivechatGlobal.options.current_partner_id; - }, - }), - defaultMessage: attr({ - compute() { - if (this.messaging.publicLivechatGlobal.options.default_message) { - return this.messaging.publicLivechatGlobal.options.default_message; - } - return this.env._t("How may I help you?"); - }, - }), - defaultUsername: attr({ - compute() { - if (this.messaging.publicLivechatGlobal.options.default_username) { - return this.messaging.publicLivechatGlobal.options.default_username; - } - return this.env._t("Visitor"); - }, - }), - headerBackgroundColor: attr({ - compute() { - return this.messaging.publicLivechatGlobal.options.header_background_color; - }, - }), - inputPlaceholder: attr({ - compute() { - if (this.messaging.publicLivechatGlobal.chatbot.isActive) { - // void the default livechat placeholder in the user input - // as we use it for specific things (e.g: showing "please select an option above") - return clear(); - } - if (this.messaging.publicLivechatGlobal.options.input_placeholder) { - return this.messaging.publicLivechatGlobal.options.input_placeholder; - } - return this.env._t("Ask something ..."); - }, - default: '', - }), - isOpenChatDebounced: attr({ - compute() { - return clear(); - }, - default: true, - }), - isOpeningChat: attr({ - default: false, - }), - isTypingTimeout: attr(), - isWidgetMounted: attr({ - default: false, - }), - openChatDebounced: attr({ - compute() { - return _.debounce(this._openChat, 200, true); - }, - }), - publicLivechatGlobalOwner: one('PublicLivechatGlobal', { - identifying: true, - inverse: 'livechatButtonView', - }), - serverUrl: attr({ - compute() { - return this.messaging.publicLivechatGlobal.serverUrl; - }, - }), - titleColor: attr({ - compute() { - return this.messaging.publicLivechatGlobal.options.title_color; - }, - }), - widget: attr(), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/livechat_operator.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/livechat_operator.js deleted file mode 100644 index 93017f0..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/livechat_operator.js +++ /dev/null @@ -1,14 +0,0 @@ -/** @odoo-module **/ - -import { registerModel } from '@mail/model/model_core'; -import { attr } from '@mail/model/model_field'; - -registerModel({ - name: 'LivechatOperator', - fields: { - id: attr({ - identifying: true, - }), - name: attr(), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/messaging.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/messaging.js deleted file mode 100644 index a8e6a1d..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/messaging.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @odoo-module **/ - -import { registerPatch } from '@mail/model/model_core'; -import { one } from '@mail/model/model_field'; - -registerPatch({ - name: 'Messaging', - fields: { - publicLivechatGlobal: one('PublicLivechatGlobal', { - isCausal: true, - }), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat.js deleted file mode 100644 index b4db544..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat.js +++ /dev/null @@ -1,130 +0,0 @@ -/** @odoo-module **/ - -import PublicLivechat from '@im_livechat/legacy/models/public_livechat'; - -import { registerModel } from '@mail/model/model_core'; -import { attr, one } from '@mail/model/model_field'; -import { clear } from '@mail/model/model_field_command'; - -import { deleteCookie, setCookie } from 'web.utils.cookies'; - -registerModel({ - name: 'PublicLivechat', - lifecycleHooks: { - _created() { - this.update({ - widget: new PublicLivechat(this.messaging, { - parent: this.publicLivechatGlobalOwner.livechatButtonView.widget, - data: this.data, - }), - }); - }, - _willDelete() { - this.widget.destroy(); - }, - }, - recordMethods: { - async createLivechatChannel() { - const livechatData = await this.messaging.rpc({ - route: "/im_livechat/get_session", - params: this.messaging.publicLivechatGlobal.livechatButtonView.widget._prepareGetSessionParameters(), - }); - if (!livechatData || !livechatData.operator_pid) { - this.update({ data: clear() }); - deleteCookie("im_livechat_session"); - this.messaging.publicLivechatGlobal.chatWindow.widget.renderChatWindow(); - } else { - this.update({ data: livechatData }); - this.widget.data = livechatData; - this.updateSessionCookie(); - } - }, - updateSessionCookie() { - deleteCookie("im_livechat_session"); - setCookie( - "im_livechat_session", - encodeURIComponent(JSON.stringify(this.widget.toData()), true), - 60 * 60, - "required" - ); - setCookie("im_livechat_auto_popup", JSON.stringify(false), 60 * 60, "optional"); - if (this.operator) { - const operatorPidId = this.operator.id; - const oneWeek = 7 * 24 * 60 * 60; - setCookie("im_livechat_previous_operator_pid", operatorPidId, oneWeek, "optional"); - } - }, - }, - fields: { - data: attr(), - id: attr({ - compute() { - if (!this.data) { - return clear(); - } - return this.data.id; - }, - }), - isFolded: attr({ - default: false, - }), - isTemporary: attr({ - compute() { - if (!this.data || !this.data.id) { - return true; - } - return false; - }, - }), - publicLivechatGlobalOwner: one('PublicLivechatGlobal', { - identifying: true, - inverse: 'publicLivechat', - }), - name: attr({ - compute() { - if (!this.data || !this.operator) { - return clear(); - } - return this.data.name; - }, - }), - operator: one('LivechatOperator', { - compute() { - if (!this.data) { - return clear(); - } - if (!this.data.operator_pid) { - return clear(); - } - if (!this.data.operator_pid[0]) { - return clear(); - } - return { - id: this.data.operator_pid[0], - name: this.data.operator_pid[1], - }; - }, - }), - status: attr({ - compute() { - if (!this.data) { - return clear(); - } - return this.data.status || ''; - }, - }), - // amount of messages that have not yet been read on this chat - unreadCounter: attr({ - default: 0, - }), - uuid: attr({ - compute() { - if (!this.data) { - return clear(); - } - return this.data.uuid; - }, - }), - widget: attr(), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_feedback_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_feedback_view.js deleted file mode 100644 index 60ae12d..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_feedback_view.js +++ /dev/null @@ -1,42 +0,0 @@ -/** @odoo-module **/ - -import Feedback from '@im_livechat/legacy/widgets/feedback/feedback'; - -import { registerModel } from '@mail/model/model_core'; -import { attr, one } from '@mail/model/model_field'; - -registerModel({ - name: 'PublicLivechatFeedbackView', - lifecycleHooks: { - _created() { - this.update({ - widget: new Feedback( - this.messaging.publicLivechatGlobal.livechatButtonView.widget, - this.messaging, - this.messaging.publicLivechatGlobal.publicLivechat.widget, - ), - }); - this.messaging.publicLivechatGlobal.chatWindow.widget.replaceContentWith(this.widget); - this.widget.on('feedback_sent', null, this._onFeedbackSent); - this.widget.on('send_message', null, this._onSendMessage); - }, - _willDelete() { - this.widget.destroy(); - }, - }, - recordMethods: { - _onFeedbackSent() { - this.messaging.publicLivechatGlobal.livechatButtonView.closeChat(); - }, - _onSendMessage(...args) { - this.messaging.publicLivechatGlobal.livechatButtonView.sendMessage(...args); - }, - }, - fields: { - publicLivechatGlobalOwner: one('PublicLivechatGlobal', { - identifying: true, - inverse: 'feedbackView', - }), - widget: attr(), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_global.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_global.js deleted file mode 100644 index 945b663..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_global.js +++ /dev/null @@ -1,296 +0,0 @@ -/** @odoo-module **/ - -import { registerModel } from '@mail/model/model_core'; -import { attr, many, one } from '@mail/model/model_field'; -import { clear } from '@mail/model/model_field_command'; - -import { session } from "@web/session"; -import legacySession from "web.session"; - -import { qweb } from 'web.core'; -import { Markup } from 'web.utils'; -import {getCookie, setCookie, deleteCookie} from 'web.utils.cookies'; - -registerModel({ - name: 'PublicLivechatGlobal', - lifecycleHooks: { - _created() { - // History tracking - const page = window.location.href.replace(/^.*\/\/[^/]+/, ''); - const pageHistory = getCookie(this.LIVECHAT_COOKIE_HISTORY); - let urlHistory = []; - if (pageHistory) { - urlHistory = JSON.parse(pageHistory) || []; - } - if (!_.contains(urlHistory, page)) { - urlHistory.push(page); - while (urlHistory.length > this.HISTORY_LIMIT) { - urlHistory.shift(); - } - setCookie(this.LIVECHAT_COOKIE_HISTORY, JSON.stringify(urlHistory), 60 * 60 * 24, 'optional'); // 1 day cookie - } - if (this.isAvailable) { - this.willStart(); - } - }, - }, - recordMethods: { - async loadQWebTemplate() { - const templates = await this.messaging.rpc({ route: '/im_livechat/load_templates' }); - for (const template of templates) { - qweb.add_template(template); - } - this.update({ hasLoadedQWebTemplate: true }); - }, - async willStart() { - await this._willStart(); - await this._willStartChatbot(); - }, - async _willStart() { - const strCookie = decodeURIComponent(getCookie('im_livechat_session')); - let isSessionCookieAvailable = Boolean(strCookie); - let cookie = JSON.parse(strCookie || '{}'); - if (isSessionCookieAvailable && (cookie.visitor_uid !== session.user_id || !cookie.id)) { - this.leaveSession(); - isSessionCookieAvailable = false; - cookie = {}; - } - if (cookie.id) { - const history = await this.messaging.rpc({ - route: '/mail/chat_history', - params: { uuid: cookie.uuid, limit: 100 }, - }); - history.reverse(); - this.update({ history }); - for (const message of this.history) { - message.body = Markup(message.body); - } - this.update({ isAvailableForMe: true }); - } else { - const result = await this.messaging.rpc({ - route: '/im_livechat/init', - params: { channel_id: this.channelId }, - }); - if (result.available_for_me) { - this.update({ isAvailableForMe: true }); - } - this.update({ rule: result.rule }); - } - const proms = [this.loadQWebTemplate()]; - if (!session.is_frontend) { - proms.push(legacySession.load_translations(["im_livechat"])); - } - return Promise.all(proms); - }, - /** - * This override handles the following use cases: - * - * - If the chat is started for the first time (first visit of a visitor) - * We register the chatbot configuration and the rest of the behavior is triggered by various - * method overrides ('sendWelcomeMessage', 'sendMessage', ...) - * - * - If the chat has been started before, but the user did not interact with the bot - * In addition, we fetch the configuration (with a '/init' call), to see if we have a bot - * configured. - * Indeed we want to trigger the bot script on every page where the associated rule is matched. - * - * - If we have a non-empty chat history, resume the chat script where the end-user left it by - * fetching the necessary information from the local storage. - * - * @override - */ - async _willStartChatbot() { - if (this.rule) { - // noop - } else if (this.history !== null && this.history.length === 0) { - this.update({ - livechatInit: await this.messaging.rpc({ - route: '/im_livechat/init', - params: { channel_id: this.channelId }, - }), - }); - } else if (this.history !== null && this.history.length !== 0) { - const sessionCookie = decodeURIComponent(getCookie('im_livechat_session')); - if (sessionCookie) { - this.update({ sessionCookie }); - } - } - - if (this.chatbot.state === 'init') { - // we landed on a website page where a channel rule is configured to run a chatbot.script - // -> initialize necessary state - if (this.rule.chatbot_welcome_steps && this.rule.chatbot_welcome_steps.length !== 0) { - this.chatbot.update({ - currentStep: { - data: this.chatbot.lastWelcomeStep, - }, - }); - } - } else if (this.chatbot.state === 'welcome') { - // we landed on a website page and a chatbot script was initialized on a previous one - // however the end-user did not interact with the bot ( :( ) - // -> remove cookie to force opening the popup again - // -> initialize necessary state - // -> batch welcome message (see '_sendWelcomeChatbotMessage') - deleteCookie('im_livechat_auto_popup'); - this.update({ history: clear() }); - this.update({ rule: this.livechatInit.rule }); - } else if (this.chatbot.state === 'restore_session') { - // we landed on a website page and a chatbot script is currently running - // -> restore the user's session (see 'Chatbot/restoreSession') - this.chatbot.restoreSession(); - } - }, - - getVisitorUserId() { - const cookie = JSON.parse(decodeURIComponent(getCookie("im_livechat_session")) || "{}"); - if ("visitor_uid" in cookie) { - return cookie.visitor_uid; - } - return session.user_id; - }, - - /** - * Called when the visitor leaves the livechat chatter the first time (first click on X button) - * this will deactivate the mail_channel, notify operator that visitor has left the channel. - */ - leaveSession() { - const cookie = decodeURIComponent(getCookie('im_livechat_session')); - if (cookie) { - const channel = JSON.parse(cookie); - if (channel.uuid) { - this.messaging.rpc({ route: '/im_livechat/visitor_leave_session', params: { uuid: channel.uuid } }); - } - deleteCookie('im_livechat_session'); - } - }, - }, - fields: { - HISTORY_LIMIT: attr({ - default: 15, - }), - LIVECHAT_COOKIE_HISTORY: attr({ - default: 'im_livechat_history', - }), - RATING_TO_EMOJI: attr({ - default: { - 5: "😊", - 3: "😐", - 1: "😞", - }, - }), - channelId: attr({ - compute() { - return this.options.channel_id; - }, - }), - chatbot: one('Chatbot', { - default: {}, - inverse: 'publicLivechatGlobalOwner', - }), - chatWindow: one('PublicLivechatWindow', { - inverse: 'publicLivechatGlobalOwner', - }), - feedbackView: one('PublicLivechatFeedbackView', { - inverse: 'publicLivechatGlobalOwner', - }), - hasLoadedQWebTemplate: attr({ - default: false, - }), - hasWebsiteLivechatFeature: attr({ - compute() { - return false; - }, - }), - history: attr({ - default: null, - }), - isAvailable: attr({ - default: false, - }), - isAvailableForMe: attr({ - default: false, - }), - isLastMessageFromCustomer: attr({ - /** - * Compares the last message of the conversation to this livechat's operator id. - */ - compute() { - if (!this.lastMessage) { - return clear(); - } - if (!this.publicLivechat) { - return clear(); - } - if (!this.publicLivechat.operator) { - return clear(); - } - return this.lastMessage.authorId !== this.publicLivechat.operator.id; - }, - default: false, - }), - isTestChatbot: attr({ - compute() { - if (!this.options) { - return clear(); - } - return Boolean(this.options.isTestChatbot); - }, - default: false, - }), - lastMessage: one('PublicLivechatMessage', { - compute() { - if (this.messages.length === 0) { - return clear(); - } - return this.messages[this.messages.length - 1]; - }, - }), - livechatButtonView: one('LivechatButtonView', { - compute() { - if (this.isAvailable && (this.isAvailableForMe || this.isTestChatbot) && this.hasLoadedQWebTemplate && this.env.services.public_livechat_service) { - return {}; - } - return clear(); - }, - inverse: 'publicLivechatGlobalOwner', - }), - livechatInit: attr(), - messages: many('PublicLivechatMessage'), - notificationHandler: one('PublicLivechatGlobalNotificationHandler', { - inverse: 'publicLivechatGlobalOwner', - compute() { - if (this.publicLivechat && !this.publicLivechat.isTemporary) { - return {}; - } - return clear(); - } - }), - options: attr({ - default: {}, - }), - publicLivechat: one('PublicLivechat', { - inverse: 'publicLivechatGlobalOwner', - }), - rule: attr(), - serverUrl: attr({ - default: '', - }), - sessionCookie: attr(), - testChatbotData: attr({ - compute() { - if (!this.options) { - return clear(); - } - return this.options.testChatbotData; - }, - }), - welcomeMessages: many('PublicLivechatMessage', { - compute() { - return this.messages.filter((message) => { - return message.id && typeof message.id === 'string' && message.id.startsWith('_welcome_'); - }); - }, - }), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_global_notification_handler.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_global_notification_handler.js deleted file mode 100644 index 04b4b9c..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_global_notification_handler.js +++ /dev/null @@ -1,114 +0,0 @@ -/** @odoo-module **/ - -import { registerModel } from '@mail/model/model_core'; -import { one } from '@mail/model/model_field'; -import { increment } from '@mail/model/model_field_command'; - -import session from 'web.session'; -import utils from 'web.utils'; -import {getCookie} from 'web.utils.cookies'; - -registerModel({ - name: 'PublicLivechatGlobalNotificationHandler', - lifecycleHooks: { - _created() { - this.env.services['bus_service'].addChannel(this.messaging.publicLivechatGlobal.publicLivechat.uuid); - this.env.services['bus_service'].addEventListener('notification', this._onNotification); - }, - }, - recordMethods: { - /** - * @private - * @param {Object} notification - * @param {Object} notification.payload - * @param {string} notification.type - */ - _handleNotification({ payload, type }) { - switch (type) { - case 'im_livechat.history_command': { - if (payload.id !== this.messaging.publicLivechatGlobal.publicLivechat.id) { - return; - } - const cookie = getCookie(this.messaging.publicLivechatGlobal.LIVECHAT_COOKIE_HISTORY); - const history = cookie ? JSON.parse(cookie) : []; - session.rpc('/im_livechat/history', { - pid: this.messaging.publicLivechatGlobal.publicLivechat.operator.id, - channel_uuid: this.messaging.publicLivechatGlobal.publicLivechat.uuid, - page_history: history, - }); - return; - } - case 'mail.channel.member/typing_status': { - if (!this.messaging.publicLivechatGlobal.chatWindow || !this.messaging.publicLivechatGlobal.chatWindow.exists()) { - return; - } - const channelMemberData = payload; - if (channelMemberData.channel.id !== this.messaging.publicLivechatGlobal.publicLivechat.id) { - return; - } - if (!channelMemberData.persona.partner) { - return; - } - if (channelMemberData.persona.partner.id === this.messaging.publicLivechatGlobal.livechatButtonView.currentPartnerId) { - // ignore typing display of current partner. - return; - } - if (channelMemberData.isTyping) { - this.messaging.publicLivechatGlobal.publicLivechat.widget.registerTyping({ partnerID: channelMemberData.persona.partner.id }); - } else { - this.messaging.publicLivechatGlobal.publicLivechat.widget.unregisterTyping({ partnerID: channelMemberData.persona.partner.id }); - } - return; - } - case 'mail.channel/new_message': { - if (!this.messaging.publicLivechatGlobal.chatWindow || !this.messaging.publicLivechatGlobal.chatWindow.exists()) { - return; - } - if (payload.id !== this.messaging.publicLivechatGlobal.publicLivechat.id) { - return; - } - const notificationData = payload.message; - // If message from notif is already in chatter messages, stop handling - if (this.messaging.publicLivechatGlobal.messages.some(message => message.id === notificationData.id)) { - return; - } - notificationData.body = utils.Markup(notificationData.body); - this.messaging.publicLivechatGlobal.livechatButtonView.addMessage(notificationData); - if (this.messaging.publicLivechatGlobal.publicLivechat.isFolded || !this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.isAtBottom()) { - this.messaging.publicLivechatGlobal.publicLivechat.update({ unreadCounter: increment() }); - } - this.messaging.publicLivechatGlobal.chatWindow.renderMessages(); - return; - } - case 'mail.message/insert': { - if (!this.messaging.publicLivechatGlobal.chatWindow || !this.messaging.publicLivechatGlobal.chatWindow.exists()) { - return; - } - const message = this.messaging.publicLivechatGlobal.messages.find(message => message.id === payload.id); - if (!message) { - return; - } - message.widget._body = utils.Markup(payload.body); - this.messaging.publicLivechatGlobal.chatWindow.renderMessages(); - return; - } - } - }, - /** - * @private - * @param {CustomEvent} ev - * @param {Array[]} [ev.detail] Notifications coming from the bus. - */ - _onNotification({ detail: notifications }) { - for (const notification of notifications) { - this._handleNotification(notification); - } - }, - }, - fields: { - publicLivechatGlobalOwner: one('PublicLivechatGlobal', { - identifying: true, - inverse: 'notificationHandler', - }), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_message.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_message.js deleted file mode 100644 index a069612..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_message.js +++ /dev/null @@ -1,34 +0,0 @@ -/** @odoo-module **/ - -import { registerModel } from '@mail/model/model_core'; -import { attr } from '@mail/model/model_field'; -import { clear } from '@mail/model/model_field_command'; - -import PublicLivechatMessage from '@im_livechat/legacy/models/public_livechat_message'; - -registerModel({ - name: 'PublicLivechatMessage', - lifecycleHooks: { - _created() { - this.update({ widget: new PublicLivechatMessage(this.messaging.publicLivechatGlobal.livechatButtonView.widget, this.messaging, this.data) }); - }, - _willDelete() { - this.widget.destroy(); - }, - }, - fields: { - authorId: attr({ - compute() { - if (this.data.author && this.data.author.id) { - return this.data.author.id; - } - return clear(); - }, - }), - data: attr(), - id: attr({ - identifying: true, - }), - widget: attr(), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_view.js deleted file mode 100644 index 4c02df4..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_view.js +++ /dev/null @@ -1,27 +0,0 @@ -/** @odoo-module **/ - -import PublicLivechatView from '@im_livechat/legacy/widgets/public_livechat_view/public_livechat_view'; - -import { registerModel } from '@mail/model/model_core'; -import { attr, one } from '@mail/model/model_field'; - -registerModel({ - name: 'PublicLivechatView', - lifecycleHooks: { - _created() { - this.update({ - widget: new PublicLivechatView(this, this.messaging, { displayMarkAsRead: false }), - }); - }, - _willDelete() { - this.widget.destroy(); - }, - }, - fields: { - publicLivechatWindowOwner: one('PublicLivechatWindow', { - identifying: true, - inverse: 'publicLivechatView', - }), - widget: attr(), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_window.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_window.js deleted file mode 100644 index a5771fb..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/public_models/public_livechat_window.js +++ /dev/null @@ -1,95 +0,0 @@ -/** @odoo-module **/ - -import PublicLivechatWindow from '@im_livechat/legacy/widgets/public_livechat_window/public_livechat_window'; - -import { registerModel } from '@mail/model/model_core'; -import { attr, one } from '@mail/model/model_field'; - -registerModel({ - name: 'PublicLivechatWindow', - lifecycleHooks: { - _created() { - this.update({ - widget: new PublicLivechatWindow( - this.messaging.publicLivechatGlobal.livechatButtonView.widget, - this.messaging, - this.messaging.publicLivechatGlobal.publicLivechat.widget, - ), - }); - }, - _willDelete() { - this.widget.destroy(); - }, - }, - recordMethods: { - enableInput() { - const $composerTextField = this.widget.$('.o_composer_text_field'); - $composerTextField - .prop('disabled', false) - .removeClass('text-center fst-italic bg-200') - .val('') - .focus(); - - $composerTextField.off('keydown', this.messaging.publicLivechatGlobal.chatbot.onKeydownInput); - if (this.messaging.publicLivechatGlobal.chatbot.currentStep.data.chatbot_step_type === 'free_input_multi') { - $composerTextField.on('keydown', this.messaging.publicLivechatGlobal.chatbot.onKeydownInput); - } - }, - /** - * Disable the input allowing the user to type. - * This is typically used when we want to force him to click on one of the chatbot options. - * - * @private - */ - disableInput(disableText) { - this.widget.$('.o_composer_text_field') - .prop('disabled', true) - .addClass('text-center fst-italic bg-200') - .val(disableText); - }, - renderMessages() { - const shouldScroll = !this.isFolded && this.publicLivechatView.widget.isAtBottom(); - this.widget.render(); - if (shouldScroll) { - this.publicLivechatView.widget.scrollToBottom(); - } - const self = this; - - this.widget.$('.o_thread_message:last .o_livechat_chatbot_options li').each(function () { - $(this).on('click', self.messaging.publicLivechatGlobal.livechatButtonView.widget._onChatbotOptionClicked.bind(self.messaging.publicLivechatGlobal.livechatButtonView.widget)); - }); - - this.widget.$('.o_livechat_chatbot_main_restart').on('click', (ev) => { - ev.stopPropagation(); // prevent fold behaviour - this.messaging.publicLivechatGlobal.livechatButtonView.onChatbotRestartScript(ev); - }); - - if (this.messaging.publicLivechatGlobal.messages.length !== 0) { - const lastMessage = this.messaging.publicLivechatGlobal.lastMessage; - const stepAnswers = lastMessage.widget.getChatbotStepAnswers(); - if (stepAnswers && stepAnswers.length !== 0 && !lastMessage.widget.getChatbotStepAnswerId()) { - this.disableInput(this.env._t("Select an option above")); - } - } - }, - }, - fields: { - inputPlaceholder: attr({ - compute() { - if (this.messaging.publicLivechatGlobal.livechatButtonView.inputPlaceholder) { - return this.messaging.publicLivechatGlobal.livechatButtonView.inputPlaceholder; - } - return this.env._t("Say something"); - }, - }), - publicLivechatGlobalOwner: one('PublicLivechatGlobal', { - identifying: true, - inverse: 'chatWindow', - }), - publicLivechatView: one('PublicLivechatView', { - default: {}, - inverse: 'publicLivechatWindowOwner', - }), - widget: attr(), - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/scss/im_livechat_bootstrap.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/scss/im_livechat_bootstrap.scss deleted file mode 100644 index 4b56875..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/scss/im_livechat_bootstrap.scss +++ /dev/null @@ -1,59 +0,0 @@ -.text-muted { - color: map-get($grays, '600'); -} - -.text-start { - text-align: left; -} - -.text-center { - text-align: center; -} - -.o_thread_window { - &,* { - box-sizing: border-box; - } - .o_thread_window_header { - height: 35px; - .fa-close { - text-decoration: none; - font-weight: bold; - &:before { - content: "\00d7"; - font-size: initial; - } - } - > span { - margin: auto 0; - } - } - - .o_email_chat_button:after { - content:' \27A4'; - } -} - -// Bootstrap classnames are not available in external lib -// These are equivalent scss rules so visually it still works. -.o_livechat_chatbot_end { - // @extend .bg-200; - font-style: italic !important; // @extend .fst-italic; - text-align: center !important; // @extend .text-center; - border: 1px solid #dee2e6 !important; // @extend .border; -} - -.o_livechat_chatbot_stepAnswer { - display: inline-block !important; // @extend .d-inline-block; - border: 1px solid #dee2e6 !important; // @extend .border; - border-color: #017e84 !important; // @extend .border-primary; - border-radius: 0.25rem !important; // @extend .rounded; - padding: 0.5rem !important; // @extend .p-2; - margin-right: 1rem !important; // @extend .me-3; - margin-bottom: 0.25rem !important; // @extend .mb-1; - font-weight: 700 !important; // @extend .fw-bold; -} - -.o_livechat_chatbot_options li:not(.disabled):hover { - background-color: #017e84 !important; -} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/scss/im_livechat_form.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/scss/im_livechat_form.scss index 8c319f9..4be9abe 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/scss/im_livechat_form.scss +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/scss/im_livechat_form.scss @@ -13,4 +13,3 @@ margin: 0 5px 0 0; } } - diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/scss/im_livechat_history.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/scss/im_livechat_history.scss index 6356ebd..65c61ae 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/scss/im_livechat_history.scss +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/scss/im_livechat_history.scss @@ -1,18 +1,18 @@ -.o_history_container{ +.o_history_container { table-layout: fixed; width: 100% !important; > tbody > tr > td { padding: 0px !important; } .o_history_kanban_container { - text-align:center; + text-align: center; .o_history_kanban_sub_container { .o_kanban_ungrouped { - max-height:500px; - overflow:auto; - padding:2px 0px; + max-height: 500px; + overflow: auto; + padding: 2px 0px; .rounded-circle { - width:32px; + width: 32px; height: 32px; } .o_kanban_record { @@ -23,16 +23,9 @@ word-break: break-all; } } - .oe_module_vignette { - text-align:left; - } .o_kanban_image { padding-top: 8px; } - .oe_module_desc { - padding: 8px 8px 0px 64px; - } } - } } diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/services/public_livechat_service.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/services/public_livechat_service.js deleted file mode 100644 index fb1f0fd..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/services/public_livechat_service.js +++ /dev/null @@ -1,30 +0,0 @@ -/** @odoo-module **/ - -import LivechatButton from '@im_livechat/legacy/widgets/livechat_button'; - -import rootWidget from 'root.widget'; - -import {getCookie, deleteCookie} from 'web.utils.cookies'; - -export const publicLivechatService = { - dependencies: ['messaging'], - async start(env, { messaging: messagingService }) { - const messaging = await messagingService.get(); - try { - JSON.parse(decodeURIComponent(getCookie('im_livechat_session'))); - } catch { - // Cookies are not supposed to contain non-ASCII characters. - // However, some were set in the past. Let's clean them up. - deleteCookie('im_livechat_session'); - } - return { - mountLivechatButton() { - const livechatButton = new LivechatButton(rootWidget, messaging); - livechatButton.appendTo(document.body).catch(error => { - console.info("Can't load 'LivechatButton' because:", error); - }); - return livechatButton; - }, - }; - }, -}; diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/boolean_phone/boolean_phone.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/boolean_phone/boolean_phone.js new file mode 100644 index 0000000..5d9b985 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/boolean_phone/boolean_phone.js @@ -0,0 +1,13 @@ +import { registry } from "@web/core/registry"; +import { _t } from "@web/core/l10n/translation"; +import { booleanField, BooleanField } from "@web/views/fields/boolean/boolean_field"; + +export class BooleanPhoneField extends BooleanField { + static template = "im_livechat.BooleanPhoneField"; +} + +registry.category("fields").add("boolean_phone", { + ...booleanField, + component: BooleanPhoneField, + displayName: _t("In call"), +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/boolean_phone/boolean_phone.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/boolean_phone/boolean_phone.xml new file mode 100644 index 0000000..e9b6ce3 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/boolean_phone/boolean_phone.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_kanban/discuss_channel_kanban_looking_for_help_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_kanban/discuss_channel_kanban_looking_for_help_view.js new file mode 100644 index 0000000..2d958de --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_kanban/discuss_channel_kanban_looking_for_help_view.js @@ -0,0 +1,20 @@ +import { KanbanController } from "@web/views/kanban/kanban_controller"; +import { kanbanView } from "@web/views/kanban/kanban_view"; +import { registry } from "@web/core/registry"; +import { LivechatLookingForHelpReloadMixin } from "../livechat_looking_for_help_controller_mixin"; + +class DiscussChannelKanbanLookingForHelpController extends LivechatLookingForHelpReloadMixin( + KanbanController +) {} + +const discussChannelLookingForHelpKanbanView = { + ...kanbanView, + Controller: DiscussChannelKanbanLookingForHelpController, +}; + +registry + .category("views") + .add( + "im_livechat.discuss_channel_looking_for_help_kanban", + discussChannelLookingForHelpKanbanView + ); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_kanban/discuss_channel_kanban_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_kanban/discuss_channel_kanban_view.js new file mode 100644 index 0000000..87872c0 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_kanban/discuss_channel_kanban_view.js @@ -0,0 +1,13 @@ +import { KanbanController } from "@web/views/kanban/kanban_controller"; +import { kanbanView } from "@web/views/kanban/kanban_view"; +import { registry } from "@web/core/registry"; +import { LivechatViewControllerMixin } from "../livechat_view_controller_mixin"; + +class DiscussChannelKanbanController extends LivechatViewControllerMixin(KanbanController) {} + +const discussChannelKanbanView = { + ...kanbanView, + Controller: DiscussChannelKanbanController, +}; + +registry.category("views").add("im_livechat.discuss_channel_kanban", discussChannelKanbanView); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_kanban/one2many_names_field.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_kanban/one2many_names_field.js new file mode 100644 index 0000000..250dac8 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_kanban/one2many_names_field.js @@ -0,0 +1,18 @@ +import { formatList } from "@web/core/l10n/utils"; +import { registry } from "@web/core/registry"; +import { ListX2ManyField } from "@web/views/fields/x2many/list_x2many_field"; + +export class One2manyNamesField extends ListX2ManyField { + get formattedValue() { + return formatList( + this.props.record.data[this.props.name].records.map((r) => r.data.display_name) + ); + } +} + +registry.category("fields").add("im_livechat.one2many_names", { + component: One2manyNamesField, + relatedFields() { + return [{ name: "display_name", type: "char" }]; + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_list/discuss_channel_list_looking_for_help_list_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_list/discuss_channel_list_looking_for_help_list_view.js new file mode 100644 index 0000000..adab96c --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_list/discuss_channel_list_looking_for_help_list_view.js @@ -0,0 +1,18 @@ +import { LivechatLookingForHelpReloadMixin } from "@im_livechat/views/livechat_looking_for_help_controller_mixin"; + +import { registry } from "@web/core/registry"; +import { ListController } from "@web/views/list/list_controller"; +import { listView } from "@web/views/list/list_view"; + +class DiscussChannelLookingForHelpListController extends LivechatLookingForHelpReloadMixin( + ListController +) {} + +const discussChannelLookingForHelpListView = { + ...listView, + Controller: DiscussChannelLookingForHelpListController, +}; + +registry + .category("views") + .add("im_livechat.discuss_channel_looking_for_help_list", discussChannelLookingForHelpListView); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_list/discuss_channel_list_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_list/discuss_channel_list_view.js new file mode 100644 index 0000000..8f1366e --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_channel_list/discuss_channel_list_view.js @@ -0,0 +1,13 @@ +import { ListController } from "@web/views/list/list_controller"; +import { LivechatViewControllerMixin } from "../livechat_view_controller_mixin"; +import { listView } from "@web/views/list/list_view"; +import { registry } from "@web/core/registry"; + +class DiscussChannelListController extends LivechatViewControllerMixin(ListController) {} + +const discussChannelListView = { + ...listView, + Controller: DiscussChannelListController, +}; + +registry.category("views").add("im_livechat.discuss_channel_list", discussChannelListView); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_template.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_template.xml new file mode 100644 index 0000000..3b401e2 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/discuss_template.xml @@ -0,0 +1,16 @@ + + + + + + + +
+ No conversation found + +
+
+
+
diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_agent_history_graph.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_agent_history_graph.js new file mode 100644 index 0000000..9f0baec --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_agent_history_graph.js @@ -0,0 +1,9 @@ +import { LivechatGraphRendererMixin } from "@im_livechat/views/lazy/im_livechat_graph_renderer_mixin"; + +import { registry } from "@web/core/registry"; +import { graphView } from "@web/views/graph/graph_view"; + +registry.category("views").add("im_livechat.agent_history_graph", { + ...graphView, + Renderer: LivechatGraphRendererMixin("im_livechat.channel.member.history"), +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_agent_history_pivot.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_agent_history_pivot.js new file mode 100644 index 0000000..10084ee --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_agent_history_pivot.js @@ -0,0 +1,8 @@ +import { registry } from "@web/core/registry"; +import { pivotView } from "@web/views/pivot/pivot_view"; +import { LivechatPivotRendererMixin } from "@im_livechat/views/lazy/im_livechat_pivot_renderer_mixin"; + +registry.category("views").add("im_livechat.agent_history_pivot", { + ...pivotView, + Renderer: LivechatPivotRendererMixin("im_livechat.channel.member.history"), +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_channel_report_graph_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_channel_report_graph_view.js new file mode 100644 index 0000000..37daa97 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_channel_report_graph_view.js @@ -0,0 +1,9 @@ +import { LivechatGraphRendererMixin } from "@im_livechat/views/lazy/im_livechat_graph_renderer_mixin"; + +import { registry } from "@web/core/registry"; +import { graphView } from "@web/views/graph/graph_view"; + +registry.category("views").add("im_livechat.channel_report_graph_views", { + ...graphView, + Renderer: LivechatGraphRendererMixin("im_livechat.report.channel"), +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_channel_report_pivot.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_channel_report_pivot.js new file mode 100644 index 0000000..555d4bd --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_channel_report_pivot.js @@ -0,0 +1,8 @@ +import { registry } from "@web/core/registry"; +import { pivotView } from "@web/views/pivot/pivot_view"; +import { LivechatPivotRendererMixin } from "@im_livechat/views/lazy/im_livechat_pivot_renderer_mixin"; + +registry.category("views").add("im_livechat.report_channel_pivot", { + ...pivotView, + Renderer: LivechatPivotRendererMixin("im_livechat.report.channel"), +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_graph_renderer_mixin.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_graph_renderer_mixin.js new file mode 100644 index 0000000..c05ebb6 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_graph_renderer_mixin.js @@ -0,0 +1,14 @@ +import { GraphRenderer } from "@web/views/graph/graph_renderer"; + +export const LivechatGraphRendererMixin = (model) => + class extends GraphRenderer { + async onGraphClickedFinal(domain) { + const action = this.env.services.orm.call( + model, + "action_open_discuss_channel_view", + [], + { domain } + ); + this.env.services.action.doAction(action); + } + }; diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_pivot_renderer_mixin.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_pivot_renderer_mixin.js new file mode 100644 index 0000000..9af8922 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/lazy/im_livechat_pivot_renderer_mixin.js @@ -0,0 +1,14 @@ +import { PivotRenderer } from "@web/views/pivot/pivot_renderer"; + +export const LivechatPivotRendererMixin = (model) => + class extends PivotRenderer { + async openView(domain) { + const action = this.env.services.orm.call( + model, + "action_open_discuss_channel_view", + [], + { domain } + ); + this.env.services.action.doAction(action); + } + }; diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban.scss new file mode 100644 index 0000000..344d8f1 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban.scss @@ -0,0 +1,8 @@ +.o-livechat-ChannelKanban-highlighted { + background-color: $o-component-active-bg !important; + color: $o-component-active-color; + + &::before { + background-color: $o-component-active-bg !important; + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_menu.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_menu.xml new file mode 100644 index 0000000..23b0ad2 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_menu.xml @@ -0,0 +1,9 @@ + + + + + 'bottom-start' + + + + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_record.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_record.js new file mode 100644 index 0000000..7eaf1e9 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_record.js @@ -0,0 +1,4 @@ +import { KanbanRecord } from "@web/views/kanban/kanban_record"; + +export class LivechatChannelKanbanRecord extends KanbanRecord {} +LivechatChannelKanbanRecord.menuTemplate = "im_livechat.KanbanRecordMenu"; diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_renderer.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_renderer.js new file mode 100644 index 0000000..14485a5 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_renderer.js @@ -0,0 +1,9 @@ +import { KanbanRenderer } from "@web/views/kanban/kanban_renderer"; +import { LivechatChannelKanbanRecord } from "./livechat_channel_kanban_record"; + +export class LivechatChannelKanbanRenderer extends KanbanRenderer { + static components = { + ...KanbanRenderer.components, + KanbanRecord: LivechatChannelKanbanRecord, + }; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_view.js new file mode 100644 index 0000000..8ee153c --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_view.js @@ -0,0 +1,11 @@ +import { LivechatChannelKanbanRenderer } from "@im_livechat/views/livechat_channel_kanban/livechat_channel_kanban_renderer"; + +import { registry } from "@web/core/registry"; +import { kanbanView } from "@web/views/kanban/kanban_view"; + +const livechatChannelKanbanView = { + ...kanbanView, + Renderer: LivechatChannelKanbanRenderer, +}; + +registry.category("views").add("im_livechat.livechat_channel_kanban", livechatChannelKanbanView); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_form_controller.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_form_controller.js new file mode 100644 index 0000000..33af6d7 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_form_controller.js @@ -0,0 +1,18 @@ +import { useService } from "@web/core/utils/hooks"; +import { FormController } from "@web/views/form/form_controller"; + +export class LivechatSessionFormController extends FormController { + setup() { + super.setup(); + this.store = useService("mail.store"); + } + get thread() { + return this.store.Thread.get({ + model: "discuss.channel", + id: this.model.root.resId, + }); + } + displayName() { + return this.thread?.displayName || super.displayName(); + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_form_renderer.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_form_renderer.js new file mode 100644 index 0000000..a1baeba --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_form_renderer.js @@ -0,0 +1,54 @@ +import { Discuss } from "@mail/core/public_web/discuss"; + +import { onWillStart, onWillUpdateProps, useEffect, useState } from "@odoo/owl"; + +import { useService } from "@web/core/utils/hooks"; +import { FormRenderer } from "@web/views/form/form_renderer"; + +export class LivechatSessionFormRenderer extends FormRenderer { + static template = "im_livechat.LivechatDiscuss"; + static components = { + ...FormRenderer.components, + Discuss, + }; + + setup() { + super.setup(); + this.store = useState(useService("mail.store")); + useEffect( + (thread) => { + if (thread) { + thread.shadowedBySelf++; + return () => thread.shadowedBySelf--; + } + }, + () => [this.thread] + ); + onWillStart(() => this.getChannel(this.props)); + onWillUpdateProps(async (nextProps) => { + if (nextProps.record.resId === this.props.record.resId) { + return; + } + await this.getChannel(nextProps); + }); + } + + /** + * Restore the discuss thread according to record id in the props if + * necessary. + * + * @param {Props} props + */ + async getChannel(props) { + this.thread = await this.store.Thread.getOrFetch({ + model: "discuss.channel", + id: props.record.resId, + }); + } + + redirectToSessions() { + this.env.services.action.doAction("im_livechat.discuss_channel_action", { + clearBreadcrumbs: true, + }); + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_form_view.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_form_view.js new file mode 100644 index 0000000..99c4901 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_form_view.js @@ -0,0 +1,13 @@ +import { registry } from "@web/core/registry"; +import { formView } from "@web/views/form/form_view"; +import { LivechatSessionFormController } from "./livechat_form_controller"; +import { LivechatSessionFormRenderer } from "./livechat_form_renderer"; + +export const LivechatSesionFormView = { + ...formView, + Controller: LivechatSessionFormController, + Renderer: LivechatSessionFormRenderer, + display: { controlPanel: false }, +}; + +registry.category("views").add("livechat_session_form", LivechatSesionFormView); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_looking_for_help_controller_mixin.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_looking_for_help_controller_mixin.js new file mode 100644 index 0000000..e2318f2 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_looking_for_help_controller_mixin.js @@ -0,0 +1,60 @@ +import { LivechatViewControllerMixin } from "@im_livechat/views/livechat_view_controller_mixin"; + +import { onWillDestroy } from "@odoo/owl"; + +import { useService } from "@web/core/utils/hooks"; +import { useDebounced } from "@web/core/utils/timing"; + +const RELOAD_DEBOUNCE_DELAY = 300; + +export const LivechatLookingForHelpReloadMixin = (ViewController) => + class extends LivechatViewControllerMixin(ViewController) { + setup() { + super.setup(...arguments); + const busService = useService("bus_service"); + this.reloadDebounced = useDebounced(() => this.model.load(), RELOAD_DEBOUNCE_DELAY); + this.lookingForHelpOnUpdate = this.lookingForHelpOnUpdate.bind(this); + this.tagsOnUpdate = this.tagsOnUpdate.bind(this); + busService.addChannel("im_livechat.looking_for_help"); + busService.subscribe( + "im_livechat.looking_for_help/update", + this.lookingForHelpOnUpdate + ); + busService.subscribe("im_livechat.looking_for_help/tags", this.tagsOnUpdate); + onWillDestroy(() => { + busService.unsubscribe( + "im_livechat.looking_for_help/update", + this.lookingForHelpOnUpdate + ); + busService.unsubscribe("im_livechat.looking_for_help/tags", this.tagsOnUpdate); + busService.deleteChannel("im_livechat.looking_for_help"); + }); + } + + lookingForHelpOnUpdate({ added_channel_ids, removed_channel_ids }) { + const recordIdByResId = {}; + this.model.root.records.forEach((rec) => (recordIdByResId[rec.resId] = rec.id)); + if (added_channel_ids.some((resId) => !recordIdByResId[resId])) { + // Filter/Groups/Search are handled server side, it's easier to reload the data + // rather than trying to guess where/if the record should be inserted. + this.reloadDebounced(); + return; + } + const recordIdsToRemove = removed_channel_ids + .map((resId) => recordIdByResId[resId]) + .filter(Boolean); + if (recordIdsToRemove.length) { + this.model.root._removeRecords(recordIdsToRemove); + } + } + + tagsOnUpdate({ channel_id, tag_ids }) { + const channel = this.model.root.records.find((r) => r.resId === channel_id); + if ( + JSON.stringify(channel?.data.livechat_conversation_tag_ids._currentIds) !== + JSON.stringify(tag_ids) + ) { + channel?.load(); + } + } + }; diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_view_controller_mixin.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_view_controller_mixin.js new file mode 100644 index 0000000..e967a47 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/livechat_view_controller_mixin.js @@ -0,0 +1,23 @@ +import { useService } from "@web/core/utils/hooks"; + +export const LivechatViewControllerMixin = (ViewController) => + class extends ViewController { + setup() { + super.setup(...arguments); + this.store = useService("mail.store"); + this.ui = useService("ui"); + } + + async openRecord(record) { + if (this.ui.isSmall) { + const thread = await this.store.Thread.getOrFetch({ + model: "discuss.channel", + id: record.resId, + }); + if (thread) { + return thread.open(); + } + } + return super.openRecord(record); + } + }; diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/mail_message_form/mail_message_form.scss b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/mail_message_form/mail_message_form.scss new file mode 100644 index 0000000..d071ce7 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/mail_message_form/mail_message_form.scss @@ -0,0 +1,8 @@ +.o-livechat-HistoryKanban-body p { + margin: 0; +} + +.o-livechat-HistoryKanban-authorAvatar { + width: $o-mail-Avatar-size; + height: $o-mail-Avatar-size; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/rating_percentage_widget.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/rating_percentage_widget.js new file mode 100644 index 0000000..0516f5f --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/src/views/rating_percentage_widget.js @@ -0,0 +1,6 @@ +import { registry } from "@web/core/registry"; + +registry.category("formatters").add("im_livechat.rating_percentage", (value) => { + const percentage = (Math.max(value - 1, 0) * 100) / 4; + return Math.round((percentage + Number.EPSILON) * 10) / 10; +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/call.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/call.test.js new file mode 100644 index 0000000..da16dd9 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/call.test.js @@ -0,0 +1,41 @@ +import { + click, + contains, + mockGetMedia, + setupChatHub, + start, + startServer, +} from "@mail/../tests/mail_test_helpers"; + +import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers"; + +import { test } from "@odoo/hoot"; +import { mockDate } from "@odoo/hoot-mock"; + +import { Command, serverState } from "@web/../tests/web_test_helpers"; + +defineLivechatModels(); + +test.tags("desktop"); +test("should display started a call message with operator livechat username", async () => { + mockDate("2025-01-01 12:00:00", +1); + mockGetMedia(); + const pyEnv = await startServer(); + pyEnv["res.partner"].write(serverState.partnerId, { + user_livechat_username: "mitchell boss", + }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + setupChatHub({ opened: [channelId] }); + await start(); + await contains(".o-mail-ChatWindow", { text: "Visitor" }); + await click("[title='Start Call']"); + await contains(".o-mail-NotificationMessage", { text: "mitchell boss started a call.1:00 PM" }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/channel_invite.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/channel_invite.test.js new file mode 100644 index 0000000..db3cb8a --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/channel_invite.test.js @@ -0,0 +1,237 @@ +import { click, contains, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { Command, serverState } from "@web/../tests/web_test_helpers"; +import { defineLivechatModels } from "./livechat_test_helpers"; +import { mockDate } from "@odoo/hoot-mock"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("Can invite a partner to a livechat channel", async () => { + mockDate("2023-01-03 12:00:00", +1); + const pyEnv = await startServer(); + const langIds = pyEnv["res.lang"].create([ + { code: "en", name: "English" }, + { code: "fr", name: "French" }, + { code: "de", name: "German" }, + ]); + const expertiseIds = pyEnv["im_livechat.expertise"].create([ + { name: "pricing" }, + { name: "events" }, + ]); + pyEnv["res.partner"].write([serverState.partnerId], { user_livechat_username: "Mitch (FR)" }); + const userId = pyEnv["res.users"].create({ + name: "James", + livechat_lang_ids: langIds, + livechat_expertise_ids: expertiseIds, + }); + pyEnv["res.partner"].create({ + lang: "en", + name: "James", + user_ids: [userId], + }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 20" }); + const channelId = pyEnv["discuss.channel"].create({ + name: "Visitor 20", + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + last_interest_dt: "2021-01-03 12:00:00", + livechat_member_type: "agent", + }), + Command.create({ + guest_id: guestId, + last_interest_dt: "2021-01-03 12:00:00", + livechat_member_type: "visitor", + }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(channelId); + await contains(".o-livechat-ChannelInfoList"); // wait for auto-open of this panel + await click("button[title='Invite People']"); + await click("input", { + parent: [".o-discuss-ChannelInvitation-selectable", { text: "James" }], + }); + await contains( + ".o-discuss-ChannelInvitation-selectable:contains('James English French German pricing events')" + ); + await click("button:enabled", { text: "Invite" }); + await contains(".o-mail-NotificationMessage", { + text: "Mitch (FR) invited James to the channel1:00 PM", + }); + await contains(".o-discuss-ChannelInvitation", { count: 0 }); + await click("button[title='Members']"); + await contains(".o-discuss-ChannelMember", { text: "James" }); +}); + +test("Available operators come first", async () => { + const pyEnv = await startServer(); + pyEnv["res.partner"].create({ + name: "Harry", + im_status: "offline", + user_ids: [pyEnv["res.users"].create({ name: "Harry" })], + }); + const ronId = pyEnv["res.partner"].create({ + name: "Ron", + im_status: "online", + user_ids: [pyEnv["res.users"].create({ name: "Available operator" })], + }); + pyEnv["im_livechat.channel"].create({ + available_operator_ids: [Command.create({ partner_id: ronId })], + }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor #1" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + }); + await start(); + await openDiscuss(channelId); + await contains(".o-livechat-ChannelInfoList"); // wait for auto-open of this panel + await click("button[title='Invite People']"); + await contains(".o-discuss-ChannelInvitation-selectable", { count: 2 }); + await contains(":nth-child(1 of .o-discuss-ChannelInvitation-selectable)", { text: "Ron" }); + await contains(":nth-child(2 of .o-discuss-ChannelInvitation-selectable)", { text: "Harry" }); +}); + +test("Partners invited most frequently by the current user come first", async () => { + mockDate("2023-01-03 12:00:00"); + const pyEnv = await startServer(); + pyEnv["res.partner"].create({ + name: "John", + im_status: "offline", + user_ids: [pyEnv["res.users"].create({ name: "John" })], + }); + pyEnv["res.partner"].create({ + name: "Albert", + im_status: "offline", + user_ids: [pyEnv["res.users"].create({ name: "Albert" })], + }); + const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor #1" }); + pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + last_interest_dt: "2021-01-03 12:00:00", + livechat_member_type: "agent", + }), + Command.create({ + guest_id: guestId_1, + last_interest_dt: "2021-01-03 12:00:00", + livechat_member_type: "visitor", + }), + ], + livechat_operator_id: serverState.partnerId, + }); + const guestId_2 = pyEnv["mail.guest"].create({ name: "Visitor #2" }); + pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + last_interest_dt: "2021-01-03 11:00:00", + livechat_member_type: "agent", + }), + Command.create({ + guest_id: guestId_2, + last_interest_dt: "2021-01-03 11:00:00", + livechat_member_type: "visitor", + }), + ], + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + await click(".o-mail-DiscussSidebarChannel", { text: "Visitor #1" }); + await contains(".o-livechat-ChannelInfoList"); // wait for auto-open of this panel + await click("button[title='Invite People']"); + await click("input", { parent: [".o-discuss-ChannelInvitation-selectable", { text: "John" }] }); + await click("button:enabled", { text: "Invite" }); + await click(".o-mail-DiscussSidebarChannel", { text: "Visitor #2" }); + await click("button[title='Invite People']"); + await contains(".o-discuss-ChannelInvitation-selectable", { count: 2 }); + await contains(":nth-child(1 of .o-discuss-ChannelInvitation-selectable)", { text: "John" }); + await contains(":nth-child(2 of .o-discuss-ChannelInvitation-selectable)", { text: "Albert" }); +}); + +test("shows operators are in call", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor #1" }); + const [bobPartnerId] = pyEnv["res.partner"].create([ + { name: "bob", user_ids: [Command.create({ name: "bob" })] }, + { name: "john", user_ids: [Command.create({ name: "john" })] }, + ]); + const bobChannelId = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ partner_id: bobPartnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + }); + const [bobMemberId] = pyEnv["discuss.channel.member"].search([ + ["partner_id", "=", bobPartnerId], + ["channel_id", "=", bobChannelId], + ]); + pyEnv["discuss.channel.rtc.session"].create({ + channel_id: bobChannelId, + channel_member_id: bobMemberId, + }); + pyEnv["res.partner"]._compute_is_in_call(); + const channelId = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + }); + await start(); + await openDiscuss(channelId); + await contains(".o-livechat-ChannelInfoList"); // wait for auto-open of this panel + await click("[title='Invite People']"); + await contains(".o-discuss-ChannelInvitation-selectable:contains('bob in a call')"); + await contains(".o-discuss-ChannelInvitation-selectable:contains('john')"); + await contains(".o-discuss-ChannelInvitation-selectable:contains('john in a call')", { + count: 0, + }); +}); + +test("Operator invite shows livechat_username", async () => { + const pyEnv = await startServer(); + pyEnv["res.partner"].create({ + name: "John", + im_status: "offline", + user_ids: [pyEnv["res.users"].create({ name: "John" })], + user_livechat_username: "Johnny", + }); + const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor #1" }); + pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + last_interest_dt: "2021-01-03 12:00:00", + livechat_member_type: "agent", + }), + Command.create({ + guest_id: guestId_1, + last_interest_dt: "2021-01-03 12:00:00", + livechat_member_type: "visitor", + }), + ], + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + await click(".o-mail-DiscussSidebarChannel", { text: "Visitor #1" }); + await contains(".o-livechat-ChannelInfoList"); // wait for auto-open of this panel + await click("button[title='Invite People']"); + await contains("input", { + parent: [".o-discuss-ChannelInvitation-selectable", { text: "Johnny" }], + }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/channel_join_leave.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/channel_join_leave.test.js new file mode 100644 index 0000000..48b5956 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/channel_join_leave.test.js @@ -0,0 +1,151 @@ +import { Command, serverState } from "@web/../tests/web_test_helpers"; +import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers"; +import { + click, + contains, + openDiscuss, + setupChatHub, + start, + startServer, + triggerHotkey, +} from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { withGuest } from "@mail/../tests/mock_server/mail_mock_server"; +import { rpc } from "@web/core/network/rpc"; +import { serializeDate, today } from "@web/core/l10n/dates"; +import { livechatLastAgentLeaveFromChatWindow } from "./im_livechat_shared_tests"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("from the discuss app", async () => { + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { + group_ids: pyEnv["res.groups"] + .search_read([["id", "=", serverState.groupLivechatId]]) + .map(({ id }) => id), + }); + const [guestId_1, guestId_2] = pyEnv["mail.guest"].create([ + { name: "guest_1" }, + { name: "guest_2" }, + ]); + const livechatChannelId = pyEnv["im_livechat.channel"].create({ + name: "HR", + user_ids: [serverState.userId], + }); + pyEnv["discuss.channel"].create([ + { + channel_type: "livechat", + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }), + ], + livechat_end_dt: false, + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + create_uid: serverState.publicUserId, + }, + { + channel_type: "livechat", + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }), + ], + livechat_end_dt: serializeDate(today()), + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + create_uid: serverState.publicUserId, + }, + ]); + await start(); + await openDiscuss(); + await contains( + ".o-mail-DiscussSidebarCategory-livechat:has(:text('HR')) .fa-circle[title='You have joined this live chat channel']" + ); + await click("[title='Leave HR']", { + parent: [".o-mail-DiscussSidebarCategory-livechat", { text: "HR" }], + }); + await contains( + ".o-mail-DiscussSidebarCategory-livechat:has(:text('HR')) .fa-circle[title='You have joined this live chat channel']", + { count: 0 } + ); + await click("[title='Join HR']", { + parent: [".o-mail-DiscussSidebarCategory-livechat", { text: "HR" }], + }); + await contains( + ".o-mail-DiscussSidebarCategory-livechat:has(:text('HR')) .fa-circle[title='You have joined this live chat channel']" + ); + await click("[title='Chat Actions']", { + parent: [".o-mail-DiscussSidebarChannel", { text: "guest_1" }], + }); + await click(".o-dropdown-item:contains('Leave Channel')"); + await click("button:contains(Leave Conversation)"); + await contains(".o-mail-DiscussSidebarChannel", { text: "guest_1", count: 0 }); + await click("[title='Chat Actions']", { + parent: [".o-mail-DiscussSidebarChannel", { text: "guest_2" }], + }); + await click(".o-dropdown-item:contains('Leave Channel')"); + await contains(".o-mail-DiscussSidebarChannel", { text: "guest_2", count: 0 }); + await click("[title='Leave HR']", { + parent: [".o-mail-DiscussSidebarCategory-livechat", { text: "HR" }], + }); + await contains(".o-mail-DiscussSidebarCategory-livechat", { text: "HR", count: 0 }); +}); + +test("from the command palette", async () => { + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { + group_ids: pyEnv["res.groups"] + .search_read([["id", "=", serverState.groupLivechatId]]) + .map(({ id }) => id), + }); + pyEnv["im_livechat.channel"].create({ name: "HR", user_ids: [serverState.userId] }); + await start(); + await triggerHotkey("control+k"); + await click(".o_command", { text: "Leave HR" }); + await contains(".o_notification", { text: "You left HR." }); + await contains(".o_command", { text: "HR", count: 0 }); + await triggerHotkey("control+k"); + await click(".o_command", { text: "Join HR" }); + await contains(".o_notification", { text: "You joined HR." }); +}); + +test("from chat window", livechatLastAgentLeaveFromChatWindow); + +test("visitor leaving ends the livechat conversation", async () => { + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { + group_ids: pyEnv["res.groups"] + .search_read([["id", "=", serverState.groupLivechatId]]) + .map(({ id }) => id), + }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor" }); + const livechatChannelId = pyEnv["im_livechat.channel"].create({ + name: "HR", + user_ids: [serverState.userId], + }); + const channel_id = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + create_uid: serverState.publicUserId, + }); + setupChatHub({ opened: [channel_id] }); + await start(); + await contains(".o-mail-ChatWindow"); + // simulate visitor leaving + await withGuest(guestId, () => rpc("/im_livechat/visitor_leave_session", { channel_id })); + await contains("span", { text: "This livechat conversation has ended" }); + await click("button[title*='Close Chat Window']"); + await contains(".o-mail-ChatWindow", { count: 0 }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/channel_member_list.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/channel_member_list.test.js new file mode 100644 index 0000000..84b9351 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/channel_member_list.test.js @@ -0,0 +1,34 @@ +import { click, contains, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { Command, serverState } from "@web/../tests/web_test_helpers"; +import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("display country in channel member list", async () => { + const pyEnv = await startServer(); + const userId = pyEnv["res.users"].create({ name: "James" }); + pyEnv["res.partner"].create({ + name: "James", + user_ids: [userId], + }); + const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" }); + const guestId = pyEnv["mail.guest"].create({ + name: "Visitor #20", + }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + country_id: countryId, + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(channelId); + await contains(".o-mail-ActionPanel:contains(Information)"); + await click(".o-mail-DiscussContent-header button[name='member-list']"); + await contains(".o-discuss-ChannelMember span", { text: "Belgium", count: 2 }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/chat_hub_compact.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/chat_hub_compact.test.js new file mode 100644 index 0000000..36b1e6c --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/chat_hub_compact.test.js @@ -0,0 +1,46 @@ +import { Command, serverState } from "@web/../tests/web_test_helpers"; +import { click, contains } from "@mail/../tests/mail_test_helpers_contains"; +import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers"; +import { setupChatHub, start, startServer } from "@mail/../tests/mail_test_helpers"; +import { withGuest } from "@mail/../tests/mock_server/mail_mock_server"; +import { describe, test } from "@odoo/hoot"; +import { rpc } from "@web/core/network/rpc"; + +describe.current.tags("desktop"); + +defineLivechatModels(); +test("Do not open chat windows automatically when chat hub is compact", async () => { + const pyEnv = await startServer(); + setupChatHub({ folded: [pyEnv["discuss.channel"].create({ name: "General" })] }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + create_uid: serverState.publicUserId, + }); + await start(); + await click("button[title='Chat Options']"); + await click(".o-dropdown-item", { text: "Hide all conversations" }); + await contains(".o-mail-ChatHub-bubbleBtn .fa-comments"); + await withGuest(guestId, () => + rpc("/mail/message/post", { + post_data: { + body: "I need help!", + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + thread_id: channelId, + thread_model: "discuss.channel", + }) + ); + await contains(".o-mail-ChatHub-bubbleBtn .badge", { text: "1" }); + await click("button.o-mail-ChatHub-bubbleBtn"); + await contains(".o-mail-ChatBubble[name=Visitor] .badge", { text: "1" }); + await contains(".o-mail-ChatWindow", { count: 0, text: "Visitor" }); + await click(".o-mail-ChatBubble[name=Visitor] .o-mail-ChatHub-bubbleBtn"); + await contains(".o-mail-ChatWindow", { text: "Visitor" }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/chat_window_patch.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/chat_window_patch.test.js new file mode 100644 index 0000000..bbd1c80 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/chat_window_patch.test.js @@ -0,0 +1,214 @@ +import { + click, + contains, + openDiscuss, + openFormView, + setupChatHub, + start, + startServer, +} from "@mail/../tests/mail_test_helpers"; +import { withGuest } from "@mail/../tests/mock_server/mail_mock_server"; +import { test } from "@odoo/hoot"; +import { animationFrame } from "@odoo/hoot-mock"; +import { Command, serverState } from "@web/../tests/web_test_helpers"; +import { rpc } from "@web/core/network/rpc"; +import { defineLivechatModels } from "./livechat_test_helpers"; +import { serializeDate, today } from "@web/core/l10n/dates"; + +defineLivechatModels(); + +test.tags("mobile"); +test("can fold livechat chat windows in mobile", async () => { + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({ name: "Visitor" }); + pyEnv["res.users"].create([{ partner_id: partnerId }]); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ + unpin_dt: false, + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ partner_id: partnerId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await click(".o_menu_systray i[aria-label='Messages']"); + await click(".o-mail-NotificationItem", { text: "Visitor" }); + await click(".o-mail-ChatWindow-header [title*='Fold']", { + parent: [".o-mail-ChatWindow", { text: "Visitor" }], + }); + await contains(".o-mail-ChatBubble"); +}); + +test.tags("desktop"); +test("closing a chat window with no message from admin side unpins it", async () => { + const pyEnv = await startServer(); + const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([ + { name: "Partner 1" }, + { name: "Partner 2" }, + ]); + pyEnv["res.users"].create([{ partner_id: partnerId_1 }, { partner_id: partnerId_2 }]); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ + unpin_dt: false, + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ partner_id: partnerId_1, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ + unpin_dt: false, + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ partner_id: partnerId_2, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_end_dt: serializeDate(today()), + livechat_operator_id: serverState.partnerId, + }); + await start(); + await click(".o_menu_systray i[aria-label='Messages']"); + await click(".o-mail-NotificationItem", { text: "Partner 2" }); + await click(".o-mail-ChatWindow-header [title*='Close Chat Window']", { + parent: [".o-mail-ChatWindow", { text: "Partner 2" }], + }); + await openDiscuss(); + await contains(".o-mail-DiscussSidebarChannel", { text: "Partner 1" }); + await contains(".o-mail-DiscussSidebarChannel", { count: 0, text: "Partner 2" }); +}); + +test.tags("desktop", "focus required"); +test("Focus should not be stolen when a new livechat open", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 12" }); + const channelIds = pyEnv["discuss.channel"].create([ + { name: "general" }, + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + }, + ]); + await start(); + await click(".o_menu_systray i[aria-label='Messages']"); + await click(".o-mail-NotificationItem", { text: "general" }); + await contains(".o-mail-ChatWindow", { text: "general" }); + await contains(".o-mail-Composer-input[placeholder='Message #general…']:focus"); + withGuest(guestId, () => + rpc("/mail/message/post", { + post_data: { + body: "hu", + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + thread_id: channelIds[1], + thread_model: "discuss.channel", + }) + ); + await contains(".o-mail-ChatWindow", { text: "Visitor 12" }); + await animationFrame(); + await contains(".o-mail-Composer-input[placeholder='Message #general…']:focus"); +}); + +test("do not ask confirmation if other operators are present", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor #12" }); + const otherOperatorId = pyEnv["res.partner"].create({ name: "John" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + Command.create({ partner_id: otherOperatorId, livechat_member_type: "agent" }), + ], + livechat_operator_id: serverState.partnerId, + channel_type: "livechat", + }); + setupChatHub({ opened: [channelId] }); + await start(); + await contains(".o-mail-ChatWindow"); + await click("[title*='Close Chat Window']"); + await contains(".o-mail-ChatWindow", { count: 0 }); +}); + +test.tags("desktop"); +test("Show livechats with new message in chat hub even when in discuss app)", async () => { + // Chat hub show conversations with new message only when outside of discuss app by default. + // Live chats are special in that agents are expected to see their ongoing conversations at all + // time. Closing chat window ends the conversation. Hence the livechat always are shown on chat hub. + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const [livechatId, channelId] = pyEnv["discuss.channel"].create([ + { + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId }), + Command.create({ guest_id: guestId }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }, + { + channel_member_ids: [Command.create({ partner_id: serverState.partnerId })], + channel_type: "channel", + name: "general", + }, + ]); + pyEnv["mail.message"].create({ + author_id: serverState.partnerId, + body: "

Test

", + message_type: "comment", + model: "discuss.channel", + res_id: channelId, + }); + await start(); + await openDiscuss(channelId); + await contains(".o-mail-Message:contains('Test')"); + // simulate livechat visitor sending a message + await withGuest(guestId, () => + rpc("/mail/message/post", { + post_data: { + body: "Hello, I need help!", + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + thread_id: livechatId, + thread_model: "discuss.channel", + }) + ); + await contains(".o-mail-DiscussSidebar-item:contains('Visitor 11') .badge", { text: "1" }); + await openFormView("res.partner", serverState.partnerId); + await contains(".o-mail-ChatWindow-header:contains('Visitor 11')"); +}); + +test("livechat: non-member can close immediately", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor ABC" }); + const PartnerId = pyEnv["res.partner"].create({ name: "Agent" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: PartnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + livechat_operator_id: PartnerId, + channel_type: "livechat", + }); + await start(); + setupChatHub({ opened: [channelId] }); + await contains(".o-mail-ChatWindow"); + await click("[title*='Close Chat Window']"); + await contains(".o-mail-ChatWindow", { count: 0 }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/composer_patch.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/composer_patch.test.js new file mode 100644 index 0000000..9e23f4d --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/composer_patch.test.js @@ -0,0 +1,70 @@ +import { + click, + contains, + insertText, + openDiscuss, + start, + startServer, +} from "@mail/../tests/mail_test_helpers"; +import { withGuest } from "@mail/../tests/mock_server/mail_mock_server"; +import { describe, test } from "@odoo/hoot"; +import { + asyncStep, + Command, + onRpc, + serverState, + waitForSteps, +} from "@web/../tests/web_test_helpers"; +import { defineLivechatModels } from "./livechat_test_helpers"; + +import { rpc } from "@web/core/network/rpc"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("Can execute help command on livechat channels", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + onRpc("discuss.channel", "execute_command_help", () => { + asyncStep("execute_command_help"); + return true; + }); + await start(); + await openDiscuss(channelId); + await insertText(".o-mail-Composer-input", "/help"); + await click(".o-mail-Composer button[title='Send']:enabled"); + await waitForSteps(["execute_command_help"]); +}); + +test('Receives visitor typing status "is typing"', async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 20" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(channelId); + await contains(".o-discuss-Typing", { text: "" }); + const channel = pyEnv["discuss.channel"].search_read([["id", "=", channelId]])[0]; + // simulate receive typing notification from livechat visitor "is typing" + withGuest(guestId, () => + rpc("/discuss/channel/notify_typing", { + is_typing: true, + channel_id: channel.id, + }) + ); + await contains(".o-discuss-Typing", { text: "Visitor 20 is typing..." }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/discuss_patch.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/discuss_patch.test.js new file mode 100644 index 0000000..8720e5e --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/discuss_patch.test.js @@ -0,0 +1,165 @@ +import { + click, + contains, + insertText, + openDiscuss, + start, + startServer, +} from "@mail/../tests/mail_test_helpers"; +import { withGuest } from "@mail/../tests/mock_server/mail_mock_server"; +import { describe, test } from "@odoo/hoot"; +import { mockDate } from "@odoo/hoot-mock"; +import { Command, serverState } from "@web/../tests/web_test_helpers"; + +import { rpc } from "@web/core/network/rpc"; +import { defineLivechatModels } from "./livechat_test_helpers"; +import { press } from "@odoo/hoot-dom"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("add livechat in the sidebar on visitor sending first message", async () => { + mockDate("2023-01-03 12:00:00"); // so that it's after last interest (mock server is in 2019 by default!) + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { im_status: "online" }); + const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" }); + const livechatChannelId = pyEnv["im_livechat.channel"].create({ + user_ids: [serverState.userId], + }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ + unpin_dt: "2021-01-01 12:00:00", + last_interest_dt: "2021-01-01 10:00:00", + livechat_member_type: "agent", + partner_id: serverState.partnerId, + }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + country_id: countryId, + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + await contains(".o-mail-DiscussSidebar"); + // simulate livechat visitor sending a message + withGuest(guestId, () => + rpc("/mail/message/post", { + post_data: { + body: "Hello, I need help!", + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + thread_id: channelId, + thread_model: "discuss.channel", + }) + ); + await contains( + ".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container", + { + text: "Visitor (Belgium)", + } + ); +}); + +test("invite button should be present on livechat", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(channelId); + await contains(".o-mail-Discuss button[title='Invite People']"); +}); + +test("livechats are sorted by last activity time in the sidebar: most recent at the top", async () => { + mockDate("2023-01-03 12:00:00"); // so that it's after last interest (mock server is in 2019 by default!) + const pyEnv = await startServer(); + const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const guestId_2 = pyEnv["mail.guest"].create({ name: "Visitor 12" }); + pyEnv["discuss.channel"].create([ + { + channel_member_ids: [ + Command.create({ + last_interest_dt: "2021-01-01 10:00:00", + livechat_member_type: "agent", + partner_id: serverState.partnerId, + }), + Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }, + { + channel_member_ids: [ + Command.create({ + last_interest_dt: "2021-02-01 10:00:00", + livechat_member_type: "agent", + partner_id: serverState.partnerId, + }), + Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }, + ]); + await start(); + await openDiscuss(); + await contains(".o-mail-DiscussSidebarChannel", { count: 2 }); + await contains(":nth-child(1 of .o-mail-DiscussSidebarChannel-container)", { + text: "Visitor 12", + }); + await click(".o-mail-DiscussSidebarChannel", { text: "Visitor 11" }); + await insertText(".o-mail-Composer-input", "Blabla"); + await press("Enter"); + await contains(":nth-child(1 of .o-mail-DiscussSidebarChannel-container)", { + text: "Visitor 11", + }); + await contains(":nth-child(2 of .o-mail-DiscussSidebarChannel-container)", { + text: "Visitor 12", + }); +}); + +test("sidebar search finds livechats", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + await click("input[placeholder='Search conversations']"); + await click("a", { text: "Visitor 11" }); + await contains(".o-mail-DiscussContent-threadName[title='Visitor 11']"); +}); + +test("open visitor's partner profile if visitor has one", async () => { + const pyEnv = await startServer(); + const livechatPartner = pyEnv["res.partner"].create({ name: "Joel Willis" }); + const channel = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ partner_id: livechatPartner, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(channel); + await click("a[title='View Contact']"); + await contains("div.o_field_widget > input:value(Joel Willis)"); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/autopopup.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/autopopup.test.js new file mode 100644 index 0000000..b5bf7e1 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/autopopup.test.js @@ -0,0 +1,52 @@ +import { + defineLivechatModels, + loadDefaultEmbedConfig, +} from "@im_livechat/../tests/livechat_test_helpers"; +import { contains, setupChatHub, start, startServer } from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { Command, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers"; +import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("persisted session", async () => { + const pyEnv = await startServer(); + const livechatChannelId = await loadDefaultEmbedConfig(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId }), + Command.create({ guest_id: guestId }), + ], + channel_type: "livechat", + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + }); + setupChatHub({ opened: [channelId] }); + await start({ + authenticateAs: { ...pyEnv["mail.guest"].read(guestId)[0], _name: "mail.guest" }, + }); + await contains(".o-mail-ChatWindow"); +}); + +test("rule received in init", async () => { + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + const autopopupRuleId = pyEnv["im_livechat.channel.rule"].create({ + auto_popup_timer: 0, + action: "auto_popup", + }); + patchWithCleanup(mailDataHelpers, { + _process_request_for_all(store) { + super._process_request_for_all(...arguments); + store.add(pyEnv["im_livechat.channel.rule"].browse(autopopupRuleId), { + action: "auto_popup", + auto_popup_timer: 0, + }); + store.add({ livechat_rule: autopopupRuleId }); + }, + }); + await start({ authenticateAs: false }); + await contains(".o-mail-ChatWindow"); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/chat_bubble.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/chat_bubble.test.js new file mode 100644 index 0000000..31e2c23 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/chat_bubble.test.js @@ -0,0 +1,38 @@ +import { + defineLivechatModels, + loadDefaultEmbedConfig, +} from "@im_livechat/../tests/livechat_test_helpers"; +import { contains, setupChatHub, start, startServer } from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { Command, makeMockEnv, serverState } from "@web/../tests/web_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("Do not show bot IM status", async () => { + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + await makeMockEnv({ embedLivechat: true }); + const partnerId1 = pyEnv["res.partner"].create({ name: "Mitchell", im_status: "online" }); + pyEnv["res.users"].create({ partner_id: partnerId1 }); + const channelId1 = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: partnerId1, livechat_member_type: "visitor" }), + ], + channel_type: "chat", + }); + const partnerId2 = pyEnv["res.partner"].create({ name: "Dummy" }); + const channelId2 = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "visitor" }), + Command.create({ partner_id: partnerId2, livechat_member_type: "bot" }), + ], + channel_type: "livechat", + livechat_operator_id: partnerId2, + }); + setupChatHub({ folded: [channelId1, channelId2] }); + await start({ authenticateAs: false }); + await contains(".o-mail-ChatBubble[name='Mitchell'] .o-mail-ImStatus"); + await contains(".o-mail-ChatBubble[name='Dummy']"); + await contains(".o-mail-ChatBubble[name='Dummy'] .o-mail-ImStatus", { count: 0 }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/chat_window.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/chat_window.test.js new file mode 100644 index 0000000..fa2a8d0 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/chat_window.test.js @@ -0,0 +1,183 @@ +import { + defineLivechatModels, + loadDefaultEmbedConfig, +} from "@im_livechat/../tests/livechat_test_helpers"; +import { + assertChatBubbleAndWindowImStatus, + click, + contains, + inputFiles, + insertText, + mockGetMedia, + onRpcBefore, + start, + startServer, + triggerHotkey, +} from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { asyncStep, serverState, waitForSteps, withUser } from "@web/../tests/web_test_helpers"; + +import { deserializeDateTime } from "@web/core/l10n/dates"; +import { rpc } from "@web/core/network/rpc"; +import { getOrigin } from "@web/core/utils/urls"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("internal users can upload file to temporary thread", async () => { + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + const [partnerUser] = pyEnv["res.users"].search_read([["id", "=", serverState.partnerId]]); + await start({ authenticateAs: partnerUser }); + await click(".o-livechat-LivechatButton"); + const file = new File(["hello, world"], "text.txt", { type: "text/plain" }); + await contains(".o-mail-Composer"); + await click(".o-mail-Composer button[title='More Actions']"); + await contains(".dropdown-item:contains('Attach files')"); + await inputFiles(".o-mail-Composer .o_input_file", [file]); + await contains(".o-mail-AttachmentContainer:not(.o-isUploading):contains(text.txt) .fa-check"); + await triggerHotkey("Enter"); + await contains(".o-mail-Message .o-mail-AttachmentContainer:contains(text.txt)"); +}); + +test("Conversation name is operator livechat user name", async () => { + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + pyEnv["res.partner"].write(serverState.partnerId, { user_livechat_username: "MitchellOp" }); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-ChatWindow-header", { text: "MitchellOp" }); +}); + +test("Portal users should not be able to start a call", async () => { + mockGetMedia(); + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + const joelUid = pyEnv["res.users"].create({ + name: "Joel", + share: true, + login: "joel", + password: "joel", + }); + const joelPid = pyEnv["res.partner"].create({ + name: "Joel", + user_ids: [joelUid], + }); + pyEnv["res.partner"].write(serverState.partnerId, { user_livechat_username: "MitchellOp" }); + await start({ authenticateAs: { login: "joel", password: "joel" } }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-ChatWindow-header:text('MitchellOp')"); + await insertText(".o-mail-Composer-input", "Hello MitchellOp!"); + await triggerHotkey("Enter"); + await contains(".o-mail-Message[data-persistent]:contains('Hello MitchellOp!')"); + await contains(".o-mail-ChatWindow-header .o-mail-ActionList-button", { count: 2 }); + await contains(".o-mail-ChatWindow-header .o-mail-ActionList-button[title='Fold']"); + await contains(".o-mail-ChatWindow-header .o-mail-ActionList-button[title*='Close']"); + await contains(".o-discuss-Call", { count: 0 }); + // simulate operator starts call + const [channelId] = pyEnv["discuss.channel"].search([ + ["channel_type", "=", "livechat"], + [ + "channel_member_ids", + "in", + pyEnv["discuss.channel.member"].search([["partner_id", "=", joelPid]]), + ], + ]); + await withUser(serverState.userId, () => + rpc("/mail/rtc/channel/join_call", { channel_id: channelId }, { silent: true }) + ); + await contains(".o-discuss-Call button", { count: 2 }); + await contains(".o-discuss-Call button[title='Join Video Call']"); + await contains(".o-discuss-Call button[title='Join Call']"); + // still same actions in header + await contains(".o-mail-ChatWindow-header .o-mail-ActionList-button", { count: 2 }); + await contains(".o-mail-ChatWindow-header .o-mail-ActionList-button[title='Fold']"); + await contains(".o-mail-ChatWindow-header .o-mail-ActionList-button[title*='Close']"); +}); + +test("avatar url contains access token for non-internal users", async () => { + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + pyEnv["res.partner"].write(serverState.partnerId, { user_livechat_username: "MitchellOp" }); + const [partner] = pyEnv["res.partner"].search_read([["id", "=", serverState.partnerId]]); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains( + `.o-mail-ChatWindow-threadAvatar img[data-src="${getOrigin()}/web/image/res.partner/${ + partner.id + }/avatar_128?access_token=${partner.id}&unique=${ + deserializeDateTime(partner.write_date).ts + }"]` + ); + await contains( + `.o-mail-Message-avatar[data-src="${getOrigin()}/web/image/res.partner/${ + partner.id + }/avatar_128?access_token=${partner.id}&unique=${ + deserializeDateTime(partner.write_date).ts + }"]` + ); + await insertText(".o-mail-Composer-input", "Hello World!"); + triggerHotkey("Enter"); + const guestId = pyEnv.cookie.get("dgid"); + const [guest] = pyEnv["mail.guest"].read(guestId); + await contains( + `.o-mail-Message-avatar[data-src="${getOrigin()}/web/image/mail.guest/${ + guest.id + }/avatar_128?access_token=${guest.id}&unique=${deserializeDateTime(guest.write_date).ts}"]` + ); +}); + +test("can close confirm livechat with keyboard", async () => { + await startServer(); + await loadDefaultEmbedConfig(); + onRpcBefore((route) => { + if (route === "/im_livechat/visitor_leave_session") { + asyncStep(route); + } + }); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-ChatWindow"); + await insertText(".o-mail-Composer-input", "Hello"); + await triggerHotkey("Enter"); + await contains(".o-mail-Thread:not([data-transient])"); + await triggerHotkey("Escape"); + await contains(".o-livechat-CloseConfirmation", { + text: "Leaving will end the live chat. Do you want to proceed?", + }); + await triggerHotkey("Escape"); + await contains(".o-livechat-CloseConfirmation", { count: 0 }); + await triggerHotkey("Escape"); + await contains(".o-livechat-CloseConfirmation", { + text: "Leaving will end the live chat. Do you want to proceed?", + }); + await triggerHotkey("Enter"); + await waitForSteps(["/im_livechat/visitor_leave_session"]); + await contains(".o-mail-ChatWindow", { text: "Did we correctly answer your question?" }); +}); + +test("Should not show IM status of agents", async () => { + mockGetMedia(); + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + const joelUid = pyEnv["res.users"].create({ + name: "Joel", + share: true, + login: "joel", + password: "joel", + }); + pyEnv["res.partner"].create({ name: "Joel", user_ids: [joelUid] }); + pyEnv["res.partner"].write(serverState.partnerId, { + im_status: "online", + user_livechat_username: "MitchellOp", + }); + await start({ authenticateAs: { login: "joel", password: "joel" } }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-ChatWindow-header:text('MitchellOp')"); + await insertText(".o-mail-Composer-input", "Hello MitchellOp!"); + await triggerHotkey("Enter"); + await contains(".o-mail-Message[data-persistent]:contains('Hello MitchellOp!')"); + await click(".o-mail-ChatWindow-header"); + await contains(".o-mail-ChatBubble"); + await assertChatBubbleAndWindowImStatus("MitchellOp", 0); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/expirable_storage.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/expirable_storage.test.js new file mode 100644 index 0000000..73116da --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/expirable_storage.test.js @@ -0,0 +1,35 @@ +import { expirableStorage } from "@im_livechat/core/common/expirable_storage"; + +import { describe, expect, test } from "@odoo/hoot"; +import { mockDate } from "@odoo/hoot-mock"; +import { asyncStep, waitForSteps } from "@web/../tests/web_test_helpers"; + +describe.current.tags("desktop"); + +test("value is removed from expirable storage after expiration", () => { + mockDate("2023-01-01 00:00:00"); + const ONE_DAY = 60 * 60 * 24; + expirableStorage.setItem("foo", "bar", ONE_DAY); + expect(expirableStorage.getItem("foo")).toBe("bar"); + mockDate("2023-01-01 23:00:00"); + expect(expirableStorage.getItem("foo")).toBe("bar"); + mockDate("2023-01-02 00:00:01"); + expect(expirableStorage.getItem("foo")).toBe(null); +}); + +test("subscribe/unsubscribe to storage changes", async () => { + const fooCallback = (value) => asyncStep(`foo - ${value}`); + const barCallback = (value) => asyncStep(`bar - ${value}`); + expirableStorage.onChange("foo", fooCallback); + expirableStorage.onChange("bar", barCallback); + expirableStorage.setItem("foo", 1); + await waitForSteps(["foo - 1"]); + expirableStorage.setItem("bar", 2); + await waitForSteps(["bar - 2"]); + expirableStorage.removeItem("foo"); + await waitForSteps(["foo - null"]); + expirableStorage.offChange("foo", fooCallback); + expirableStorage.setItem("foo", 3); + expirableStorage.removeItem("bar"); + await waitForSteps(["bar - null"]); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/feedback_panel.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/feedback_panel.test.js new file mode 100644 index 0000000..5d133e8 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/feedback_panel.test.js @@ -0,0 +1,173 @@ +import { + defineLivechatModels, + loadDefaultEmbedConfig, +} from "@im_livechat/../tests/livechat_test_helpers"; +import { RATING } from "@im_livechat/embed/common/livechat_service"; +import { + click, + contains, + insertText, + onRpcBefore, + start, + startServer, + triggerHotkey, +} from "@mail/../tests/mail_test_helpers"; +import { expect, test } from "@odoo/hoot"; +import { + asyncStep, + Command, + getService, + patchWithCleanup, + serverState, + waitForSteps, + withUser, +} from "@web/../tests/web_test_helpers"; + +defineLivechatModels(); + +test("Do not ask feedback if empty", async () => { + await startServer(); + await loadDefaultEmbedConfig(); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-ChatWindow"); + await click("[title*='Close Chat Window']"); +}); + +test("Close without feedback", async () => { + await startServer(); + await loadDefaultEmbedConfig(); + onRpcBefore((route) => { + if (route === "/im_livechat/visitor_leave_session" || route === "/im_livechat/feedback") { + asyncStep(route); + } + }); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-ChatWindow"); + await insertText(".o-mail-Composer-input", "Hello World!"); + triggerHotkey("Enter"); + await contains(".o-mail-Thread:not([data-transient])"); + await click("[title*='Close Chat Window']"); + await click(".o-livechat-CloseConfirmation-leave"); + await click("button", { text: "Close" }); + await contains(".o-livechat-LivechatButton"); + await waitForSteps(["/im_livechat/visitor_leave_session"]); +}); + +test("Last operator leaving ends the livechat", async () => { + await startServer(); + await loadDefaultEmbedConfig(); + const operatorUserId = serverState.userId; + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-ChatWindow"); + await insertText(".o-mail-Composer-input", "Hello World!"); + triggerHotkey("Enter"); + await contains(".o-mail-Message-content", { text: "Hello World!" }); + // simulate operator leaving + await withUser(operatorUserId, () => + getService("orm").call("discuss.channel", "action_unfollow", [ + [Object.values(getService("mail.store").Thread.records).at(-1).id], + ]) + ); + await contains("span", { text: "This livechat conversation has ended" }); + await contains(".o-mail-Composer-input", { count: 0 }); + await click("[title*='Close Chat Window']"); + await contains("p", { text: "Did we correctly answer your question?" }); // shows immediately feedback +}); + +test("Feedback with rating and comment", async () => { + await startServer(); + await loadDefaultEmbedConfig(); + onRpcBefore((route, args) => { + if (route === "/im_livechat/visitor_leave_session") { + asyncStep(route); + } + if (route === "/im_livechat/feedback") { + asyncStep(route); + expect(args.reason).toInclude("Good job!"); + expect(args.rate).toBe(RATING.OK); + } + }); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-ChatWindow"); + await insertText(".o-mail-Composer-input", "Hello World!"); + triggerHotkey("Enter"); + await contains(".o-mail-Thread:not([data-transient])"); + await click("[title*='Close Chat Window']"); + await click(".o-livechat-CloseConfirmation-leave"); + await waitForSteps(["/im_livechat/visitor_leave_session"]); + await click(`img[alt="${RATING.OK}"]`); + await insertText("textarea[placeholder='Explain your note']", "Good job!"); + await click("button:contains(Send):enabled"); + await contains("p", { text: "Thank you for your feedback" }); + await waitForSteps(["/im_livechat/feedback"]); +}); + +test("Closing folded chat window should open it with feedback", async () => { + await startServer(); + await loadDefaultEmbedConfig(); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await insertText(".o-mail-Composer-input", "Hello World!"); + triggerHotkey("Enter"); + await contains(".o-mail-Thread:not([data-transient])"); + await click("[title='Fold']"); + await click(".o-mail-ChatBubble"); + await click("[title*='Close Chat Window']"); + await click(".o-livechat-CloseConfirmation-leave"); + await click(".o-mail-ChatHub-bubbleBtn"); + await contains(".o-mail-ChatWindow p", { text: "Did we correctly answer your question?" }); +}); + +test("Start new session from feedback panel", async () => { + const pyEnv = await startServer(); + const channelId = await loadDefaultEmbedConfig(); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-ChatWindow", { text: "Mitchell Admin" }); + await insertText(".o-mail-Composer-input", "Hello World!"); + triggerHotkey("Enter"); + await contains(".o-mail-Thread:not([data-transient])"); + await click("[title*='Close Chat Window']"); + await click(".o-livechat-CloseConfirmation-leave"); + pyEnv["im_livechat.channel"].write([channelId], { + user_ids: [Command.clear(serverState.userId)], + }); + pyEnv["im_livechat.channel"].write([channelId], { + user_ids: [ + pyEnv["res.users"].create({ + partner_id: pyEnv["res.partner"].create({ name: "Bob Operator" }), + }), + ], + }); + + await click("button", { text: "New Session" }); + await contains(".o-mail-ChatWindow", { count: 1 }); + await contains(".o-mail-ChatWindow", { text: "Bob Operator" }); +}); + +test("open review link on good rating", async () => { + patchWithCleanup(window, { + open: (...args) => { + expect.step("window.open"); + expect(args[0]).toBe("https://www.odoo.com"); + expect(args[1]).toBe("_blank"); + }, + }); + await startServer(); + await loadDefaultEmbedConfig(); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await insertText(".o-mail-Composer-input", "Hello World!"); + triggerHotkey("Enter"); + await contains(".o-mail-Message-content", { text: "Hello World!" }); + await click("[title*='Close Chat Window']"); + await click(".o-livechat-CloseConfirmation-leave"); + await click(`img[alt="${RATING.GOOD}"]`); + await insertText("textarea[placeholder='Explain your note']", "Good job!"); + await click("button:contains(Send):enabled"); + await expect.waitForSteps(["window.open"]); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/history_command.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/history_command.test.js new file mode 100644 index 0000000..61c13ce --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/history_command.test.js @@ -0,0 +1,40 @@ +import { + defineLivechatModels, + loadDefaultEmbedConfig, +} from "@im_livechat/../tests/livechat_test_helpers"; +import { click, start, startServer } from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { press, waitFor } from "@odoo/hoot-dom"; +import { + asyncStep, + contains, + getService, + onRpc, + serverState, + waitForSteps, +} from "@web/../tests/web_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("Handle livechat history command", async () => { + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + onRpc("/im_livechat/history", ({ url }) => { + asyncStep(new URL(url).pathname); + return true; + }); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-Composer-input").edit("Hello World!", { confirm: false }); + await press("Enter"); + await waitFor(".o-mail-Message:contains(Hello World!)"); + const thread = Object.values(getService("mail.store").Thread.records).at(-1); + const guestId = pyEnv.cookie.get("dgid"); + const [guest] = pyEnv["mail.guest"].read(guestId); + pyEnv["bus.bus"]._sendone(guest, "im_livechat.history_command", { + id: thread.id, + partner_id: serverState.partnerId, + }); + await waitForSteps(["/im_livechat/history"]); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/livechat_button.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/livechat_button.test.js new file mode 100644 index 0000000..a39880f --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/livechat_button.test.js @@ -0,0 +1,87 @@ +import { + defineLivechatModels, + loadDefaultEmbedConfig, +} from "@im_livechat/../tests/livechat_test_helpers"; +import { + click, + contains, + insertText, + start, + startServer, + triggerHotkey, +} from "@mail/../tests/mail_test_helpers"; +import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server"; +import { describe, test } from "@odoo/hoot"; +import { asyncStep, patchWithCleanup, waitForSteps } from "@web/../tests/web_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("open/close temporary channel", async () => { + await startServer(); + await loadDefaultEmbedConfig(); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-ChatWindow"); + await contains(".o-livechat-LivechatButton", { count: 0 }); + await click("[title*='Close Chat Window']"); + await contains(".o-mail-ChatWindow", { count: 0 }); + await contains(".o-livechat-LivechatButton", { count: 1 }); +}); + +test("open/close persisted channel", async () => { + await startServer(); + await loadDefaultEmbedConfig(); + const env = await start({ authenticateAs: false }); + env.services.bus_service.subscribe("discuss.channel/new_message", () => + asyncStep("discuss.channel/new_message") + ); + await click(".o-livechat-LivechatButton"); + await insertText(".o-mail-Composer-input", "How can I help?"); + await triggerHotkey("Enter"); + await contains(".o-mail-Thread:not([data-transient])"); + await contains(".o-mail-Message-content", { text: "How can I help?" }); + await waitForSteps(["discuss.channel/new_message"]); + await click("[title*='Close Chat Window']"); + await click(".o-livechat-CloseConfirmation-leave"); + await contains(".o-mail-ChatWindow", { text: "Did we correctly answer your question?" }); + await click("[title*='Close Chat Window']"); + await contains(".o-mail-ChatWindow", { count: 0 }); + await contains(".o-livechat-LivechatButton", { count: 1 }); +}); + +test("livechat not available", async () => { + await startServer(); + await loadDefaultEmbedConfig(); + patchWithCleanup(mailDataHelpers, { + _process_request_for_all(store) { + super._process_request_for_all(...arguments); + store.add({ livechat_available: false }); + }, + }); + await start({ authenticateAs: false }); + await contains(".o-mail-ChatHub"); + await contains(".o-livechat-LivechatButton", { count: 0 }); +}); + +test("clicking on notification opens the chat", async () => { + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + const btnAndTextRuleId = pyEnv["im_livechat.channel.rule"].create({ + action: "display_button_and_text", + }); + patchWithCleanup(mailDataHelpers, { + _process_request_for_all(store) { + super._process_request_for_all(...arguments); + store.add(pyEnv["im_livechat.channel.rule"].browse(btnAndTextRuleId), { + action: "display_button_and_text", + }); + store.add({ livechat_rule: btnAndTextRuleId }); + }, + }); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton-notification", { + text: "Need help? Chat with us.", + }); + await contains(".o-mail-ChatWindow"); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/livechat_service.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/livechat_service.test.js new file mode 100644 index 0000000..f46c7a0 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/livechat_service.test.js @@ -0,0 +1,167 @@ +import { + defineLivechatModels, + loadDefaultEmbedConfig, +} from "@im_livechat/../tests/livechat_test_helpers"; +import { expirableStorage } from "@im_livechat/core/common/expirable_storage"; +import { + click, + contains, + insertText, + listenStoreFetch, + onRpcBefore, + setupChatHub, + start, + startServer, + STORE_FETCH_ROUTES, + triggerHotkey, + userContext, + waitStoreFetch, +} from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { + asyncStep, + Command, + onRpc, + serverState, + waitForSteps, +} from "@web/../tests/web_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("persisted session history", async () => { + const pyEnv = await startServer(); + const livechatChannelId = await loadDefaultEmbedConfig(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId }), + Command.create({ guest_id: guestId }), + ], + channel_type: "livechat", + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + }); + expirableStorage.setItem( + "im_livechat.saved_state", + JSON.stringify({ + store: { "discuss.channel": [{ id: channelId }] }, + persisted: true, + livechatUserId: serverState.publicUserId, + }) + ); + pyEnv["mail.message"].create({ + author_id: serverState.partnerId, + body: "Old message in history", + res_id: channelId, + model: "discuss.channel", + message_type: "comment", + }); + setupChatHub({ opened: [channelId] }); + await start({ + authenticateAs: { ...pyEnv["mail.guest"].read(guestId)[0], _name: "mail.guest" }, + }); + await contains(".o-mail-Message-content", { text: "Old message in history" }); +}); + +test("previous operator prioritized", async () => { + const pyEnv = await startServer(); + const livechatChannelId = await loadDefaultEmbedConfig(); + const userId = pyEnv["res.users"].create({ name: "John Doe", im_status: "online" }); + const previousOperatorId = pyEnv["res.partner"].create({ + name: "John Doe", + user_ids: [userId], + }); + pyEnv["im_livechat.channel"].write([livechatChannelId], { user_ids: [Command.link(userId)] }); + expirableStorage.setItem("im_livechat_previous_operator", JSON.stringify(previousOperatorId)); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-Message-author", { text: "John Doe" }); +}); + +test("Only necessary requests are made when creating a new chat", async () => { + const pyEnv = await startServer(); + const livechatChannelId = await loadDefaultEmbedConfig(); + const operatorPartnerId = serverState.partnerId; + onRpcBefore((route, args) => { + if (!route.includes("assets") && !STORE_FETCH_ROUTES.includes(route)) { + asyncStep(`${route} - ${JSON.stringify(args)}`); + } + }); + listenStoreFetch(undefined, { logParams: ["init_livechat"] }); + await start({ authenticateAs: false }); + await contains(".o-livechat-LivechatButton"); + await waitStoreFetch([ + "failures", // called because mail/core/web is loaded in test bundle + "systray_get_activities", // called because mail/core/web is loaded in test bundle + "init_messaging", + ["init_livechat", livechatChannelId], + ]); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-Message", { text: "Hello, how may I help you?" }); + await waitForSteps([ + `/im_livechat/get_session - ${JSON.stringify({ + channel_id: livechatChannelId, + previous_operator_id: null, + persisted: false, + })}`, + ]); + await insertText(".o-mail-Composer-input", "Hello!"); + await waitForSteps([]); + await triggerHotkey("Enter"); + await contains(".o-mail-Message", { text: "Hello!" }); + const [threadId] = pyEnv["discuss.channel"].search([], { order: "id DESC" }); + await waitStoreFetch( + [ + "failures", // called because mail/core/web is loaded in test bundle + "systray_get_activities", // called because mail/core/web is loaded in test bundle + "init_messaging", + ], + { + stepsBefore: [ + `/im_livechat/get_session - ${JSON.stringify({ + channel_id: livechatChannelId, + previous_operator_id: operatorPartnerId, + persisted: true, + })}`, + `/mail/message/post - ${JSON.stringify({ + post_data: { + body: "Hello!", + email_add_signature: true, + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + thread_id: threadId, + thread_model: "discuss.channel", + context: { ...userContext(), temporary_id: 0.8200000000000001 }, + })}`, + ], + } + ); +}); + +test("do not create new thread when operator answers to visitor", async () => { + const pyEnv = await startServer(); + const livechatChannelId = await loadDefaultEmbedConfig(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + onRpc("/im_livechat/get_session", async () => asyncStep("/im_livechat/get_session")); + onRpc("/mail/message/post", async () => asyncStep("/mail/message/post")); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId }), + Command.create({ guest_id: guestId }), + ], + channel_type: "livechat", + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + create_uid: serverState.publicUserId, + }); + setupChatHub({ opened: [channelId] }); + await start({ + authenticateAs: pyEnv["res.users"].search_read([["id", "=", serverState.userId]])[0], + }); + await insertText(".o-mail-Composer-input", "Hello!"); + await triggerHotkey("Enter"); + await contains(".o-mail-Message", { text: "Hello!" }); + await waitForSteps(["/mail/message/post"]); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/livechat_session.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/livechat_session.test.js new file mode 100644 index 0000000..c2a2884 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/livechat_session.test.js @@ -0,0 +1,106 @@ +import { waitUntilSubscribe } from "@bus/../tests/bus_test_helpers"; +import { + defineLivechatModels, + loadDefaultEmbedConfig, +} from "@im_livechat/../tests/livechat_test_helpers"; +import { + assertChatHub, + click, + contains, + focus, + insertText, + onRpcBefore, + start, + startServer, + triggerHotkey, +} from "@mail/../tests/mail_test_helpers"; +import { describe, expect, test } from "@odoo/hoot"; +import { advanceTime } from "@odoo/hoot-mock"; +import { getService, serverState, withUser } from "@web/../tests/web_test_helpers"; + +import { LivechatButton } from "@im_livechat/embed/common/livechat_button"; +import { queryFirst } from "@odoo/hoot-dom"; +import { rpc } from "@web/core/network/rpc"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("Session is reset after failing to persist the channel", async () => { + await startServer(); + await loadDefaultEmbedConfig(); + onRpcBefore("/im_livechat/get_session", (args) => { + if (args.persisted) { + return false; + } + }); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await insertText(".o-mail-Composer-input", "Hello World!"); + triggerHotkey("Enter"); + await contains(".o_notification", { + text: "No available collaborator, please try again later.", + }); + await contains(".o-livechat-LivechatButton"); + await advanceTime(LivechatButton.DEBOUNCE_DELAY + 10); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-ChatWindow"); +}); + +test("Fold state is saved", async () => { + await startServer(); + await loadDefaultEmbedConfig(); + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-Thread"); + await insertText(".o-mail-Composer-input", "Hello World!"); + triggerHotkey("Enter"); + await contains(".o-mail-Thread:not([data-transient])"); + assertChatHub({ opened: [1] }); + await click(".o-mail-ChatWindow-header"); + await contains(".o-mail-Thread", { count: 0 }); + assertChatHub({ folded: [1] }); + await click(".o-mail-ChatBubble"); + assertChatHub({ opened: [1] }); +}); + +test.tags("focus required"); +test("Seen message is saved on the server", async () => { + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + const userId = serverState.userId; + await start({ authenticateAs: false }); + await click(".o-livechat-LivechatButton"); + await contains(".o-mail-Thread"); + await insertText(".o-mail-Composer-input", "Hello, I need help!"); + triggerHotkey("Enter"); + await contains(".o-mail-Message", { text: "Hello, I need help!" }); + await waitUntilSubscribe(); + const initialSeenMessageId = Object.values(getService("mail.store").Thread.records).at(-1) + .self_member_id.seen_message_id?.id; + queryFirst(".o-mail-Composer-input").blur(); + await withUser(userId, () => + rpc("/mail/message/post", { + post_data: { + body: "Hello World!", + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + thread_id: Object.values(getService("mail.store").Thread.records).at(-1).id, + thread_model: "discuss.channel", + }) + ); + await contains(".o-mail-Thread-newMessage"); + await contains(".o-mail-ChatWindow-counter", { text: "1" }); + await focus(".o-mail-Composer-input"); + await contains(".o-mail-ChatWindow-counter", { count: 0 }); + const guestId = pyEnv.cookie.get("dgid"); + const [member] = pyEnv["discuss.channel.member"].search_read([ + ["guest_id", "=", guestId], + ["channel_id", "=", Object.values(getService("mail.store").Thread.records).at(-1).id], + ]); + expect(initialSeenMessageId).not.toBe(member.seen_message_id[0]); + expect( + Object.values(getService("mail.store").Thread.records).at(-1).self_member_id.seen_message_id + .id + ).toBe(member.seen_message_id[0]); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/message_reactions.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/message_reactions.test.js new file mode 100644 index 0000000..1e83b4b --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/message_reactions.test.js @@ -0,0 +1,66 @@ +import { + defineLivechatModels, + loadDefaultEmbedConfig, +} from "@im_livechat/../tests/livechat_test_helpers"; +import { expirableStorage } from "@im_livechat/core/common/expirable_storage"; + +import { + click, + contains, + hover, + setupChatHub, + start, + startServer, +} from "@mail/../tests/mail_test_helpers"; + +import { describe, test } from "@odoo/hoot"; + +import { Command, serverState } from "@web/../tests/web_test_helpers"; + +defineLivechatModels(); +describe.current.tags("desktop"); + +test("user custom live chat user name for message reactions", async () => { + const pyEnv = await startServer(); + const livechatChannelId = await loadDefaultEmbedConfig(); + pyEnv["res.partner"].write([serverState.partnerId], { user_livechat_username: "Michou" }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId }), + Command.create({ guest_id: guestId }), + ], + channel_type: "livechat", + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + }); + pyEnv["mail.message"].create({ + body: "Hello world", + res_id: channelId, + message_type: "comment", + model: "discuss.channel", + reaction_ids: [ + pyEnv["mail.message.reaction"].create({ + content: "👍", + partner_id: serverState.partnerId, + }), + ], + }); + expirableStorage.setItem( + "im_livechat.saved_state", + JSON.stringify({ + store: { "discuss.channel": [{ id: channelId }] }, + persisted: true, + livechatUserId: serverState.publicUserId, + }) + ); + setupChatHub({ opened: [channelId] }); + await start({ + authenticateAs: { ...pyEnv["mail.guest"].read(guestId)[0], _name: "mail.guest" }, + }); + await hover(".o-mail-MessageReaction"); + await click(".o-mail-MessageReactionList-preview", { + text: "👍:+1: reacted by Michou", + }); + await contains(".o-mail-MessageReactionMenu-persona", { text: "Michou" }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/transcript_sender.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/transcript_sender.test.js new file mode 100644 index 0000000..063dbab --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/transcript_sender.test.js @@ -0,0 +1,58 @@ +import { + defineLivechatModels, + loadDefaultEmbedConfig, +} from "@im_livechat/../tests/livechat_test_helpers"; +import { + click, + contains, + insertText, + onRpcBefore, + start, + startServer, + triggerHotkey, +} from "@mail/../tests/mail_test_helpers"; +import { describe, expect, test } from "@odoo/hoot"; +import { onRpc } from "@web/../tests/web_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("send", async () => { + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + onRpcBefore("/im_livechat/email_livechat_transcript", () => expect.step(`send_transcript`)); + const partnerId = pyEnv["res.partner"].create({ email: "paul@example.com", name: "Paul" }); + pyEnv["res.users"].create({ partner_id: partnerId, login: "paul", password: "paul" }); + await start({ authenticateAs: { login: "paul", password: "paul" } }); + await click(".o-livechat-LivechatButton"); + await insertText(".o-mail-Composer-input", "Hello World!"); + triggerHotkey("Enter"); + await contains(".o-mail-Thread:not([data-transient])"); + await click(".o-mail-ChatWindow-header [title*='Close']"); + await click(".o-livechat-CloseConfirmation-leave"); + await contains("label", { text: "Receive a copy of this conversation" }); + await contains("input:enabled", { value: "paul@example.com" }); + await click("button[data-action='sendTranscript']:enabled"); + await contains(".form-text", { text: "The conversation was sent." }); + await expect.waitForSteps(["send_transcript"]); +}); + +test("send failed", async () => { + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + onRpc("/im_livechat/email_livechat_transcript", () => { + throw new Error(); + }); + const partnerId = pyEnv["res.partner"].create({ email: "paul@example.com", name: "Paul" }); + pyEnv["res.users"].create({ partner_id: partnerId, login: "paul", password: "paul" }); + await start({ authenticateAs: { login: "paul", password: "paul" } }); + await click(".o-livechat-LivechatButton"); + await insertText(".o-mail-Composer-input", "Hello World!"); + triggerHotkey("Enter"); + await contains(".o-mail-Thread:not([data-transient])"); + await click(".o-mail-ChatWindow-header [title*='Close']"); + await click(".o-livechat-CloseConfirmation-leave"); + await contains("input", { value: "paul@example.com" }); + await click("button[data-action='sendTranscript']:enabled"); + await contains(".form-text", { text: "An error occurred. Please try again." }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/unread_messages.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/unread_messages.test.js new file mode 100644 index 0000000..8ee349b --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/embed/unread_messages.test.js @@ -0,0 +1,107 @@ +import { waitUntilSubscribe } from "@bus/../tests/bus_test_helpers"; +import { expirableStorage } from "@im_livechat/core/common/expirable_storage"; +import { + defineLivechatModels, + loadDefaultEmbedConfig, +} from "@im_livechat/../tests/livechat_test_helpers"; +import { + click, + contains, + focus, + insertText, + listenStoreFetch, + setupChatHub, + start, + startServer, + triggerHotkey, + waitStoreFetch, +} from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { asyncStep, Command, onRpc, serverState, withUser } from "@web/../tests/web_test_helpers"; + +import { queryFirst } from "@odoo/hoot-dom"; +import { rpc } from "@web/core/network/rpc"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("new message from operator displays unread counter", async () => { + const pyEnv = await startServer(); + const livechatChannelId = await loadDefaultEmbedConfig(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId }), + Command.create({ guest_id: guestId }), + ], + channel_type: "livechat", + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + }); + expirableStorage.setItem( + "im_livechat.saved_state", + JSON.stringify({ + store: { "discuss.channel": [{ id: channelId }] }, + persisted: true, + livechatUserId: serverState.publicUserId, + }) + ); + setupChatHub({ opened: [channelId] }); + onRpc("/discuss/channel/messages", () => asyncStep("/discuss/channel/message")); + const userId = serverState.userId; + listenStoreFetch(["init_messaging", "init_livechat", "discuss.channel"]); + await start({ + authenticateAs: { ...pyEnv["mail.guest"].read(guestId)[0], _name: "mail.guest" }, + }); + await waitStoreFetch(["init_messaging", "init_livechat", "discuss.channel"], { + stepsAfter: ["/discuss/channel/message"], + }); + // send after init_messaging because bus subscription is done after init_messaging + await withUser(userId, () => + rpc("/mail/message/post", { + post_data: { body: "Are you there?", message_type: "comment" }, + thread_id: channelId, + thread_model: "discuss.channel", + }) + ); + await contains(".o-mail-ChatWindow-counter", { text: "1" }); +}); + +test.tags("focus required"); +test("focus on unread livechat marks it as read", async () => { + const pyEnv = await startServer(); + await loadDefaultEmbedConfig(); + const userId = serverState.userId; + listenStoreFetch(["init_messaging", "init_livechat"]); + await start({ authenticateAs: false }); + await waitStoreFetch(["init_messaging", "init_livechat"]); + await click(".o-livechat-LivechatButton"); + await insertText(".o-mail-Composer-input", "Hello World!"); + await triggerHotkey("Enter"); + // Wait for bus subscription to be done after persisting the thread: + // presence of the message is not enough (temporary message). + await waitUntilSubscribe(); + await contains(".o-mail-Message-content", { text: "Hello World!" }); + const [channelId] = pyEnv["discuss.channel"].search([ + ["channel_type", "=", "livechat"], + [ + "channel_member_ids", + "in", + pyEnv["discuss.channel.member"].search([["guest_id", "=", pyEnv.cookie.get("dgid")]]), + ], + ]); + await waitStoreFetch("init_messaging"); + queryFirst(".o-mail-Composer-input").blur(); + // send after init_messaging because bus subscription is done after init_messaging + await withUser(userId, () => + rpc("/mail/message/post", { + post_data: { body: "Are you there?", message_type: "comment" }, + thread_id: channelId, + thread_model: "discuss.channel", + }) + ); + await contains(".o-mail-ChatWindow-counter", { text: "1" }); + await contains(".o-mail-Message", { text: "Are you there?" }); + await focus(".o-mail-Composer-input"); + await contains(".o-mail-ChatWindow-counter", { count: 0 }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/go_to_oldest_unread_thread.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/go_to_oldest_unread_thread.test.js new file mode 100644 index 0000000..20c1d6f --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/go_to_oldest_unread_thread.test.js @@ -0,0 +1,422 @@ +import { + click, + contains, + focus, + insertText, + openDiscuss, + patchUiSize, + setupChatHub, + start, + startServer, + triggerHotkey, +} from "@mail/../tests/mail_test_helpers"; +import { withGuest } from "@mail/../tests/mock_server/mail_mock_server"; +import { describe, test } from "@odoo/hoot"; +import { press, waitFor } from "@odoo/hoot-dom"; +import { Command, serverState } from "@web/../tests/web_test_helpers"; +import { rpc } from "@web/core/network/rpc"; +import { defineLivechatModels } from "./livechat_test_helpers"; +import { advanceTime, mockDate } from "@odoo/hoot-mock"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("tab on discuss composer goes to oldest unread livechat", async () => { + const pyEnv = await startServer(); + const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const guestId_2 = pyEnv["mail.guest"].create({ name: "Visitor 12" }); + const guestId_3 = pyEnv["mail.guest"].create({ name: "Visitor 13" }); + const channelIds = pyEnv["discuss.channel"].create([ + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + name: "Livechat 1", + }, + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + message_unread_counter: 1, + last_interest_dt: "2021-01-02 10:00:00", + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + name: "Livechat 2", + }, + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + message_unread_counter: 1, + last_interest_dt: "2021-01-01 10:00:00", + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_3, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + name: "Livechat 3", + }, + ]); + pyEnv["mail.message"].create([ + { + author_guest_id: guestId_2, + body: "Hello", + model: "discuss.channel", + res_id: channelIds[1], + }, + { + author_guest_id: guestId_3, + body: "Hello", + model: "discuss.channel", + res_id: channelIds[2], + }, + ]); + await start(); + await openDiscuss(channelIds[0]); + await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 11" }); + await focus(".o-mail-Composer-input"); + await contains(".o-mail-Composer-input[placeholder='Tab to next livechat']"); + await contains(".o-active .o-mail-DiscussSidebar-badge", { count: 0 }); + triggerHotkey("Tab"); + await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 13" }); + await focus(".o-mail-Composer-input"); + await contains(".o-active .o-mail-DiscussSidebar-badge", { count: 0 }); + triggerHotkey("Tab"); + await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 12" }); +}); + +test.tags("focus required"); +test("Tab livechat picks ended livechats last", async () => { + mockDate("2021-01-02T10:05:00"); + const pyEnv = await startServer(); + const guestIds = pyEnv["mail.guest"].create([ + { name: "Visitor 0" }, + { name: "Visitor 1" }, + { name: "Visitor 2" }, + { name: "Visitor 3" }, + { name: "Visitor 4" }, + ]); + const livechatChannelId = pyEnv["im_livechat.channel"].create({ + name: "Test", + user_ids: [serverState.userId], + }); + const channelIds = pyEnv["discuss.channel"].create( + guestIds.map((guestId, idx) => ({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + last_interest_dt: `2021-01-02 10:00:0${idx}`, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + create_uid: serverState.publicUserId, + })) + ); + pyEnv["mail.message"].create( + guestIds.map((guestId, idx) => ({ + author_guest_id: guestId, + body: "Hello", + model: "discuss.channel", + res_id: channelIds[idx], + })) + ); + /** + * channel id | last_interest_dt | livechat_end_dt | unread + * -----------+---------------------+-----------------+-------- + * 0 | 2021-01-02 10:00:00 | false | true + * 1 | 2021-01-02 10:00:01 | false | true + * 2 | 2021-01-02 10:00:02 | false | true + * 3 | 2021-01-02 10:00:03 | false | true + * 4 | 2021-01-02 10:00:04 | false | true + */ + patchUiSize({ width: 1920 }); + setupChatHub({ folded: [channelIds[0], channelIds[1], channelIds[2], channelIds[3]] }); + await start(); + await click(".o_menu_systray i[aria-label='Messages']"); + await click(".o-mail-NotificationItem", { text: "Visitor 4" }); + await contains(".o-mail-ChatWindow:contains('Visitor 4') .o-mail-Message:contains('Hello')"); + await contains(".o-mail-ChatWindow:contains('Visitor 4') .o-mail-Composer.o-focused"); + await contains(".o-mail-ChatWindow:contains('Visitor 4') .badge", { count: 0 }); + await advanceTime(5_000); + await withGuest(guestIds[1], () => + rpc("/mail/message/post", { + post_data: { + body: "livechat 1", + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + thread_id: channelIds[1], + thread_model: "discuss.channel", + }) + ); + await advanceTime(5_000); + await withGuest(guestIds[1], () => + rpc("/im_livechat/visitor_leave_session", { channel_id: channelIds[1] }) + ); + await advanceTime(5_000); + await withGuest(guestIds[3], () => + rpc("/mail/message/post", { + post_data: { + body: "livechat 3", + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + thread_id: channelIds[3], + thread_model: "discuss.channel", + }) + ); + await waitFor(".o-mail-ChatBubble[name='Visitor 3'] .badge:contains('2')", { timeout: 3000 }); + /** + * channel id | last_interest_dt | livechat_end_dt | unread + * -----------+---------------------+-----------------+-------- + * 0 | 2021-01-02 10:00:00 | false | true + * 1 | 2021-01-02 10:05:10 | now() | true + * 2 | 2021-01-02 10:00:02 | false | true + * 3 | 2021-01-02 10:05:15 | false | true + * 4 | 2021-01-02 10:00:04 | false | false + */ + await press("Tab"); + await contains(".o-mail-ChatWindow", { count: 2 }); + await contains(".o-mail-ChatWindow:contains('Visitor 0') .o-mail-Message:contains('Hello')"); + await contains(".o-mail-ChatWindow:contains('Visitor 0') .o-mail-Composer.o-focused"); + await contains(".o-mail-ChatWindow:contains('Visitor 0') .badge", { count: 0 }); + await press("Tab"); + await contains(".o-mail-ChatWindow", { count: 3 }); + await contains(".o-mail-ChatWindow:contains('Visitor 2') .o-mail-Message:contains('Hello')"); + await contains(".o-mail-ChatWindow:contains('Visitor 2') .o-mail-Composer.o-focused"); + await contains(".o-mail-ChatWindow:contains('Visitor 2') .badge", { count: 0 }); + await advanceTime(5_000); + await withGuest(guestIds[0], () => + rpc("/mail/message/post", { + post_data: { + body: "livechat 0", + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + thread_id: channelIds[0], + thread_model: "discuss.channel", + }) + ); + await waitFor(".o-mail-ChatWindow:contains('Visitor 0') .badge:contains('1')", { + timeout: 3000, + }); + /** + * channel id | last_interest_dt | livechat_end_dt | unread + * -----------+---------------------+-----------------+-------- + * 0 | 2021-01-02 10:05:20 | false | true + * 1 | 2021-01-02 10:05:10 | now() | true + * 2 | 2021-01-02 10:00:02 | false | false + * 3 | 2021-01-02 10:05:15 | false | true + * 4 | 2021-01-02 10:00:04 | false | false + */ + await press("Tab"); + await contains(".o-mail-ChatWindow:contains('Visitor 3') .o-mail-Message:contains('Hello')"); + await contains(".o-mail-ChatWindow:contains('Visitor 3') .o-mail-Composer.o-focused"); + await contains(".o-mail-ChatWindow:contains('Visitor 3') .badge", { count: 0 }); + await press("Tab"); + await contains(".o-mail-ChatWindow:contains('Visitor 0') .o-mail-Composer.o-focused"); + await contains(".o-mail-ChatWindow:contains('Visitor 0') .badge", { count: 0 }); + await press("Tab"); + await contains(".o-mail-ChatWindow:contains('Visitor 1') .o-mail-Message:contains('Hello')"); + await contains("span", { text: "This livechat conversation has ended" }); +}); + +test.tags("focus required"); +test("switching to folded chat window unfolds it", async () => { + const pyEnv = await startServer(); + const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const guestId_2 = pyEnv["mail.guest"].create({ name: "Visitor 12" }); + const channelIds = pyEnv["discuss.channel"].create([ + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + name: "Livechat 1", + }, + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + last_interest_dt: "2021-01-02 10:00:00", + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + name: "Livechat 2", + }, + ]); + pyEnv["mail.message"].create({ + author_guest_id: guestId_2, + body: "Hello", + model: "discuss.channel", + res_id: channelIds[1], + }); + setupChatHub({ opened: [channelIds[0]], folded: [channelIds[1]] }); + await start(); + await contains(".o-mail-ChatBubble[name='Visitor 12']"); + await focus(".o-mail-Composer-input", { + parent: [".o-mail-ChatWindow", { text: "Visitor 11" }], + }); + triggerHotkey("Tab"); + await contains(".o-mail-ChatWindow", { + text: "Visitor 12", + contains: [".o-mail-Composer-input:focus"], + }); +}); + +test.tags("focus required"); +test("switching to hidden chat window unhides it", async () => { + const pyEnv = await startServer(); + const [guestId_1, guestId_2] = pyEnv["mail.guest"].create([ + { name: "Visitor 11" }, + { name: "Visitor 12" }, + ]); + const channelIds = pyEnv["discuss.channel"].create([ + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + name: "Livechat 1", + }, + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + last_interest_dt: "2021-01-02 10:00:00", + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + name: "Livechat 2", + }, + { name: "general" }, + ]); + const [livechat_1] = channelIds; + pyEnv["mail.message"].create({ + author_guest_id: guestId_2, + body: "Hello", + model: "discuss.channel", + res_id: livechat_1, + }); + setupChatHub({ opened: channelIds.reverse() }); + patchUiSize({ width: 900 }); // enough for 2 chat windows max + await start(); + // FIXME: expected order: general, 12, 11 + await contains(".o-mail-ChatWindow", { count: 2 }); + await contains(".o-mail-ChatWindow", { count: 0, text: "Visitor 11" }); + await focus(".o-mail-Composer-input", { + parent: [".o-mail-ChatWindow", { text: "Visitor 12" }], + }); + triggerHotkey("Tab"); + await contains(".o-mail-ChatWindow", { + text: "Visitor 11", + contains: [".o-mail-Composer-input:focus"], + }); +}); + +test("tab on composer doesn't switch thread if user is typing", async () => { + const pyEnv = await startServer(); + const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const guestId_2 = pyEnv["mail.guest"].create({ name: "Visitor 12" }); + const channelIds = pyEnv["discuss.channel"].create([ + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + name: "Livechat 1", + }, + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + message_unread_counter: 1, + last_interest_dt: "2021-01-02 10:00:00", + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + name: "Livechat 2", + }, + ]); + await start(); + await openDiscuss(channelIds[0]); + await insertText(".o-mail-Composer-input", "Hello, "); + triggerHotkey("Tab"); + await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 11" }); +}); + +test("tab on composer doesn't switch thread if no unread thread", async () => { + const pyEnv = await startServer(); + const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const guestId_2 = pyEnv["mail.guest"].create({ name: "Visitor 12" }); + const channelIds = pyEnv["discuss.channel"].create([ + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + name: "Livechat 1", + }, + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + name: "Livechat 2", + }, + ]); + await start(); + await openDiscuss(channelIds[0]); + await focus(".o-mail-Composer-input"); + triggerHotkey("Tab"); + await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 11" }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/helpers/mock_server.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/helpers/mock_server.js deleted file mode 100644 index e7109d2..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/helpers/mock_server.js +++ /dev/null @@ -1,198 +0,0 @@ -/** @odoo-module **/ - -import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first - -import { patch } from "@web/core/utils/patch"; -import { MockServer } from "@web/../tests/helpers/mock_server"; - -patch(MockServer.prototype, 'im_livechat', { - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - async _performRPC(route, args) { - if (route === '/im_livechat/get_session') { - const channel_id = args.channel_id; - const anonymous_name = args.anonymous_name; - const previous_operator_id = args.previous_operator_id; - const context = args.context; - return this._mockRouteImLivechatGetSession(channel_id, anonymous_name, previous_operator_id, context); - } - return this._super(...arguments); - }, - - //-------------------------------------------------------------------------- - // Private Mocked Routes - //-------------------------------------------------------------------------- - - /** - * Simulates the `/im_livechat/get_session` route. - * - * @private - * @param {integer} channel_id - * @param {string} anonymous_name - * @param {integer} [previous_operator_id] - * @param {Object} [context={}] - * @returns {Object} - */ - _mockRouteImLivechatGetSession(channel_id, anonymous_name, previous_operator_id, context = {}) { - let user_id; - let country_id; - if ('mockedUserId' in context) { - // can be falsy to simulate not being logged in - user_id = context.mockedUserId; - } else { - user_id = this.currentUserId; - } - // don't use the anonymous name if the user is logged in - if (user_id) { - const user = this.getRecords('res.users', [['id', '=', user_id]])[0]; - country_id = user.country_id; - } else { - // simulate geoip - const countryCode = context.mockedCountryCode; - const country = this.getRecords('res.country', [['code', '=', countryCode]])[0]; - if (country) { - country_id = country.id; - anonymous_name = anonymous_name + ' (' + country.name + ')'; - } - } - return this._mockImLivechatChannel_openLivechatMailChannel(channel_id, anonymous_name, previous_operator_id, user_id, country_id); - }, - - //-------------------------------------------------------------------------- - // Private Mocked Methods - //-------------------------------------------------------------------------- - - /** - * @override - */ - _mockMailChannelChannelInfo(ids) { - const channelInfos = this._super(...arguments); - for (const channelInfo of channelInfos) { - const channel = this.getRecords('mail.channel', [['id', '=', channelInfo.id]])[0]; - channelInfo['channel']['anonymous_name'] = channel.anonymous_name; - // add the last message date - if (channel.channel_type === 'livechat') { - // add the operator id - if (channel.livechat_operator_id) { - const operator = this.getRecords('res.partner', [['id', '=', channel.livechat_operator_id]])[0]; - // livechat_username ignored for simplicity - channelInfo.operator_pid = [operator.id, operator.display_name.replace(',', '')]; - } - } - } - return channelInfos; - }, - /** - * Simulates `_get_available_users` on `im_livechat.channel`. - * - * @private - * @param {integer} id - * @returns {Object} - */ - _mockImLivechatChannel_getAvailableUsers(id) { - const livechatChannel = this.getRecords('im_livechat.channel', [['id', '=', id]])[0]; - const users = this.getRecords('res.users', [['id', 'in', livechatChannel.user_ids]]); - return users.filter(user => user.im_status === 'online'); - }, - /** - * Simulates `_get_livechat_mail_channel_vals` on `im_livechat.channel`. - * - * @private - * @param {integer} id - * @returns {Object} - */ - _mockImLivechatChannel_getLivechatMailChannelVals(id, anonymous_name, operator, user_id, country_id) { - // partner to add to the mail.channel - const operator_partner_id = operator.partner_id; - const membersToAdd = [[0, 0, { - is_pinned: false, - partner_id: operator_partner_id, - }]]; - let visitor_user; - if (user_id) { - const visitor_user = this.getRecords('res.users', [['id', '=', user_id]])[0]; - if (visitor_user && visitor_user.active && visitor_user !== operator) { - // valid session user (not public) - membersToAdd.push([0, 0, { partner_id: visitor_user.partner_id.id }]); - } - } else { - membersToAdd.push([0, 0, { partner_id: this.publicPartnerId }]); - } - const membersName = [ - visitor_user ? visitor_user.display_name : anonymous_name, - operator.livechat_username ? operator.livechat_username : operator.name, - ]; - return { - 'channel_member_ids': membersToAdd, - 'livechat_active': true, - 'livechat_operator_id': operator_partner_id, - 'livechat_channel_id': id, - 'anonymous_name': user_id ? false : anonymous_name, - 'country_id': country_id, - 'channel_type': 'livechat', - 'name': membersName.join(' '), - }; - }, - /** - * Simulates `_get_random_operator` on `im_livechat.channel`. - * Simplified mock implementation: returns the first available operator. - * - * @private - * @param {integer} id - * @returns {Object} - */ - _mockImLivechatChannel_getRandomOperator(id) { - const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id); - return availableUsers[0]; - }, - /** - * Simulates `_open_livechat_mail_channel` on `im_livechat.channel`. - * - * @private - * @param {integer} id - * @param {string} anonymous_name - * @param {integer} [previous_operator_id] - * @param {integer} [user_id] - * @param {integer} [country_id] - * @returns {Object} - */ - _mockImLivechatChannel_openLivechatMailChannel(id, anonymous_name, previous_operator_id, user_id, country_id) { - let operator; - if (previous_operator_id) { - const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id); - operator = availableUsers.find(user => user.partner_id === previous_operator_id); - } - if (!operator) { - operator = this._mockImLivechatChannel_getRandomOperator(id); - } - if (!operator) { - // no one available - return false; - } - // create the session, and add the link with the given channel - const mailChannelVals = this._mockImLivechatChannel_getLivechatMailChannelVals(id, anonymous_name, operator, user_id, country_id); - const mailChannelId = this.pyEnv['mail.channel'].create(mailChannelVals); - this._mockMailChannel_broadcast([mailChannelId], [operator.partner_id]); - return this._mockMailChannelChannelInfo([mailChannelId])[0]; - }, - /** - * @override - */ - _mockResPartner_GetChannelsAsMember(ids) { - const partner = this.getRecords('res.partner', [['id', 'in', ids]])[0]; - const members = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id], ['is_pinned', '=', true]]); - const livechats = this.getRecords('mail.channel', [ - ['channel_type', '=', 'livechat'], - ['channel_member_ids', 'in', members.map(member => member.id)], - ]); - return [ - ...this._super(ids), - ...livechats, - ]; - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/helpers/mock_server/controllers/main.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/helpers/mock_server/controllers/main.js deleted file mode 100644 index 318ee9c..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/helpers/mock_server/controllers/main.js +++ /dev/null @@ -1,35 +0,0 @@ -/** @odoo-module **/ - -import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first - -import { patch } from "@web/core/utils/patch"; -import { MockServer } from "@web/../tests/helpers/mock_server"; - -patch(MockServer.prototype, 'im_livechat/controllers/main', { - /** - * @override - */ - async _performRPC(route, args) { - if (route === '/im_livechat/notify_typing') { - const uuid = args.uuid; - const is_typing = args.is_typing; - const context = args.context; - return this._mockRouteImLivechatNotifyTyping(uuid, is_typing, context); - } - return this._super(...arguments); - }, - /** - * Simulates the `/im_livechat/notify_typing` route. - * - * @private - * @param {string} uuid - * @param {boolean} is_typing - * @param {Object} [context={}] - */ - _mockRouteImLivechatNotifyTyping(uuid, is_typing, context = {}) { - const [mailChannel] = this.getRecords('mail.channel', [['uuid', '=', uuid]]); - const partnerId = context.mockedPartnerId || this.currentPartnerId; - const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', mailChannel.id], ['partner_id', '=', partnerId]]); - this._mockMailChannelMember_NotifyTyping([memberOfCurrentUser.id], is_typing); - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/helpers/mock_server/models/mail_channel_member.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/helpers/mock_server/models/mail_channel_member.js deleted file mode 100644 index 1f4cc63..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/helpers/mock_server/models/mail_channel_member.js +++ /dev/null @@ -1,40 +0,0 @@ -/** @odoo-module **/ - -import '@mail/../tests/helpers/mock_server/models/mail_channel_member'; // ensure mail overrides are applied first - -import { patch } from "@web/core/utils/patch"; -import { MockServer } from "@web/../tests/helpers/mock_server"; - -patch(MockServer.prototype, 'im_livechat/models/mail_channel_member', { - /** - * @override - */ - _mockMailChannelMember_GetPartnerData(ids) { - const [member] = this.getRecords('mail.channel.member', [['id', 'in', ids]]); - const [channel] = this.getRecords('mail.channel', [['id', '=', member.channel_id]]); - const [partner] = this.getRecords('res.partner', [['id', '=', member.partner_id]], { active_test: false }); - if (channel.channel_type === 'livechat') { - const data = { - 'id': partner.id, - 'is_public': partner.is_public, - }; - if (partner.user_livechat_username) { - data['user_livechat_username'] = partner.user_livechat_username; - } else { - data['name'] = partner.name; - } - if (!partner.is_public) { - const [country] = this.getRecords('res.country', [['id', '=', partner.country_id]]); - data['country'] = country - ? { - 'code': country.code, - 'id': country.id, - 'name': country.name, - } - : [['clear']]; - } - return data; - } - return this._super(ids); - }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/helpers/model_definitions_setup.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/helpers/model_definitions_setup.js deleted file mode 100644 index 2207dad..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/helpers/model_definitions_setup.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @odoo-module **/ - -import { addModelNamesToFetch, insertModelFields } from '@bus/../tests/helpers/model_definitions_helpers'; - -addModelNamesToFetch(['im_livechat.channel']); -insertModelFields('res.users.settings', { - is_discuss_sidebar_category_livechat_open: { default: true }, -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/im_livechat_shared_tests.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/im_livechat_shared_tests.js new file mode 100644 index 0000000..97c29e3 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/im_livechat_shared_tests.js @@ -0,0 +1,43 @@ +import { expect } from "@odoo/hoot"; +import { + click, + contains, + setupChatHub, + start, + startServer, +} from "@mail/../tests/mail_test_helpers"; +import { Command, onRpc, serverState } from "@web/../tests/web_test_helpers"; + +export async function livechatLastAgentLeaveFromChatWindow() { + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { + group_ids: pyEnv["res.groups"] + .search_read([["id", "=", serverState.groupLivechatId]]) + .map(({ id }) => id), + }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor" }); + const livechatChannelId = pyEnv["im_livechat.channel"].create({ + name: "HR", + user_ids: [serverState.userId], + }); + const channelId = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + create_uid: serverState.publicUserId, + }); + setupChatHub({ opened: [channelId] }); + onRpc("discuss.channel", "action_unfollow", () => { + expect.step("action_unfollow"); + }); + await start(); + await contains(".o-mail-ChatWindow"); + await click("button[title*='Close Chat Window']"); + await click("button:contains('Yes, leave conversation')"); + await expect.waitForSteps(["action_unfollow"]); + await contains(".o-mail-ChatWindow", { count: 0 }); +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/livechat_channel_info_list.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/livechat_channel_info_list.test.js new file mode 100644 index 0000000..93c64da --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/livechat_channel_info_list.test.js @@ -0,0 +1,352 @@ +import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers"; +import { + click, + contains, + insertText, + openDiscuss, + start, + startServer, +} from "@mail/../tests/mail_test_helpers"; +import { describe, press, test, waitFor } from "@odoo/hoot"; +import { Command, serverState } from "@web/../tests/web_test_helpers"; +import { serializeDate, today } from "@web/core/l10n/dates"; +import { getOrigin } from "@web/core/utils/urls"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("livechat note is loaded when opening the channel info list", async () => { + const pyEnv = await startServer(); + const userId = pyEnv["res.users"].create({ name: "James" }); + pyEnv["res.partner"].create({ + name: "James", + user_ids: [userId], + }); + const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" }); + const guestId = pyEnv["mail.guest"].create({ + name: "Visitor #20", + }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + country_id: countryId, + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + livechat_note: "

Initial note
Second line

", + }); + await start(); + await openDiscuss(channelId); + await contains(".o-livechat-ChannelInfoList textarea", { value: "Initial note\nSecond line" }); +}); + +test("shows country and language in channel info list", async () => { + const pyEnv = await startServer(); + const countryId = pyEnv["res.country"].create({ code: "BE", name: "Belgium" }); + const langId = pyEnv["res.lang"].create({ name: "English" }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + country_id: countryId, + channel_type: "livechat", + livechat_lang_id: langId, + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(channelId); + await contains("h6", { text: "Country & Language" }); + await contains("span[title='Language']", { text: "English" }); + const [country] = pyEnv["res.country"].search_read([["id", "=", countryId]]); + await contains(`.o_country_flag[data-src*='/country_flags/${country.code.toLowerCase()}.png']`); +}); + +test("editing livechat note is synced between tabs", async () => { + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { + group_ids: [serverState.groupLivechatId], + }); + const userId = pyEnv["res.users"].create({ name: "James" }); + pyEnv["res.partner"].create({ + name: "James", + user_ids: [userId], + }); + const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" }); + const guestId = pyEnv["mail.guest"].create({ + name: "Visitor #20", + }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + country_id: countryId, + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + livechat_note: "

Initial note

", + }); + const tab1 = await start({ asTab: true }); + const tab2 = await start({ asTab: true }); + await openDiscuss(channelId, { target: tab1 }); + await openDiscuss(channelId, { target: tab2 }); + await contains(`${tab1.selector} .o-livechat-ChannelInfoList textarea`, { + value: "Initial note", + }); + await contains(`${tab2.selector} .o-livechat-ChannelInfoList textarea`, { + value: "Initial note", + }); + await insertText(`${tab1.selector} .o-livechat-ChannelInfoList textarea`, "Updated note", { + replace: true, + }); + document.querySelector(`${tab1.selector} .o-livechat-ChannelInfoList textarea`).blur(); // Trigger the blur event to save the note + await contains(`${tab2.selector} .o-livechat-ChannelInfoList textarea`, { + value: "Updated note", + }); // Note should be synced with bus +}); + +test("shows live chat status in discuss sidebar", async () => { + const pyEnv = await startServer(); + const userId = pyEnv["res.users"].create({ name: "James" }); + pyEnv["res.partner"].create({ + name: "James", + user_ids: [userId], + }); + const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" }); + const guestId = pyEnv["mail.guest"].create({ + name: "Visitor #20", + }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + country_id: countryId, + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + livechat_status: "waiting", + }); + await start(); + await openDiscuss(channelId); + await contains(".o-livechat-ChannelInfoList button.active", { text: "Waiting for customer" }); + await contains(".o-mail-DiscussSidebar-item span[title='Waiting for customer']"); + await click(".o-livechat-ChannelInfoList button", { text: "Looking for help" }); + await contains(".o-livechat-ChannelInfoList button.active", { text: "Looking for help" }); + await contains(".o-mail-DiscussSidebar-item span[title='Looking for help']"); + // live chat status icon also in messaging menu item + await click(".o_menu_systray i[aria-label='Messages']"); + await contains( + ".o-mail-MessagingMenu .o-mail-NotificationItem:contains('Visitor #20') [title='Looking for help']" + ); +}); + +test("editing livechat status is synced between tabs", async () => { + const pyEnv = await startServer(); + const userId = pyEnv["res.users"].create({ name: "James" }); + pyEnv["res.partner"].create({ + name: "James", + user_ids: [userId], + }); + const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" }); + const guestId = pyEnv["mail.guest"].create({ + name: "Visitor #20", + }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + country_id: countryId, + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + livechat_status: "in_progress", + }); + const tab1 = await start({ asTab: true }); + const tab2 = await start({ asTab: true }); + await openDiscuss(channelId, { target: tab1 }); + await openDiscuss(channelId, { target: tab2 }); + await contains(`${tab1.selector} .o-livechat-ChannelInfoList button.active`, { + text: "In progress", + }); + await contains(`${tab2.selector} .o-livechat-ChannelInfoList button.active`, { + text: "In progress", + }); + await click(`${tab1.selector} .o-livechat-ChannelInfoList button`, { + text: "Waiting for customer", + }); + await contains(`${tab1.selector} .o-livechat-ChannelInfoList button.active`, { + text: "Waiting for customer", + }); + await contains(`${tab2.selector} .o-livechat-ChannelInfoList button.active`, { + text: "Waiting for customer", + }); // Status should be synced with bus +}); + +test("Manage expertises from channel info list", async () => { + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { + group_ids: [serverState.groupLivechatManagerId, serverState.groupLivechatId], + }); + const userId = pyEnv["res.users"].create({ name: "James" }); + pyEnv["res.partner"].create({ name: "James", user_ids: [userId] }); + const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" }); + const expertiseIds = pyEnv["im_livechat.expertise"].create([{ name: "pricing" }]); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + country_id: countryId, + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + livechat_expertise_ids: expertiseIds, + }); + await start(); + await openDiscuss(channelId); + await contains(".o-livechat-ChannelInfoList .o_tag", { text: "pricing" }); + await insertText(".o-livechat-ExpertiseTagsAutocomplete input", "events"); + await click("a", { text: 'Create "events"' }); + await contains(".o-livechat-ChannelInfoList .o_tag", { text: "events" }); + await click(".o-livechat-ExpertiseTagsAutocomplete input"); + await press("Backspace"); + await contains(".o-livechat-ChannelInfoList .o_tag", { text: "events", count: 0 }); + await press("Backspace"); + await contains(".o-livechat-ChannelInfoList .o_tag", { text: "pricing", count: 0 }); + await contains(".o-livechat-ExpertiseTagsAutocomplete input[placeholder='Add expertise']"); + await click("a", { text: "events" }); + await contains(".o-livechat-ChannelInfoList .o_tag", { text: "events" }); +}); + +test("Can download transcript from channel info panel", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_end_dt: serializeDate(today().plus({ days: -1 })), + }); + await start(); + await openDiscuss(channelId); + await contains( + `a[href='${getOrigin()}/im_livechat/download_transcript/${channelId}']:text(Download)` + ); +}); + +test("Disable actions for non-livechat users", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_status: "in_progress", + }); + await start(); + await openDiscuss(channelId); + await waitFor(".o-livechat-LivechatStatusSelection button:text(In progress):disabled"); + await waitFor(".o-livechat-LivechatStatusSelection button:text(Waiting for customer):disabled"); + await waitFor(".o-livechat-LivechatStatusSelection button:text(Looking for help):disabled"); + await waitFor("textarea[placeholder='Add your notes here...']:disabled"); + await waitFor(".o-livechat-ExpertiseTagsAutocomplete.o-disabled"); +}); + +test("info panel toggle state persists across chats", async () => { + const pyEnv = await startServer(); + const [guestId1, guestId2] = pyEnv["mail.guest"].create([ + { name: "Visitor 1" }, + { name: "Visitor 2" }, + ]); + pyEnv["discuss.channel"].create([ + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId1, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }, + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId2, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }, + ]); + await start(); + await openDiscuss(); + await click(".o-mail-DiscussSidebarChannel:text('Visitor 1')"); + await contains(".o-livechat-ChannelInfoList"); + await click("button[name='livechat-info']"); + await contains(".o-livechat-ChannelInfoList", { count: 0 }); + await click(".o-mail-DiscussSidebarChannel:text('Visitor 2')"); + await contains(".o-mail-DiscussContent-threadName[title='Visitor 2']"); + await contains(".o-livechat-ChannelInfoList", { count: 0 }); + await click("button[name='livechat-info']"); + await contains(".o-livechat-ChannelInfoList"); + await click(".o-mail-DiscussSidebarChannel:text('Visitor 1')"); + await contains(".o-mail-DiscussContent-threadName[title='Visitor 1']"); + await contains(".o-livechat-ChannelInfoList"); +}); + +test("auto-open of livechat info & members panels should combine", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor" }); + pyEnv["discuss.channel"].create([ + { + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }, + { + channel_type: "channel", + name: "General", + }, + ]); + await start(); + await openDiscuss(); + await click(".o-mail-DiscussSidebarChannel:text('General')"); + await contains(".o-discuss-ChannelMemberList"); + await click(".o-mail-DiscussSidebarChannel:text('Visitor')"); + await contains(".o-discuss-ChannelMemberList", { count: 0 }); + await contains(".o-livechat-ChannelInfoList"); + await click("button[name='livechat-info']"); + await contains(".o-livechat-ChannelInfoList", { count: 0 }); + await contains(".o-discuss-ChannelMemberList", { count: 0 }); + await click(".o-mail-DiscussSidebarChannel:text('General')"); + await contains(".o-discuss-ChannelMemberList"); + await contains(".o-livechat-ChannelInfoList", { count: 0 }); + await click("button[name='member-list']"); + await contains(".o-discuss-ChannelMemberList", { count: 0 }); + await contains(".o-livechat-ChannelInfoList", { count: 0 }); + await click(".o-mail-DiscussSidebarChannel:text('Visitor')"); + await click("button[name='livechat-info']"); + await contains(".o-livechat-ChannelInfoList"); + await contains(".o-discuss-ChannelMemberList", { count: 0 }); + await click("button[name='member-list']"); + await contains(".o-discuss-ChannelMemberList"); + await contains(".o-livechat-ChannelInfoList", { count: 0 }); + await click(".o-mail-DiscussSidebarChannel:text('General')"); + await contains(".o-discuss-ChannelMemberList"); + await contains(".o-livechat-ChannelInfoList", { count: 0 }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/livechat_test_helpers.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/livechat_test_helpers.js new file mode 100644 index 0000000..bacaef0 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/livechat_test_helpers.js @@ -0,0 +1,73 @@ +import { IrWebSocket } from "@im_livechat/../tests/mock_server/mock_models/ir_websocket"; + +import { mailModels, startServer } from "@mail/../tests/mail_test_helpers"; +import { RatingRating } from "@rating/../tests/mock_server/models/rating_rating"; +import { + defineModels, + serverState, + patchWithCleanup, + MockServer, +} from "@web/../tests/web_test_helpers"; +import { DiscussChannel } from "./mock_server/mock_models/discuss_channel"; +import { DiscussChannelMember } from "./mock_server/mock_models/discuss_channel_member"; +import { LivechatChannel } from "./mock_server/mock_models/im_livechat_channel"; +import { LivechatChannelRule } from "./mock_server/mock_models/livechat_channel_rule"; +import { Im_LivechatExpertise } from "./mock_server/mock_models/im_livechat_expertise"; +import { ResGroupsPrivilege } from "./mock_server/mock_models/res_groups_privilege"; +import { ResGroups } from "./mock_server/mock_models/res_groups"; +import { ResPartner } from "./mock_server/mock_models/res_partner"; +import { ResUsers } from "./mock_server/mock_models/res_users"; +import { session } from "@web/session"; + +export function defineLivechatModels() { + return defineModels(livechatModels); +} + +export const livechatModels = { + ...mailModels, + DiscussChannel, + DiscussChannelMember, + LivechatChannel, + LivechatChannelRule, + Im_LivechatExpertise, + IrWebSocket, + RatingRating, + ResPartner, + ResUsers, + ResGroupsPrivilege, + ResGroups, +}; + +serverState.groupLivechatId = 42; +serverState.groupLivechatManagerId = 43; + +/** + * Setup the server side of the livechat app. + * + * @returns {Promise} the id of the livechat channel. + */ +export async function loadDefaultEmbedConfig() { + const pyEnv = MockServer.env ?? (await startServer()); + const livechatChannelId = pyEnv["im_livechat.channel"].create({ + user_ids: [serverState.userId], + }); + patchWithCleanup(session, { + livechatData: { + can_load_livechat: true, + serverUrl: window.origin, + options: { + header_background_color: "#875A7B", + button_background_color: "#875A7B", + title_color: "#FFFFFF", + button_text_color: "#FFFFFF", + button_text: "Need help? Chat with us.", + default_message: "Hello, how may I help you?", + channel_name: "YourWebsite.com", + channel_id: livechatChannelId, + default_username: "Visitor", + review_link: "https://www.odoo.com", + }, + }, + }); + return livechatChannelId; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/looking_for_help.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/looking_for_help.test.js new file mode 100644 index 0000000..d47be42 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/looking_for_help.test.js @@ -0,0 +1,314 @@ +import { waitForChannels } from "@bus/../tests/bus_test_helpers"; + +import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers"; +import { LFH_UNSUBSCRIBE_DELAY } from "@im_livechat/core/public_web/discuss_app_model_patch"; + +import { + click, + contains, + openDiscuss, + openFormView, + setupChatHub, + start, + startServer, +} from "@mail/../tests/mail_test_helpers"; + +import { advanceTime, describe, expect, test } from "@odoo/hoot"; +import { tick, waitFor } from "@odoo/hoot-dom"; + +import { + Command, + getService, + onRpc, + patchWithCleanup, + serverState, + withUser, +} from "@web/../tests/web_test_helpers"; +import { rpc } from "@web/core/network/rpc"; +import { Deferred } from "@web/core/utils/concurrency"; + +defineLivechatModels(); +describe.current.tags("desktop"); + +test("Show looking for help in the sidebar while active or still seeking help", async () => { + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { + group_ids: pyEnv["res.groups"] + .search_read([["id", "=", serverState.groupLivechatId]]) + .map(({ id }) => id), + }); + const bobPartnerId = pyEnv["res.partner"].create({ + name: "bob", + user_ids: [Command.create({ name: "bob" })], + }); + const bobChannelId = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [Command.create({ partner_id: bobPartnerId })], + livechat_status: "need_help", + }); + await start(); + await openDiscuss(); + await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-down"); + await contains(".o-mail-DiscussSidebarChannel", { text: "bob" }); + await waitForChannels(["im_livechat.looking_for_help"]); + await rpc("/im_livechat/session/update_status", { + channel_id: bobChannelId, + livechat_status: "in_progress", + }); + await contains(".o-mail-DiscussSidebarChannel", { text: "bob", count: 0 }); + await rpc("/im_livechat/session/update_status", { + channel_id: bobChannelId, + livechat_status: "need_help", + }); + await click(".o-mail-DiscussSidebarChannel", { text: "bob" }); + await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "bob" }); + await waitForChannels([`discuss.channel_${bobChannelId}`]); + await rpc("/im_livechat/session/update_status", { + channel_id: bobChannelId, + livechat_status: "in_progress", + }); + await contains(".o-livechat-LivechatStatusSelection .o-inProgress.active"); + await waitForChannels([`discuss.channel_${bobChannelId}`]); + await contains(".o-mail-DiscussSidebarChannel", { text: "bob" }); + await click(".o-mail-Mailbox[data-mailbox-id=starred"); + await contains(".o-mail-DiscussSidebarChannel", { text: "bob", count: 0 }); + await waitForChannels([`discuss.channel_${bobChannelId}`], { operation: "delete" }); +}); + +test("Do not auto-open chat window on new message when locally pinned", async () => { + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { + group_ids: pyEnv["res.groups"] + .search_read([["id", "=", serverState.groupLivechatId]]) + .map(({ id }) => id), + }); + setupChatHub({ + folded: [ + pyEnv["discuss.channel"].create({ + name: "General", + channel_type: "channel", + }), + ], + opened: [ + pyEnv["discuss.channel"].create({ + name: "Support", + channel_type: "channel", + }), + ], + }); + const bobPartnerId = pyEnv["res.partner"].create({ + name: "bob", + user_ids: [Command.create({ name: "bob" })], + }); + const bobChannelId = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [Command.create({ partner_id: bobPartnerId })], + livechat_status: "need_help", + }); + await start(); + getService("bus_service").subscribe("discuss.channel/new_message", () => + expect.step("discuss.channel/new_message") + ); + await openDiscuss(); + await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-down"); + await click(".o-mail-DiscussSidebarChannel", { text: "bob" }); + await waitForChannels([`discuss.channel_${bobChannelId}`]); + await withUser(serverState.userId, async () => { + await rpc("/mail/message/post", { + post_data: { + body: "Hello, how can I help?", + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + thread_id: bobChannelId, + thread_model: "discuss.channel", + }); + }); + await contains(".o-mail-Message", { text: "Hello, how can I help?" }); + await expect.waitForSteps(["discuss.channel/new_message"]); + await openFormView("res.partner", bobPartnerId); + await contains(".o-mail-ChatBubble"); + await contains(".o-mail-ChatBubble[name=General]"); + await contains(".o-mail-ChatBubble", { count: 0, text: "bob" }); + await contains(".o-mail-ChatWindow", { text: "Support" }); + await contains(".o-mail-ChatWindow", { count: 0, text: "bob" }); +}); + +test("Enable/disable looking for help when category is opened/folded", async () => { + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { + group_ids: pyEnv["res.groups"] + .search_read([["id", "=", serverState.groupLivechatId]]) + .map(({ id }) => id), + }); + localStorage.setItem("discuss_sidebar_category_im_livechat.category_need_help_open", false); + await start(); + patchWithCleanup(getService("bus_service"), { + addChannel: (channelName) => { + if (channelName === "im_livechat.looking_for_help") { + expect.step(`addChannel - ${channelName}`); + } + }, + deleteChannel: (channelName) => { + if (channelName === "im_livechat.looking_for_help") { + expect.step(`deleteChannel - ${channelName}`); + } + }, + }); + onRpc("/mail/data", async (req) => { + const { params } = await req.json(); + if (params.fetch_params.includes("/im_livechat/looking_for_help")) { + expect.step("fetch looking_for_help"); + } + }); + await openDiscuss(); + await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-right"); + await expect.waitForSteps([]); + await click(".o-mail-DiscussSidebarCategory-livechatNeedHelp button"); + await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-down"); + await expect.waitForSteps([ + "addChannel - im_livechat.looking_for_help", + "fetch looking_for_help", + ]); + await click(".o-mail-DiscussSidebarCategory-livechatNeedHelp button"); + await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-right"); + await expect.waitForSteps([]); + await advanceTime(LFH_UNSUBSCRIBE_DELAY + 1000); + await expect.waitForSteps(["deleteChannel - im_livechat.looking_for_help"]); + await click(".o-mail-DiscussSidebarCategory-livechatNeedHelp button"); + await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-down"); + await expect.waitForSteps([ + "addChannel - im_livechat.looking_for_help", + "fetch looking_for_help", + ]); +}); + +test("Show join button when help is required and self is not a member", async () => { + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { + group_ids: pyEnv["res.groups"] + .search_read([["id", "=", serverState.groupLivechatId]]) + .map(({ id }) => id), + }); + const bobPartnerId = pyEnv["res.partner"].create({ + name: "bob", + user_ids: [Command.create({ name: "bob" })], + }); + const channel = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [Command.create({ partner_id: bobPartnerId })], + livechat_status: "need_help", + }); + await start(); + await openDiscuss(channel); + await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-down"); + await contains(".o-livechat-LivechatStatusSelection .active", { text: "Looking for help" }); + await click("button[name='join-livechat-needing-help']"); + await contains(".o-livechat-LivechatStatusSelection .active", { text: "In progress" }); + await contains("button[name='join-livechat-needing-help']", { count: 0 }); + await click(".o-livechat-LivechatStatusSelection button", { text: "Looking for help" }); + await contains(".o-livechat-LivechatStatusSelection .active", { text: "Looking for help" }); + // Now that we are members, the button is not shown, even if help is required. + await contains("button[name='join-livechat-needing-help']", { count: 0 }); +}); + +test("Show notification when joining a channel that already received help", async () => { + const pyEnv = await startServer(); + const bobPartnerId = pyEnv["res.partner"].create({ + name: "bob", + user_ids: [Command.create({ name: "bob" })], + }); + // Simulate another agent attempting to join the channel to provide help at the same time, + // but succeeding just before the current agent (server returns false when it happens). + onRpc("discuss.channel", "livechat_join_channel_needing_help", () => false); + const channel = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [Command.create({ partner_id: bobPartnerId })], + livechat_status: "need_help", + }); + const env = await start(); + patchWithCleanup(env.services.notification, { + add: (message, options) => expect.step(`${options.type} - ${message}`), + }); + await openDiscuss(channel); + await contains(".o-livechat-LivechatStatusSelection .active", { text: "Looking for help" }); + await click("button[name='join-livechat-needing-help']"); + expect.waitForSteps(["warning - Someone has already joined this conversation"]); +}); + +test("Hide 'help already received' notification when channel is not visible", async () => { + const pyEnv = await startServer(); + pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" }); + const bobPartnerId = pyEnv["res.partner"].create({ + name: "bob", + user_ids: [Command.create({ name: "bob" })], + }); + // Simulate another agent attempting to join the channel to provide help at the same time, + // but succeeding just before the current agent (server returns false when it happens). + let canRespondDeferred; + onRpc("discuss.channel", "livechat_join_channel_needing_help", async () => { + await canRespondDeferred; + return false; + }); + const channel = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [Command.create({ partner_id: bobPartnerId })], + livechat_status: "need_help", + }); + const env = await start(); + patchWithCleanup(env.services.notification, { + add: (message, options) => expect.step(`${options.type} - ${message}`), + }); + await openDiscuss(channel); + await contains(".o-livechat-LivechatStatusSelection .active", { text: "Looking for help" }); + await click("button[name='join-livechat-needing-help']"); + expect.waitForSteps(["warning - Someone has already joined this conversation"]); + canRespondDeferred = new Deferred(); + await click("button[name='join-livechat-needing-help']"); + await click(".o-mail-DiscussSidebar-item", { text: "Inbox" }); + await contains(".o-mail-DiscussContent-threadName[title='Inbox']"); + canRespondDeferred.resolve(); + await tick(); + await expect.waitForSteps([]); +}); + +test("Expertise matching hint is shown in the sidebar when chat is looking for help", async () => { + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { + group_ids: pyEnv["res.groups"] + .search_read([["id", "=", serverState.groupLivechatId]]) + .map(({ id }) => id), + }); + const bobPartnerId = pyEnv["res.partner"].create({ + name: "bob", + user_ids: [Command.create({ name: "bob" })], + }); + const janePartnerId = pyEnv["res.partner"].create({ + name: "jane", + user_ids: [Command.create({ name: "jane" })], + }); + const expertiseIds = pyEnv["im_livechat.expertise"].create([{ name: "pricing" }]); + pyEnv["res.users"].write([serverState.userId], { livechat_expertise_ids: expertiseIds }); + pyEnv["discuss.channel"].create([ + { + channel_type: "livechat", + channel_member_ids: [Command.create({ partner_id: bobPartnerId })], + livechat_status: "need_help", + livechat_expertise_ids: expertiseIds, + }, + { + channel_type: "livechat", + channel_member_ids: [Command.create({ partner_id: janePartnerId })], + livechat_status: "need_help", + }, + ]); + await start(); + await openDiscuss(); + await waitFor( + ".o-mail-DiscussSidebarChannel:text(bob):has([title='Relevant to your expertise'])" + ); + await waitFor(".o-mail-DiscussSidebarChannel:text(jane)"); + await waitFor( + ".o-mail-DiscussSidebarChannel:text(jane):not(:has([title='Relevant to your expertise']))" + ); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/messaging_menu_patch.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/messaging_menu_patch.test.js new file mode 100644 index 0000000..342c38d --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/messaging_menu_patch.test.js @@ -0,0 +1,47 @@ +import { click, contains, patchUiSize, start, startServer } from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { Command, serverState } from "@web/../tests/web_test_helpers"; +import { defineLivechatModels } from "./livechat_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test('livechats should be in "chat" filter', async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await click(".o_menu_systray i[aria-label='Messages']"); + await contains(".o-mail-MessagingMenu button.fw-bold", { text: "Notifications" }); + await contains(".o-mail-NotificationItem", { text: "Visitor 11" }); + await click(".o-mail-MessagingMenu button", { text: "Chats" }); + await contains(".o-mail-MessagingMenu button.fw-bold", { text: "Chats" }); + await contains(".o-mail-NotificationItem", { text: "Visitor 11" }); +}); + +test('livechats should be in "livechat" tab in mobile', async () => { + patchUiSize({ height: 360, width: 640 }); + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await click(".o_menu_systray i[aria-label='Messages']"); + await click("button", { text: "Live Chats" }); + await contains(".o-mail-NotificationItem", { text: "Visitor 11" }); + await click("button", { text: "Chats" }); + await contains(".o-mail-NotificationItem", { count: 0, text: "Visitor 11" }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/messaging_service_patch.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/messaging_service_patch.test.js new file mode 100644 index 0000000..ced6884 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/messaging_service_patch.test.js @@ -0,0 +1,60 @@ +import { + contains, + listenStoreFetch, + start, + startServer, + waitStoreFetch, +} from "@mail/../tests/mail_test_helpers"; +import { withGuest } from "@mail/../tests/mock_server/mail_mock_server"; +import { describe, test } from "@odoo/hoot"; +import { Command, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers"; + +import { rpc } from "@web/core/network/rpc"; +import { defineLivechatModels } from "./livechat_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("push notifications are Odoo toaster on Android", async () => { + // Notifications without ServiceWorker in Chrome Android no longer work. + // This simulates Android Notification behavior by throwing a + // ServiceWorkerRegistration error as a fallback. + patchWithCleanup(window, { + Notification: class Notification { + static get permission() { + return "granted"; + } + constructor() { + throw new Error("ServiceWorkerRegistration error"); + } + }, + }); + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor" }); + const channelId = pyEnv["discuss.channel"].create({ + name: "Livechat 1", + channel_type: "livechat", + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + }); + listenStoreFetch("init_messaging"); + await start(); + await waitStoreFetch("init_messaging"); + // send after init_messaging because bus subscription is done after init_messaging + await withGuest(guestId, () => + rpc("/mail/message/post", { + post_data: { + body: "Hello world!", + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + thread_model: "discuss.channel", + thread_id: channelId, + }) + ); + await contains(".o_notification:has(.o_notification_bar.bg-info)", { + text: "Visitor. Hello world!", + }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mobile_messaging_menu_patch.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mobile_messaging_menu_patch.test.js new file mode 100644 index 0000000..76b3938 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mobile_messaging_menu_patch.test.js @@ -0,0 +1,42 @@ +import { describe, test } from "@odoo/hoot"; +import { + SIZES, + click, + contains, + patchUiSize, + start, + startServer, +} from "@mail/../tests/mail_test_helpers"; +import { Command, serverState } from "@web/../tests/web_test_helpers"; +import { defineLivechatModels } from "./livechat_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("Livechat button is not present when there is no livechat thread", async () => { + patchUiSize({ size: SIZES.SM }); + await start(); + await click(".o_menu_systray i[aria-label='Messages']"); + await contains(".o-mail-MessagingMenu"); + await contains(".o-mail-MessagingMenu-navbar span", { count: 0, text: "Livechat" }); +}); + +test("Livechat button is present when there is at least one livechat thread", async () => { + patchUiSize({ size: SIZES.SM }); + const pyEnv = await startServer(); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ + partner_id: serverState.publicPartnerId, + livechat_member_type: "visitor", + }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await click(".o_menu_systray i[aria-label='Messages']"); + await contains(".o-mail-MessagingMenu"); + await contains(".o-mail-MessagingMenu-navbar", { text: "Live Chats" }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/livechat_mock_server.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/livechat_mock_server.js new file mode 100644 index 0000000..821aba9 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/livechat_mock_server.js @@ -0,0 +1,265 @@ +import { + mailDataHelpers, + parseRequestParams, + registerRoute, +} from "@mail/../tests/mock_server/mail_mock_server"; +import { Command, makeKwArgs, serverState } from "@web/../tests/web_test_helpers"; +import { loadBundle } from "@web/core/assets"; +import { patch } from "@web/core/utils/patch"; + +/** + * @template [T={}] + * @typedef {import("@web/../tests/web_test_helpers").RouteCallback} RouteCallback + */ + +registerRoute("/im_livechat/get_session", get_session); +/** @type {RouteCallback} */ +async function get_session(request) { + /** @type {import("mock_models").DiscussChannel} */ + const DiscussChannel = this.env["discuss.channel"]; + /** @type {import("mock_models").LivechatChannel} */ + const LivechatChannel = this.env["im_livechat.channel"]; + /** @type {import("mock_models").ResCountry} */ + const ResCountry = this.env["res.country"]; + /** @type {import("mock_models").ResPartner} */ + const ResPartner = this.env["res.partner"]; + /** @type {import("mock_models").ResUsers} */ + const ResUsers = this.env["res.users"]; + + let { + channel_id, + previous_operator_id, + persisted, + context = {}, + } = await parseRequestParams(request); + previous_operator_id = parseInt(previous_operator_id); + const agent = LivechatChannel._get_operator(channel_id, previous_operator_id); + if (!agent) { + return false; + } + let country_id; + if (this.env.user && !ResUsers._is_public(this.env.uid)) { + country_id = this.env.user.country_id; + } else if (context.mockedCountryCode) { + // simulate geoip + const country = ResCountry._filter([["code", "=", context.mockedCountryCode]])[0]; + if (country) { + country_id = country.id; + } + } + if (!persisted) { + const store = new mailDataHelpers.Store(); + ResUsers._init_store_data(store); + store.add("discuss.channel", { + channel_type: "livechat", + fetchChannelInfoState: "fetched", + id: -1, + isLoaded: true, + livechat_operator_id: mailDataHelpers.Store.one( + ResPartner.browse(agent.partner_id), + makeKwArgs({ fields: ["avatar_128", "user_livechat_username"] }) + ), + scrollUnread: false, + }); + return { store_data: store.get_result(), channel_id: -1 }; + } + const channelVals = LivechatChannel._get_livechat_discuss_channel_vals(channel_id, { + agent: agent, + }); + channelVals.country_id = country_id; + const channelId = DiscussChannel.create(channelVals); + const store = new mailDataHelpers.Store(); + ResUsers._init_store_data(store); + store.add(DiscussChannel.browse(channelId)); + store.add(DiscussChannel.browse(channelId), { + isLoaded: true, + scrollUnread: false, + }); + return { store_data: store.get_result(), channel_id: channelId }; +} + +registerRoute("/im_livechat/visitor_leave_session", visitor_leave_session); +/** @type {RouteCallback} */ +async function visitor_leave_session(request) { + /** @type {import("mock_models").DiscussChannel} */ + const DiscussChannel = this.env["discuss.channel"]; + + const { channel_id } = await parseRequestParams(request); + const [channel] = DiscussChannel.search_read([["id", "=", channel_id]]); + if (!channel) { + return; + } + DiscussChannel._close_livechat_session(channel_id); +} +registerRoute("/im_livechat/feedback", feedback); +/** @type {RouteCallback} */ +async function feedback(request) { + /** @type {import("mock_models").DiscussChannel} */ + const DiscussChannel = this.env["discuss.channel"]; + /** @type {import("mock_models").RatingRating} */ + const RatingRating = this.env["rating.rating"]; + + const { channel_id, rate, reason } = await parseRequestParams(request); + let [channel] = DiscussChannel.search_read([["id", "=", channel_id]]); + if (!channel) { + return false; + } + const values = { + rating: rate, + consumed: true, + feedback: reason, + is_internal: false, + res_id: channel.id, + res_model: "discuss.channel", + rated_partner_id: channel.channel_partner_ids[0], + }; + if (channel.rating_ids.length === 0) { + RatingRating.create(values); + } else { + RatingRating.write([channel.rating_ids[0]], values); + } + [channel] = DiscussChannel.search_read([["id", "=", channel_id]]); + return channel.rating_ids[0]; +} + +registerRoute("/im_livechat/init", livechat_init); +/** @type {RouteCallback} */ +async function livechat_init(request) { + return { + available_for_me: true, + rule: {}, + }; +} + +registerRoute("/im_livechat/email_livechat_transcript", email_livechat_transcript); +/** @type {RouteCallback} */ +async function email_livechat_transcript(request) { + const DiscussChannel = this.env["discuss.channel"]; + const { channel_id, email } = await parseRequestParams(request); + const [channel] = DiscussChannel.search_read([["id", "=", channel_id]]); + if (!channel) { + return; + } + DiscussChannel._email_livechat_transcript(channel_id, email); +} + +registerRoute("/im_livechat/emoji_bundle", get_emoji_bundle); +/** @type {RouteCallback} */ +async function get_emoji_bundle(request) { + await loadBundle("web.assets_emoji"); + return new Response(); +} + +registerRoute("/im_livechat/session/update_status", session_update_status); +/** @type {RouteCallback} */ +async function session_update_status(request) { + /** @type {import("mock_models").DiscussChannel} */ + const DiscussChannel = this.env["discuss.channel"]; + const { channel_id, livechat_status } = await parseRequestParams(request); + if (this.env.user.share) { + return false; + } + const [channel] = DiscussChannel.search_read([["id", "=", channel_id]]); + if (!channel) { + return false; + } + DiscussChannel.write([channel_id], { + livechat_status: livechat_status, + }); + return true; +} + +registerRoute("/im_livechat/session/update_note", session_update_note); +/** @type {RouteCallback} */ +async function session_update_note(request) { + /** @type {import("mock_models").DiscussChannel} */ + const DiscussChannel = this.env["discuss.channel"]; + const { channel_id, note } = await parseRequestParams(request); + if (this.env.user.share) { + return false; + } + const [channel] = DiscussChannel.search_read([["id", "=", channel_id]]); + if (!channel) { + return false; + } + DiscussChannel.write([channel_id], { + livechat_note: note, + }); + return true; +} + +registerRoute("/im_livechat/conversation/write_expertises", livechat_conversation_write_expertises); +/** @type {RouteCallback} */ +async function livechat_conversation_write_expertises(request) { + /** @type {import("mock_models").DiscussChannel} */ + const DiscussChannel = this.env["discuss.channel"]; + const { channel_id, orm_commands } = await parseRequestParams(request); + const [channel] = DiscussChannel.search_read([["id", "=", channel_id]]); + if (!channel) { + return false; + } + DiscussChannel.write(channel_id, { livechat_expertise_ids: orm_commands }); +} + +registerRoute( + "/im_livechat/conversation/create_and_link_expertise", + livechat_conversation_create_and_link_expertise +); +/** @type {RouteCallback} */ +async function livechat_conversation_create_and_link_expertise(request) { + /** @type {import("mock_models").DiscussChannel} */ + const DiscussChannel = this.env["discuss.channel"]; + /** @type {import("mock_models").ImLivechatExpertise} */ + const ImLivechatExpertise = this.env["im_livechat.expertise"]; + const { channel_id, expertise_name } = await parseRequestParams(request); + const [channel] = DiscussChannel.search([["id", "=", channel_id]]); + if (!channel) { + return false; + } + const [expertise] = ImLivechatExpertise.search([["name", "=", expertise_name]]); + let expertiseId = expertise?.id; + if (!expertise) { + expertiseId = ImLivechatExpertise.create({ name: expertise_name }); + } + DiscussChannel.write(channel_id, { livechat_expertise_ids: [Command.link(expertiseId)] }); +} + +patch(mailDataHelpers, { + _process_request_for_all(store, name, params) { + const ResPartner = this.env["res.partner"]; + const ResUsers = this.env["res.users"]; + super._process_request_for_all(...arguments); + store.add({ livechat_available: true }); + if (name === "init_livechat") { + if (this.env.user && !ResUsers._is_public(this.env.uid)) { + store.add( + ResPartner.browse(this.env.user.partner_id), + makeKwArgs({ fields: ["email"] }) + ); + } + } + }, + _process_request_for_internal_user(store, name, params) { + super._process_request_for_internal_user(...arguments); + if (name === "im_livechat.channel") { + const LivechatChannel = this.env["im_livechat.channel"]; + store.add( + LivechatChannel.browse(LivechatChannel.search([])), + makeKwArgs({ fields: ["are_you_inside", "name"] }) + ); + return; + } + if (name === "/im_livechat/looking_for_help") { + const DiscussChannel = this.env["discuss.channel"]; + store.add( + DiscussChannel.browse( + DiscussChannel.search([["livechat_status", "=", "need_help"]]) + ) + ); + } + if (name === "/im_livechat/fetch_self_expertise") { + const ResUsers = this.env["res.users"]; + store.add(ResUsers.browse(serverState.userId), ["livechat_expertise_ids"]); + } + }, +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/@types/mock_models.d.ts b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/@types/mock_models.d.ts new file mode 100644 index 0000000..652beda --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/@types/mock_models.d.ts @@ -0,0 +1,11 @@ +declare module "mock_models" { + import { LivechatChannel as LivechatChannel2 } from "../im_livechat_channel"; + import { RatingRating as RatingRating2 } from "../rating_rating"; + + export interface LivechatChannel extends LivechatChannel2 {} + export interface RatingRating extends RatingRating2 {} + + export interface Models { + "im_livechat.channel": LivechatChannel, + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/discuss_channel.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/discuss_channel.js new file mode 100644 index 0000000..260794f --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/discuss_channel.js @@ -0,0 +1,221 @@ +import { mailModels } from "@mail/../tests/mail_test_helpers"; +import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server"; + +import { fields, getKwArgs, makeKwArgs, serverState } from "@web/../tests/web_test_helpers"; +import { serializeDate } from "@web/core/l10n/dates"; +import { ensureArray } from "@web/core/utils/arrays"; + +export class DiscussChannel extends mailModels.DiscussChannel { + livechat_channel_id = fields.Many2one({ relation: "im_livechat.channel", string: "Channel" }); // FIXME: somehow not fetched properly + livechat_lang_id = fields.Many2one({ relation: "res.lang", string: "Language" }); + livechat_note = fields.Html({ sanitize: true }); + livechat_status = fields.Selection({ + selection: [ + ("in_progress", "In progress"), + ("waiting", "Waiting for customer"), + ("need_help", "Looking for help"), + ], + }); + livechat_expertise_ids = fields.Many2many({ + relation: "im_livechat.expertise", + }); + + action_unfollow(idOrIds) { + /** @type {import("mock_models").BusBus} */ + const BusBus = this.env["bus.bus"]; + + const ids = ensureArray(idOrIds); + for (const channel_id of ids) { + const [channel] = this.browse(channel_id); + if (channel.channel_type == "livechat" && channel.channel_member_ids.length <= 2) { + this.write([channel.id], { livechat_end_dt: serializeDate(luxon.DateTime.now()) }); + BusBus._sendone( + channel, + "mail.record/insert", + new mailDataHelpers.Store() + .add(this.browse(channel_id), makeKwArgs({ fields: ["livechat_end_dt"] })) + .get_result() + ); + } + } + return super.action_unfollow(...arguments); + } + + /** + * @override + * @param {number[]} ids + * @param {number[]} partner_ids + * @param {boolean} [invite_to_rtc_call=undefined] + */ + add_members(ids, partner_ids, invite_to_rtc_call) { + const kwargs = getKwArgs(arguments, "ids", "partner_ids", "invite_to_rtc_call"); + ids = kwargs.ids; + delete kwargs.ids; + partner_ids = kwargs.partner_ids || []; + const channels = this.browse( + Array.from(super.add_members(ids, partner_ids, invite_to_rtc_call)).map( + ({ channel_id }) => channel_id + ) + ); + for (const channel of channels) { + if (channel.livechat_status == "need_help") { + this.write([channel.id], { livechat_status: "in_progress" }); + } + } + } + + _channel_basic_info_fields() { + return [ + ...super._channel_basic_info_fields(), + "livechat_lang_id", + "livechat_note", + "livechat_status", + "livechat_expertise_ids", + ]; + } + + /** + * @override + * @type {typeof mailModels.DiscussChannel["prototype"]["_to_store"]} + */ + _to_store(store) { + /** @type {import("mock_models").ResCountry} */ + const ResCountry = this.env["res.country"]; + /** @type {import("mock_models").ResLang} */ + const ResLang = this.env["res.lang"]; + /** @type {import("mock_models").ResPartner} */ + const ResPartner = this.env["res.partner"]; + + super._to_store(...arguments); + for (const channel of this) { + const channelInfo = {}; + const [country] = ResCountry.browse(channel.country_id); + channelInfo["country_id"] = country + ? { + code: country.code, + id: country.id, + name: country.name, + } + : false; + // add the last message date + if (channel.channel_type === "livechat") { + // add the operator id + if (channel.livechat_operator_id) { + // livechat_username ignored for simplicity + channelInfo.livechat_operator_id = mailDataHelpers.Store.one( + ResPartner.browse(channel.livechat_operator_id), + makeKwArgs({ fields: ["avatar_128", "user_livechat_username"] }) + ); + } else { + channelInfo.livechat_operator_id = false; + } + channelInfo.livechat_lang_id = channel.livechat_lang_id + ? mailDataHelpers.Store.one( + ResLang.browse(channel.livechat_lang_id), + makeKwArgs({ fields: ["name"] }) + ) + : false; + channelInfo["livechat_end_dt"] = channel.livechat_end_dt; + channelInfo["livechat_note"] = ["markup", channel.livechat_note]; + channelInfo["livechat_status"] = channel.livechat_status; + channelInfo["livechat_expertise_ids"] = mailDataHelpers.Store.many( + this.env["im_livechat.expertise"].browse(channel.livechat_expertise_ids), + makeKwArgs({ fields: ["name"] }) + ); + channelInfo.livechat_channel_id = mailDataHelpers.Store.one( + this.env["im_livechat.channel"].browse(channel.livechat_channel_id), + makeKwArgs({ fields: ["name"] }) + ); + } + store._add_record_fields(this.browse(channel.id), channelInfo); + } + } + _close_livechat_session(channel_id) { + /** @type {import("mock_models").BusBus} */ + const BusBus = this.env["bus.bus"]; + + if (this.browse(channel_id)[0].livechat_end_dt) { + return; + } + this.write([channel_id], { livechat_end_dt: serializeDate(luxon.DateTime.now()) }); + const [channel] = this.browse(channel_id); + BusBus._sendone( + channel, + "mail.record/insert", + new mailDataHelpers.Store() + .add(this.browse(channel_id), makeKwArgs({ fields: ["livechat_end_dt"] })) + .get_result() + ); + if (channel.message_ids.length === 0) { + return; + } + this.message_post( + channel.id, + makeKwArgs({ + body: this._get_visitor_leave_message(), + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }) + ); + } + _get_visitor_leave_message() { + return "Visitor left the conversation."; + } + + _email_livechat_transcript(channel_id, email) { + const [channel] = this.browse(channel_id); + this.message_post( + channel.id, + makeKwArgs({ + body: `
${this.env.user.name} sent the conversation to ${email}
`, + message_type: "notification", + subtype_xmlid: "mail.mt_comment", + }) + ); + } + + /** + * @override + * @type {typeof mailModels.DiscussChannel["prototype"]["_types_allowing_seen_infos"]} + */ + _types_allowing_seen_infos() { + return super._types_allowing_seen_infos(...arguments).concat(["livechat"]); + } + + livechat_join_channel_needing_help(idOrIds) { + const channel = this.browse(idOrIds)[0]; + if (channel.livechat_status !== "need_help") { + return false; + } + this.add_members([channel.id], [this.env.user.partner_id]); + return true; + } + + /** @type {typeof models.Model["prototype"]["write"]} */ + write(idOrIds, values) { + const kwargs = getKwArgs(arguments, "ids", "vals"); + ({ ids: idOrIds, vals: values } = kwargs); + const needHelpBefore = []; + for (const channel of this._filter([["livechat_status", "=", "need_help"]])) { + needHelpBefore.push(channel.id); + } + const result = super.write(...arguments); + const needHelpAfter = []; + for (const channel of this._filter([["livechat_status", "=", "need_help"]])) { + needHelpAfter.push(channel.id); + } + const updatedChannelIds = [ + ...needHelpBefore.filter((id) => !needHelpAfter.includes(id)), + ...needHelpAfter.filter((id) => !needHelpBefore.includes(id)), + ]; + if (updatedChannelIds.length) { + this.env["bus.bus"]._sendone( + [this.env["res.groups"].browse(serverState.groupLivechatId), "LOOKING_FOR_HELP"], + "mail.record/insert", + new mailDataHelpers.Store().add(this.browse(updatedChannelIds)).get_result() + ); + } + + return result; + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/discuss_channel_member.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/discuss_channel_member.js new file mode 100644 index 0000000..dfb194e --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/discuss_channel_member.js @@ -0,0 +1,55 @@ +import { mailModels } from "@mail/../tests/mail_test_helpers"; +import { fields } from "@web/../tests/web_test_helpers"; + +export class DiscussChannelMember extends mailModels.DiscussChannelMember { + livechat_member_type = fields.Selection({ + selection: [ + ["agent", "Agent"], + ["visitor", "Visitor"], + ["bot", "Chatbot"], + ], + compute: false, + }); + /** + * @override + * @type {typeof mailModels.DiscussChannelMember["prototype"]["_get_store_partner_fields"]} + */ + _get_store_partner_fields(fields) { + /** @type {import("mock_models").DiscussChannel} */ + const DiscussChannel = this.env["discuss.channel"]; + + const member = this[0]; + const [channel] = DiscussChannel.browse(member.channel_id); + if (channel.channel_type === "livechat") { + if (!fields) { + fields = [ + "active", + "avatar_128", + "country_id", + "im_status", + "is_public", + "user_livechat_username", + ]; + if (member.livechat_member_type == "visitor") { + fields.push("offline_since", "email"); + } + } + } + return super._get_store_partner_fields(fields); + } + /** + * @override + * @type {typeof mailModels.DiscussChannelMember["prototype"]["_to_store"]} + */ + _to_store(store, fields, extra_fields) { + super._to_store(...arguments); + for (const member of this) { + store._add_record_fields(this.browse(member.id), { + livechat_member_type: member.livechat_member_type, + }); + } + } + get _to_store_defaults() { + return super._to_store_defaults.concat(["livechat_member_type"]); + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/im_livechat_channel.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/im_livechat_channel.js new file mode 100644 index 0000000..fdcded0 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/im_livechat_channel.js @@ -0,0 +1,124 @@ +import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server"; + +import { Command, fields, getKwArgs, makeKwArgs, models } from "@web/../tests/web_test_helpers"; + +export class LivechatChannel extends models.ServerModel { + _name = "im_livechat.channel"; + + available_operator_ids = fields.Many2many({ relation: "res.users" }); // FIXME: somehow not fetched properly + user_ids = fields.Many2many({ relation: "res.users" }); // FIXME: somehow not fetched properly + + /** @param {integer} id */ + action_join(id) { + this.write([id], { user_ids: [Command.link(this.env.user.id)] }); + const [partner] = this.env["res.partner"].read(this.env.user.partner_id); + this.env["bus.bus"]._sendone( + partner, + "mail.record/insert", + new mailDataHelpers.Store( + this.browse(id), + makeKwArgs({ fields: ["are_you_inside", "name"] }) + ).get_result() + ); + } + + /** @param {integer} id */ + action_quit(id) { + this.write(id, { user_ids: [Command.unlink(this.env.user.id)] }); + const [partner] = this.env["res.partner"].read(this.env.user.partner_id); + this.env["bus.bus"]._sendone( + partner, + "mail.record/insert", + new mailDataHelpers.Store( + this.browse(id), + makeKwArgs({ fields: ["are_you_inside", "name"] }) + ).get_result() + ); + } + + /** @param {integer} id */ + _compute_available_operator_ids(id) { + /** @type {import("mock_models").ResUsers} */ + const ResUsers = this.env["res.users"]; + + const [livechatChannel] = this.browse(id); + const users = ResUsers.browse(livechatChannel.user_ids); + return users.filter((user) => user.im_status === "online"); + } + /** @param {integer} id */ + _get_livechat_discuss_channel_vals(id, operator_info) { + /** @type {import("mock_models").MailGuest} */ + const MailGuest = this.env["mail.guest"]; + /** @type {import("mock_models").ResUsers} */ + const ResUsers = this.env["res.users"]; + const agent = operator_info["agent"]; + + const membersToAdd = [ + Command.create({ + last_interest_dt: "2021-01-01 10:00:00", + livechat_member_type: "agent", + partner_id: agent.partner_id, + unpin_dt: "2021-01-01 12:00:00", + }), + ]; + const guest = ResUsers._is_public(this.env.uid) && MailGuest._get_guest_from_context(); + if (guest) { + membersToAdd.push( + Command.create({ guest_id: guest.id, livechat_member_type: "visitor" }) + ); + } + let visitorUser; + if (this.env.user && !ResUsers._is_public(this.env.uid) && this.env.user !== agent) { + visitorUser = this.env.user; + membersToAdd.push( + Command.create({ + livechat_member_type: "visitor", + partner_id: visitorUser.partner_id, + }) + ); + } + const membersName = [ + visitorUser ? visitorUser.display_name : guest.name, + agent.livechat_username ? agent.livechat_username : agent.name, + ]; + return { + channel_partner_ids: [agent.partner_id], + channel_member_ids: membersToAdd, + livechat_operator_id: agent.partner_id, + livechat_channel_id: id, + livechat_status: "in_progress", + channel_type: "livechat", + name: membersName.join(" "), + }; + } + /** + * Simplified mock implementation: returns + * the previous operator if he is still available + * or the first available operator. + * + * @param {integer} id + */ + _get_operator(id, previous_operator_id) { + const availableUsers = this._compute_available_operator_ids(id); + return ( + availableUsers.find((operator) => operator.partner_id === previous_operator_id) ?? + availableUsers[0] + ); + } + + _to_store(store, fields) { + const kwargs = getKwArgs(arguments, "store", "fields"); + fields = kwargs.fields; + store._add_record_fields( + this, + fields.filter((field) => field !== "are_you_inside") + ); + for (const livechatChannel of this) { + if (fields.includes("are_you_inside")) { + store._add_record_fields(this.browse(livechatChannel.id), { + are_you_inside: livechatChannel.user_ids.includes(this.env.user.id), + }); + } + } + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/im_livechat_expertise.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/im_livechat_expertise.js new file mode 100644 index 0000000..ca9c03c --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/im_livechat_expertise.js @@ -0,0 +1,5 @@ +import { models } from "@web/../tests/web_test_helpers"; + +export class Im_LivechatExpertise extends models.ServerModel { + _name = "im_livechat.expertise"; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/ir_websocket.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/ir_websocket.js new file mode 100644 index 0000000..52f20f9 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/ir_websocket.js @@ -0,0 +1,23 @@ +import { mailModels } from "@mail/../tests/mail_test_helpers"; + +import { serverState } from "@web/../tests/web_test_helpers"; + +export class IrWebSocket extends mailModels.IrWebSocket { + /** + * @override + * @type {typeof busModels.IrWebSocket["prototype"]["_build_bus_channel_list"]} + */ + _build_bus_channel_list(channels) { + channels = [...super._build_bus_channel_list(channels)]; + const result = channels; + for (const channel of channels) { + if (channel === "im_livechat.looking_for_help") { + result.push([ + this.env["res.groups"].browse(serverState.groupLivechatId)[0], + "LOOKING_FOR_HELP", + ]); + } + } + return result.filter((channel) => channel !== "im_livechat.looking_for_help"); + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/livechat_channel_rule.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/livechat_channel_rule.js new file mode 100644 index 0000000..9601c5d --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/livechat_channel_rule.js @@ -0,0 +1,5 @@ +import { models } from "@web/../tests/web_test_helpers"; + +export class LivechatChannelRule extends models.ServerModel { + _name = "im_livechat.channel.rule"; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/mail_message.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/mail_message.js new file mode 100644 index 0000000..cd63df1 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/mail_message.js @@ -0,0 +1,39 @@ +import { mailModels } from "@mail/../tests/mail_test_helpers"; +import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server"; + +import { makeKwArgs } from "@web/../tests/web_test_helpers"; + +export class MailMessage extends mailModels.MailMessage { + _author_to_store(ids, store) { + /** @type {import("mock_models").DiscussChannel} */ + const DiscussChannel = this.env["discuss.channel"]; + /** @type {import("mock_models").MailMessage} */ + const MailMessage = this.env["mail.message"]; + /** @type {import("mock_models").ResPartner} */ + const ResPartner = this.env["res.partner"]; + + const messages_w_author_livechat = MailMessage.browse(ids).filter((message) => { + if (!message.author_id || message.model !== "discuss.channel" || !message.res_id) { + return false; + } + const channel = DiscussChannel.browse(message.res_id); + return channel.channel_type === "livechat"; + }); + super._author_to_store( + ids.filter( + (id) => !messages_w_author_livechat.map((message) => message.id).includes(id) + ), + store + ); + for (const message of messages_w_author_livechat) { + store.add(this.browse(message.id), { + author_id: mailDataHelpers.Store.one( + ResPartner.browse(message.author_id), + makeKwArgs({ + fields: ["avatar_128", "is_company", "user_livechat_username", "user"], + }) + ), + }); + } + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/res_groups.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/res_groups.js new file mode 100644 index 0000000..1317c6f --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/res_groups.js @@ -0,0 +1,18 @@ +import { serverState } from "@web/../tests/web_test_helpers"; +import { mailModels } from "@mail/../tests/mail_test_helpers"; + +export class ResGroups extends mailModels.ResGroups { + _records = [ + ...this._records, + { + id: serverState.groupLivechatId, + name: "Livechat User", + privilege_id: false, + }, + { + id: serverState.groupLivechatManagerId, + name: "Livechat Manager", + privilege_id: false, + }, + ]; +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/res_groups_privilege.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/res_groups_privilege.js new file mode 100644 index 0000000..b776c52 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/res_groups_privilege.js @@ -0,0 +1,3 @@ +import { webModels } from "@web/../tests/web_test_helpers"; + +export class ResGroupsPrivilege extends webModels.ResGroupsPrivilege {} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/res_partner.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/res_partner.js new file mode 100644 index 0000000..3ca4ec9 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/res_partner.js @@ -0,0 +1,79 @@ +import { mailModels } from "@mail/../tests/mail_test_helpers"; + +import { getKwArgs, makeKwArgs, serverState } from "@web/../tests/web_test_helpers"; + +export class ResPartner extends mailModels.ResPartner { + /** + * @override + * @type {typeof mailModels.ResPartner["prototype"]["_search_for_channel_invite_to_store"]} + */ + _search_for_channel_invite_to_store(ids, store, channel_id) { + /** @type {import("mock_models").DiscussChannel} */ + const DiscussChannel = this.env["discuss.channel"]; + /** @type {import("mock_models").DiscussChannelMember} */ + const DiscussChannelMember = this.env["discuss.channel.member"]; + /** @type {import("mock_models").LivechatChannel} */ + const LivechatChannel = this.env["im_livechat.channel"]; + /** @type {import("mock_models").Im_LivechatExpertise} */ + const Im_LivechatExpertise = this.env["im_livechat.expertise"]; + /** @type {import("mock_models").ResLang} */ + const ResLang = this.env["res.lang"]; + /** @type {import("mock_models").ResPartner} */ + const ResPartner = this.env["res.partner"]; + /** @type {import("mock_models").ResUsers} */ + const ResUsers = this.env["res.users"]; + + super._search_for_channel_invite_to_store(ids, store, channel_id); + const [channel] = DiscussChannel.browse(channel_id); + if (channel?.channel_type !== "livechat") { + return; + } + const activeLivechatPartners = LivechatChannel._filter([]) + .map(({ available_operator_ids }) => available_operator_ids) + .flat() + .map((userId) => ResUsers.browse(userId)[0].partner_id); + for (const partner of ResPartner.browse(ids)) { + const data = { + invite_by_self_count: DiscussChannelMember.search_count([ + ["partner_id", "=", partner.id], + ["create_uid", "=", serverState.userId], + ]), + is_available: activeLivechatPartners.includes(partner.id), + }; + if (partner.lang) { + data.lang_name = ResLang.search_read([["code", "=", partner.lang]])[0].name; + } + if (partner.user_ids.length) { + const [user] = ResUsers.browse(partner.user_ids[0]); + if (user) { + const userLangs = user.livechat_lang_ids + .map((langId) => ResLang.browse(langId)[0]) + .filter((lang) => lang.name !== data.lang_name); + data.livechat_languages = userLangs.map((lang) => lang.name); + data.livechat_expertise = user.livechat_expertise_ids.map( + (expId) => Im_LivechatExpertise.browse(expId)[0].name + ); + } + } + store.add(this.browse(partner.id), makeKwArgs({ fields: ["user_livechat_username"] })); + store.add(this.browse(partner.id), data); + store.add(this.browse(partner.id), makeKwArgs({ extra_fields: ["is_in_call"] })); + } + } + /** + * @override + * @type {typeof mailModels.ResPartner["prototype"]["_to_store"]} + */ + _to_store(store, fields) { + const kwargs = getKwArgs(arguments, "store", "fields"); + fields = kwargs.fields; + + super._to_store(...arguments); + if (fields && fields.includes("user_livechat_username")) { + store._add_record_fields( + this.filter((partner) => !partner.user_livechat_username), + ["name"] + ); + } + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/res_users.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/res_users.js new file mode 100644 index 0000000..b6a2d47 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/mock_server/mock_models/res_users.js @@ -0,0 +1,19 @@ +import { mailModels } from "@mail/../tests/mail_test_helpers"; +import { serverState } from "@web/../tests/web_test_helpers"; + +export class ResUsers extends mailModels.ResUsers { + /** + * @override + */ + _init_store_data(store) { + super._init_store_data(...arguments); + store.add({ + has_access_livechat: this.env.user?.group_ids.includes(serverState.groupLivechatId), + }); + store.add(this.browse(this.env.uid), { + is_livechat_manager: this.env.user?.group_ids.includes( + serverState.groupLivechatManagerId + ), + }); + } +} diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js deleted file mode 100644 index c88451b..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js +++ /dev/null @@ -1,52 +0,0 @@ -/** @odoo-module **/ - -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; - -QUnit.module('im_livechat', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('chat_window_manager', {}, function () { -QUnit.module('chat_window_manager_tests.js'); - -QUnit.test('closing a chat window with no message from admin side unpins it', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create( - { - channel_member_ids: [ - [0, 0, { - is_pinned: true, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: "livechat", - uuid: 'channel-10-uuid', - }, - ); - const { messaging } = await start(); - - await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); - await afterNextRender(() => document.querySelector(`.o_NotificationList_preview`).click()); - await afterNextRender(() => document.querySelector(`.o_ChatWindowHeader_commandClose`).click()); - const channels = await messaging.rpc({ - model: 'mail.channel', - method: 'channel_info', - args: [mailChannelId1], - }, { shadow: true }); - assert.strictEqual( - channels[0].is_pinned, - false, - 'Livechat channel should not be pinned', - ); -}); - -}); -}); -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/composer_tests.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/composer_tests.js deleted file mode 100644 index 7bd9c39..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/composer_tests.js +++ /dev/null @@ -1,50 +0,0 @@ -/** @odoo-module **/ - -import { start, startServer } from '@mail/../tests/helpers/test_utils'; - -QUnit.module('im_livechat', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('composer_tests.js'); - -QUnit.test('livechat: no add attachment button', async function (assert) { - // Attachments are not yet supported in livechat, especially from livechat - // visitor PoV. This may likely change in the future with task-2029065. - assert.expect(2); - - const pyEnv = await startServer(); - const livechatId = pyEnv['mail.channel'].create({ channel_type: 'livechat' }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: livechatId }, - }, - }); - await openDiscuss(); - assert.containsOnce(document.body, '.o_Composer', "should have a composer"); - assert.containsNone( - document.body, - '.o_Composer_buttonAttachment', - "composer linked to livechat should not have a 'Add attachment' button" - ); -}); - -QUnit.test('livechat: disable attachment upload via drag and drop', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const livechatId = pyEnv['mail.channel'].create({ channel_type: 'livechat' }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: livechatId }, - }, - }); - await openDiscuss(); - assert.containsOnce(document.body, '.o_Composer', "should have a composer"); - assert.containsNone( - document.body, - '.o_Composer_dropZone', - "composer linked to livechat should not have a dropzone" - ); -}); - -}); -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/discuss_sidebar_category_item_tests.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/discuss_sidebar_category_item_tests.js deleted file mode 100644 index 9f357ba..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/discuss_sidebar_category_item_tests.js +++ /dev/null @@ -1,77 +0,0 @@ -/** @odoo-module **/ - -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; - -QUnit.module('im_livechat', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_sidebar_category_item_tests.js'); - -QUnit.test('livechat - avatar: should have a smiley face avatar for an anonymous livechat item', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - const livechatItem = document.querySelector(` - .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] - `); - assert.containsOnce( - livechatItem, - `.o_DiscussSidebarCategoryItem_image`, - "should have an avatar" - ); - assert.strictEqual( - livechatItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src, - '/mail/static/src/img/smiley/avatar.jpg', - 'should have the smiley face as the avatar for anonymous users' - ); -}); - -QUnit.test('livechat - avatar: should have a partner profile picture for a livechat item linked with a partner', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - name: "Jean", - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - const livechatItem = document.querySelector(` - .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] - `); - assert.containsOnce( - livechatItem, - `.o_DiscussSidebarCategoryItem_image`, - "should have an avatar" - ); - assert.strictEqual( - livechatItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src, - `/web/image/res.partner/${resPartnerId1}/avatar_128`, - 'should have the partner profile picture as the avatar for partners' - ); -}); - -}); -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/discuss_sidebar_category_tests.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/discuss_sidebar_category_tests.js deleted file mode 100644 index bc31c3b..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/discuss_sidebar_category_tests.js +++ /dev/null @@ -1,449 +0,0 @@ -/** @odoo-module **/ - -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; - -QUnit.module('im_livechat', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_sidebar_category_tests.js'); - -QUnit.test('livechat - counter: should not have a counter if the category is unfolded and without unread messages', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.containsNone( - document.body, - `.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategory_counter`, - "should not have a counter if the category is unfolded and without unread messages", - ); -}); - -QUnit.test('livechat - counter: should not have a counter if the category is unfolded and with unread messages', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { - message_unread_counter: 10, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.containsNone( - document.body, - `.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategory_counter`, - "should not have a counter if the category is unfolded and with unread messages", - ); -}); - -QUnit.test('livechat - counter: should not have a counter if category is folded and without unread messages', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_livechat_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - assert.containsNone( - document.body, - `.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategory_counter`, - "should not have a counter if the category is folded and without unread messages" - ); -}); - -QUnit.test('livechat - counter: should have correct value of unread threads if category is folded and with unread messages', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { - message_unread_counter: 10, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_livechat_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - assert.strictEqual( - document.querySelector(`.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategory_counter`).textContent, - "1", - "should have correct value of unread threads if category is folded and with unread messages" - ); -}); - -QUnit.test('livechat - states: close manually by clicking the title', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_livechat_open: true, - }); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` - ); - - // fold the livechat category - await afterNextRender(() => - document.querySelector(`.o_DiscussSidebarCategory[data-category-local-id="${ - messaging.discuss.categoryLivechat.localId}"] - .o_DiscussSidebarCategory_title - `).click() - ); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category livechat should be closed and the content should be invisible" - ); -}); - -QUnit.test('livechat - states: open manually by clicking the title', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_livechat_open: false, - }); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` - ); - - // open the livechat category - await afterNextRender(() => - document.querySelector(`.o_DiscussSidebarCategory[data-category-local-id="${ - messaging.discuss.categoryLivechat.localId}"] - .o_DiscussSidebarCategory_title - `).click() - ); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category livechat should be open and the content should be visible" - ); -}); - -QUnit.test('livechat - states: close should update the value on the server', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_livechat_open: true, - }); - const currentUserId = pyEnv.currentUserId; - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - - const initalSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - initalSettings.is_discuss_sidebar_category_livechat_open, - true, - "the value in server side should be true" - ); - - await afterNextRender(() => - document.querySelector(`.o_DiscussSidebarCategory[data-category-local-id="${ - messaging.discuss.categoryLivechat.localId}"] - .o_DiscussSidebarCategory_title - `).click() - ); - const newSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - newSettings.is_discuss_sidebar_category_livechat_open, - false, - "the value in server side should be false" - ); -}); - -QUnit.test('livechat - states: open should update the value on the server', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_livechat_open: false, - }); - const currentUserId = pyEnv.currentUserId; - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - - const initalSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - initalSettings.is_discuss_sidebar_category_livechat_open, - false, - "the value in server side should be false" - ); - - await afterNextRender(() => - document.querySelector(`.o_DiscussSidebarCategory[data-category-local-id="${ - messaging.discuss.categoryLivechat.localId}"] - .o_DiscussSidebarCategory_title - `).click() - ); - const newSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - newSettings.is_discuss_sidebar_category_livechat_open, - true, - "the value in server side should be true" - ); -}); - -QUnit.test('livechat - states: close from the bus', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - const resUsersSettingsId1 = pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_livechat_open: true, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` - ); - - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'res.users.settings/insert', { - id: resUsersSettingsId1, - 'is_discuss_sidebar_category_livechat_open': false, - }); - }); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category livechat should be closed and the content should be invisible" - ); -}); - -QUnit.test('livechat - states: open from the bus', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - const resUsersSettingsId1 = pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_livechat_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` - ); - - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'res.users.settings/insert', { - id: resUsersSettingsId1, - 'is_discuss_sidebar_category_livechat_open': true, - }); - }); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category livechat should be open and the content should be visible" - ); -}); - - -QUnit.test('livechat - states: category item should be invisible if the category is closed', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` - ); - - await afterNextRender(() => - document.querySelector(`.o_DiscussSidebarCategory[data-category-local-id="${ - messaging.discuss.categoryLivechat.localId}"] - .o_DiscussSidebarCategory_title - `).click() - ); - - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "inactive item should be invisible if the category is folded" - ); -}); - -QUnit.test('livechat - states: the active category item should be visble even if the category is closed', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` - ); - - const livechat = document.querySelector(`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`); - await afterNextRender(() => { - livechat.click(); - }); - assert.ok(livechat.classList.contains('o-active')); - - await afterNextRender(() => - document.querySelector(`.o_DiscussSidebarCategory[data-category-local-id="${ - messaging.discuss.categoryLivechat.localId}"] - .o_DiscussSidebarCategory_title - `).click() - ); - - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - 'the active livechat item should remain open even if the category is folded' - ); -}); - -}); -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/discuss_tests.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/discuss_tests.js deleted file mode 100644 index db83b08..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/discuss_tests.js +++ /dev/null @@ -1,435 +0,0 @@ -/** @odoo-module **/ - -import { - afterNextRender, - nextAnimationFrame, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; - -import { datetime_to_str } from 'web.time'; - -QUnit.module('im_livechat', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_tests.js'); - -QUnit.test('livechat in the sidebar: basic rendering', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce(document.body, '.o_Discuss_sidebar', - "should have a sidebar section" - ); - const groupLivechat = document.querySelector('.o_DiscussSidebar_categoryLivechat'); - assert.ok(groupLivechat, - "should have a channel group livechat" - ); - const titleText = groupLivechat.querySelector('.o_DiscussSidebarCategory_titleText'); - assert.strictEqual( - titleText.textContent.trim(), - "Livechat", - "should have a channel group named 'Livechat'" - ); - const livechat = groupLivechat.querySelector(` - .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] - `); - assert.ok( - livechat, - "should have a livechat in sidebar" - ); - assert.strictEqual( - livechat.textContent, - "Visitor 11", - "should have 'Visitor 11' as livechat name" - ); -}); - -QUnit.test('livechat in the sidebar: existing user with country', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resCountryId1 = pyEnv['res.country'].create({ - code: 'be', - name: "Belgium", - }); - const resPartnerId1 = pyEnv['res.partner'].create({ - country_id: resCountryId1, - name: "Jean", - }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_DiscussSidebar_categoryLivechat', - "should have a channel group livechat in the side bar" - ); - const livechat = document.querySelector('.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategoryItem'); - assert.ok( - livechat, - "should have a livechat in sidebar" - ); - assert.strictEqual( - livechat.textContent, - "Jean (Belgium)", - "should have user name and country as livechat name" - ); -}); - -QUnit.test('do not add livechat in the sidebar on visitor opening his chat', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - pyEnv['res.users'].write([pyEnv.currentUserId], { im_status: 'online' }); - const imLivechatChannelId1 = pyEnv['im_livechat.channel'].create({ - user_ids: [pyEnv.currentUserId], - }); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_DiscussSidebar_categoryLivechat', - "should not have any livechat in the sidebar initially" - ); - - // simulate livechat visitor opening his chat - await messaging.rpc({ - route: '/im_livechat/get_session', - params: { - context: { - mockedUserId: false, - }, - channel_id: imLivechatChannelId1, - }, - }); - await nextAnimationFrame(); - assert.containsNone( - document.body, - '.o_DiscussSidebar_categoryLivechat', - "should still not have any livechat in the sidebar after visitor opened his chat" - ); -}); - -QUnit.test('do not add livechat in the sidebar on visitor typing', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - pyEnv['res.users'].write([pyEnv.currentUserId], { im_status: 'online' }); - const imLivechatChannelId1 = pyEnv['im_livechat.channel'].create({ - user_ids: [pyEnv.currentUserId], - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - is_pinned: false, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_channel_id: imLivechatChannelId1, - livechat_operator_id: pyEnv.currentPartnerId, - }); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_DiscussSidebar_categoryLivechat', - "should not have any livechat in the sidebar initially" - ); - - // simulate livechat visitor typing - const channel = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; - await messaging.rpc({ - route: '/im_livechat/notify_typing', - params: { - context: { - mockedPartnerId: pyEnv.publicPartnerId, - }, - is_typing: true, - uuid: channel.uuid, - }, - }); - await nextAnimationFrame(); - assert.containsNone( - document.body, - '.o_DiscussSidebar_categoryLivechat', - "should still not have any livechat in the sidebar after visitor started typing" - ); -}); - -QUnit.test('add livechat in the sidebar on visitor sending first message', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - pyEnv['res.users'].write([pyEnv.currentUserId], { im_status: 'online' }); - const resCountryId1 = pyEnv['res.country'].create({ - code: 'be', - name: "Belgium", - }); - const imLivechatChannelId1 = pyEnv['im_livechat.channel'].create({ - user_ids: [pyEnv.currentUserId], - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor (Belgium)", - channel_member_ids: [ - [0, 0, { - is_pinned: false, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - country_id: resCountryId1, - livechat_channel_id: imLivechatChannelId1, - livechat_operator_id: pyEnv.currentPartnerId, - }); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.containsNone( - document.body, - '.o_DiscussSidebar_categoryLivechat', - "should not have any livechat in the sidebar initially" - ); - - // simulate livechat visitor sending a message - const channel = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; - await afterNextRender(async () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: false, - }, - uuid: channel.uuid, - message_content: "new message", - }, - })); - assert.containsOnce( - document.body, - '.o_DiscussSidebar_categoryLivechat', - "should have a channel group livechat in the side bar after receiving first message" - ); - assert.containsOnce( - document.body, - '.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategoryItem', - "should have a livechat in the sidebar after receiving first message" - ); - assert.strictEqual( - document.querySelector('.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategoryItem .o_DiscussSidebarCategoryItem_name').textContent, - "Visitor (Belgium)", - "should have visitor name and country as livechat name" - ); -}); - -QUnit.test('livechats are sorted by last activity time in the sidebar: most recent at the top', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { - last_interest_dt: datetime_to_str(new Date(2021, 0, 1)), - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }, - { - anonymous_name: "Visitor 12", - channel_member_ids: [ - [0, 0, { - last_interest_dt: datetime_to_str(new Date(2021, 0, 2)), - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }, - ]); - const { openDiscuss } = await start(); - await openDiscuss(); - const initialLivechats = document.querySelectorAll('.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategory_item'); - assert.strictEqual( - initialLivechats.length, - 2, - "should have 2 livechat items" - ); - assert.strictEqual( - Number(initialLivechats[0].dataset.channelId), - mailChannelId2, - "first livechat should be the one with the more recent last activity time" - ); - assert.strictEqual( - Number(initialLivechats[1].dataset.channelId), - mailChannelId1, - "second livechat should be the one with the less recent last activity time" - ); - - // post a new message on the last channel - await afterNextRender(() => initialLivechats[1].click()); - await afterNextRender(() => document.execCommand('insertText', false, "Blabla")); - await afterNextRender(() => document.querySelector('.o_Composer_buttonSend').click()); - - const newLivechats = document.querySelectorAll('.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategory_item'); - assert.strictEqual( - newLivechats.length, - 2, - "should have 2 livechat items" - ); - assert.strictEqual( - Number(newLivechats[0].dataset.channelId), - mailChannelId1, - "first livechat should be the one with the more recent last activity time" - ); - assert.strictEqual( - Number(newLivechats[1].dataset.channelId), - mailChannelId2, - "second livechat should be the one with the less recent last activity time" - ); -}); - -QUnit.test('invite button should be present on livechat', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create( - { - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }, - ); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_ThreadViewTopbar_inviteButton', - "Invite button should be visible in top bar when livechat is active thread" - ); -}); - -QUnit.test('call buttons should not be present on livechat', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create( - { - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }, - ); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.containsNone( - document.body, - '.o_ThreadViewTopbar_callButton', - "Call buttons should not be visible in top bar when livechat is active thread" - ); -}); - -QUnit.test('reaction button should not be present on livechat', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - channel_partner_ids: [pyEnv.currentPartnerId, pyEnv.publicPartnerId], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInput_textarea', "Test"); - await click('.o_Composer_buttonSend'); - await click('.o_Message'); - assert.containsNone( - document.body, - '.o_MessageActionView_actionReaction', - "should not have action to add a reaction" - ); -}); - -QUnit.test('reply button should not be present on livechat', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - channel_partner_ids: [pyEnv.currentPartnerId, pyEnv.publicPartnerId], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInput_textarea', "Test"); - await click('.o_Composer_buttonSend'); - await click('.o_Message'); - assert.containsNone( - document.body, - '.o_MessageActionView_actionReplyTo', - "should not have reply action" - ); -}); - -}); -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/messaging_menu_tests.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/messaging_menu_tests.js deleted file mode 100644 index 2191dbe..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/messaging_menu_tests.js +++ /dev/null @@ -1,71 +0,0 @@ -/** @odoo-module **/ - -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; - -QUnit.module('im_livechat', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('messaging_menu_tests.js'); - -QUnit.test('livechats should be in "chat" filter', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 11", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - await start(); - assert.containsOnce( - document.body, - '.o_MessagingMenu', - "should have messaging menu" - ); - - await afterNextRender(() => document.querySelector('.o_MessagingMenu_toggler').click()); - assert.containsOnce( - document.body, - '.o_MessagingMenuTab[data-tab-id="all"]', - "should have a tab/filter 'all' in messaging menu" - ); - assert.containsOnce( - document.body, - '.o_MessagingMenuTab[data-tab-id="chat"]', - "should have a tab/filter 'chat' in messaging menu" - ); - assert.hasClass( - document.querySelector('.o_MessagingMenuTab[data-tab-id="all"]'), - 'o-active', - "tab/filter 'all' of messaging menu should be active initially" - ); - assert.containsOnce( - document.body, - `.o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]`, - "livechat should be listed in 'all' tab/filter of messaging menu" - ); - - await afterNextRender(() => - document.querySelector('.o_MessagingMenuTab[data-tab-id="chat"]').click() - ); - assert.hasClass( - document.querySelector('.o_MessagingMenuTab[data-tab-id="chat"]'), - 'o-active', - "tab/filter 'chat' of messaging menu should become active after click" - ); - assert.containsOnce( - document.body, - `.o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]`, - "livechat should be listed in 'chat' tab/filter of messaging menu" - ); -}); - -}); -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/thread_icon_tests.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/thread_icon_tests.js deleted file mode 100644 index 7fa88a5..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/thread_icon_tests.js +++ /dev/null @@ -1,68 +0,0 @@ -/** @odoo-module **/ - -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; - -QUnit.module('im_livechat', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('thread_icon_tests.js'); - -QUnit.test('livechat: public website visitor is typing', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 20", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_ThreadViewTopbar .o_ThreadIcon', - "should have thread icon" - ); - assert.containsOnce( - document.body, - '.o_ThreadIcon .fa.fa-comments', - "should have default livechat icon" - ); - - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; - // simulate receive typing notification from livechat visitor "is typing" - await afterNextRender(() => messaging.rpc({ - route: '/im_livechat/notify_typing', - params: { - context: { - mockedPartnerId: pyEnv.publicPartnerId, - }, - is_typing: true, - uuid: mailChannel1.uuid, - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadIcon_typing', - "should have thread icon with visitor currently typing" - ); - assert.strictEqual( - document.querySelector('.o_ThreadIcon_typing').title, - "Visitor 20 is typing...", - "title of icon should tell visitor is currently typing" - ); -}); - -}); -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/thread_textual_typing_status_tests.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/thread_textual_typing_status_tests.js deleted file mode 100644 index fc8e745..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/qunit_suite_tests/components/thread_textual_typing_status_tests.js +++ /dev/null @@ -1,59 +0,0 @@ -/** @odoo-module **/ - -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; - -QUnit.module('im_livechat', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('thread_textual_typing_status_tests.js'); - -QUnit.test('receive visitor typing status "is typing"', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - anonymous_name: "Visitor 20", - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: pyEnv.publicPartnerId }], - ], - channel_type: 'livechat', - livechat_operator_id: pyEnv.currentPartnerId, - }); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatus').textContent, - "", - "Should display no one is currently typing" - ); - - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; - // simulate receive typing notification from livechat visitor "is typing" - await afterNextRender(() => messaging.rpc({ - route: '/im_livechat/notify_typing', - params: { - context: { - mockedPartnerId: pyEnv.publicPartnerId, - }, - is_typing: true, - uuid: mailChannel1.uuid, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatus').textContent, - "Visitor 20 is typing...", - "Should display that visitor is typing" - ); -}); - -}); -}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/sidebar_patch.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/sidebar_patch.test.js new file mode 100644 index 0000000..94b9e40 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/sidebar_patch.test.js @@ -0,0 +1,469 @@ +import { waitForChannels } from "@bus/../tests/bus_test_helpers"; +import { + click, + contains, + insertText, + openDiscuss, + start, + startServer, +} from "@mail/../tests/mail_test_helpers"; +import { withGuest } from "@mail/../tests/mock_server/mail_mock_server"; +import { describe, test } from "@odoo/hoot"; +import { mockDate, tick } from "@odoo/hoot-mock"; +import { asyncStep, Command, serverState, waitForSteps } from "@web/../tests/web_test_helpers"; + +import { deserializeDateTime } from "@web/core/l10n/dates"; +import { rpc } from "@web/core/network/rpc"; +import { url } from "@web/core/utils/urls"; +import { defineLivechatModels } from "./livechat_test_helpers"; +import { press } from "@odoo/hoot-dom"; +import { browser } from "@web/core/browser/browser"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("Unknown visitor", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + await contains(".o-mail-DiscussSidebar .o-mail-DiscussSidebarCategory-livechat"); + await contains(".o-mail-DiscussSidebarChannel", { text: "Visitor 11" }); +}); + +test("Do not show channel when visitor is typing", async () => { + mockDate("2023-01-03 12:00:00"); // so that it's after last interest (mock server is in 2019 by default!) + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { im_status: "online" }); + const livechatChannelId = pyEnv["im_livechat.channel"].create({ + user_ids: [serverState.userId], + }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ + unpin_dt: "2021-01-01 12:00:00", + last_interest_dt: "2021-01-01 10:00:00", + livechat_member_type: "agent", + partner_id: serverState.partnerId, + }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + await contains(".o-mail-DiscussSidebarCategory", { count: 2 }); + await contains( + ".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container", + { + count: 0, + } + ); + // simulate livechat visitor typing + const channel = pyEnv["discuss.channel"].search_read([["id", "=", channelId]])[0]; + await withGuest(guestId, () => + rpc("/discuss/channel/notify_typing", { + is_typing: true, + channel_id: channel.id, + }) + ); + // weak test, no guaranteed that we waited long enough for the livechat to potentially appear + await tick(); + await contains( + ".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container", + { + count: 0, + } + ); +}); + +test("Smiley face avatar for livechat item linked to a guest", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + const guest = pyEnv["mail.guest"].search_read([["id", "=", guestId]])[0]; + await contains( + `.o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container img[data-src='${url( + `/web/image/mail.guest/${guestId}/avatar_128?unique=${ + deserializeDateTime(guest.write_date).ts + }` + )}']` + ); +}); + +test("Partner profile picture for livechat item linked to a partner", async () => { + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({ name: "Jean" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ partner_id: partnerId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(channelId); + const partner = pyEnv["res.partner"].search_read([["id", "=", partnerId]])[0]; + await contains( + `.o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container img[data-src='${url( + `/web/image/res.partner/${partnerId}/avatar_128?unique=${ + deserializeDateTime(partner.write_date).ts + }` + )}']` + ); +}); + +test("No counter if the category is unfolded and with unread messages", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ + message_unread_counter: 10, + livechat_member_type: "agent", + partner_id: serverState.partnerId, + }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + await contains(".o-mail-DiscussSidebarCategory-livechat"); + await contains(".o-mail-DiscussSidebarCategory-livechat .o-mail-Discuss-category-counter", { + count: 0, + }); +}); + +test("No counter if category is folded and without unread messages", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + await contains(".o-mail-DiscussSidebarCategory-livechat"); + await click(".o-mail-DiscussSidebarCategory-livechat .btn"); + await contains(".o-mail-DiscussSidebarCategory-livechat .o-discuss-badge", { count: 0 }); +}); + +test("Counter should have correct value of unread threads if category is folded and with unread messages", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ + livechat_member_type: "agent", + partner_id: serverState.partnerId, + }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + pyEnv["mail.message"].create({ + author_guest_id: guestId, + message_type: "comment", + model: "discuss.channel", + res_id: channelId, + }); + await start(); + await openDiscuss(); + // first, close the live chat category + await click(".o-mail-DiscussSidebarCategory-livechat .btn"); + await contains(".o-mail-DiscussSidebarCategory-livechat .o-discuss-badge", { text: "1" }); +}); + +test("Close manually by clicking the title", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + await contains( + ".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container" + ); + // fold the livechat category + await click(".o-mail-DiscussSidebarCategory-livechat .btn"); + await contains(".o-mail-DiscussSidebarChannel", { count: 0 }); +}); + +test("Open manually by clicking the title", async () => { + mockDate("2023-01-03 12:00:00"); + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + last_interest_dt: "2021-01-01 10:00:00", + livechat_member_type: "agent", + }), + Command.create({ + guest_id: guestId, + last_interest_dt: "2021-01-01 10:00:00", + livechat_member_type: "visitor", + }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + // first, close the live chat category + await click(".o-mail-DiscussSidebarCategory-livechat .btn"); + await contains(".o-mail-DiscussSidebarCategory-livechat"); + await contains( + ".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container", + { + count: 0, + } + ); + // open the livechat category + await click(".o-mail-DiscussSidebarCategory-livechat .btn"); + await contains( + ".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container" + ); +}); + +test("Category item should be invisible if the category is closed", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + await contains( + ".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container" + ); + await click(".o-mail-DiscussSidebarCategory-livechat .btn"); + await contains( + ".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container", + { + count: 0, + } + ); +}); + +test("Active category item should be visible even if the category is closed", async () => { + mockDate("2023-01-03 12:00:00"); + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + last_interest_dt: "2021-01-01 10:00:00", + livechat_member_type: "agent", + }), + Command.create({ + guest_id: guestId, + last_interest_dt: "2021-01-01 10:00:00", + livechat_member_type: "visitor", + }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + await click(".o-mail-DiscussSidebarChannel", { text: "Visitor 11" }); + await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 11" }); + await click(".o-mail-DiscussSidebarCategory-livechat .btn"); + await contains(".o-mail-DiscussSidebarChannel", { text: "Visitor 11" }); +}); + +test("Clicking on leave button leaves the channel", async () => { + const pyEnv = await startServer(); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ + guest_id: pyEnv["mail.guest"].create({ name: "Visitor 11" }), + livechat_member_type: "visitor", + }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + create_uid: serverState.publicUserId, + }); + await start(); + await openDiscuss(); + await contains(".o-mail-DiscussSidebarChannel", { text: "Visitor 11" }); + await click("[title='Chat Actions']"); + await click(".o-dropdown-item:contains('Leave Channel')"); + await click("button:contains(Leave Conversation)"); + await contains(".o-mail-DiscussSidebarChannel", { count: 0, text: "Visitor 11" }); +}); + +test("Message unread counter", async () => { + mockDate("2023-01-03 12:00:00"); + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + last_interest_dt: "2021-01-03 10:00:00", + livechat_member_type: "agent", + }), + Command.create({ + guest_id: guestId, + last_interest_dt: "2021-01-03 10:00:00", + livechat_member_type: "visitor", + }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + withGuest(guestId, () => + rpc("/mail/message/post", { + post_data: { + body: "hu", + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + thread_id: channelId, + thread_model: "discuss.channel", + }) + ); + await contains(".o-mail-DiscussSidebarChannel .badge", { text: "1" }); +}); + +test("unknown livechat can be displayed and interacted with", async () => { + mockDate("2023-01-03 12:00:00"); + const pyEnv = await startServer(); + pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" }); + const partnerId = pyEnv["res.partner"].create({ name: "Jane" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ + partner_id: partnerId, + last_interest_dt: "2021-01-01 10:00:00", + livechat_member_type: "agent", + }), + Command.create({ + guest_id: pyEnv["mail.guest"].create({ name: "Jane" }), + livechat_member_type: "visitor", + }), + ], + channel_type: "livechat", + livechat_operator_id: partnerId, + create_uid: serverState.publicUserId, + }); + const env = await start(); + env.services.bus_service.subscribe("discuss.channel/new_message", () => + asyncStep("discuss.channel/new_message") + ); + await openDiscuss("mail.box_inbox"); + await contains("button.o-active", { text: "Inbox" }); + await contains(".o-mail-DiscussSidebarCategory-livechat", { count: 0 }); + await contains(".o-mail-DiscussSidebarChannel", { count: 0 }); + await openDiscuss(channelId); + await waitForChannels([`discuss.channel_${channelId}`]); + await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Jane" }); + await insertText(".o-mail-Composer-input", "Hello", { replace: true }); + await press("Enter"); + await contains(".o-mail-Message", { text: "Hello" }); + await waitForSteps(["discuss.channel/new_message"]); + await click("button", { text: "Inbox" }); + await contains(".o-mail-DiscussSidebarChannel:not(.o-active)", { text: "Jane" }); + await click("[title='Chat Actions']"); + await click(".o-dropdown-item:contains('Leave Channel')"); + await contains(".o-mail-DiscussSidebarCategory-livechat", { count: 0 }); + await contains(".o-mail-DiscussSidebarChannel", { count: 0 }); +}); + +test("Local sidebar category state is shared between tabs", async () => { + const pyEnv = await startServer(); + pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ + guest_id: pyEnv["mail.guest"].create({ name: "Visitor #12" }), + livechat_member_type: "visitor", + }), + ], + livechat_operator_id: serverState.user, + }); + const env1 = await start({ asTab: true }); + const env2 = await start({ asTab: true }); + await openDiscuss(undefined, { target: env1 }); + await openDiscuss(undefined, { target: env2 }); + await contains(`${env1.selector} .o-mail-DiscussSidebarCategory-livechat .oi-chevron-down`); + await contains(`${env2.selector} .o-mail-DiscussSidebarCategory-livechat .oi-chevron-down`); + await click(`${env1.selector} .o-mail-DiscussSidebarCategory-livechat .btn`); + await contains(`${env1.selector} .o-mail-DiscussSidebarCategory-livechat .oi-chevron-right`); + await contains(`${env2.selector} .o-mail-DiscussSidebarCategory-livechat .oi-chevron-right`); +}); + +test("live chat is displayed below its category", async () => { + const pyEnv = await startServer(); + const livechatChannelId = pyEnv["im_livechat.channel"].create({ name: "Helpdesk" }); + browser.localStorage.setItem( + `discuss_sidebar_category_im_livechat.category_${livechatChannelId}_open`, + false + ); + pyEnv["discuss.channel"].create({ + channel_type: "livechat", + livechat_channel_id: livechatChannelId, + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ + guest_id: pyEnv["mail.guest"].create({ name: "Visitor #12" }), + livechat_member_type: "visitor", + }), + ], + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(); + await click(".o-mail-DiscussSidebarCategory .btn", { text: "Helpdesk" }); + await contains( + ".o-mail-DiscussSidebarCategory:contains(Helpdesk) + .o-mail-DiscussSidebarChannel-container:contains(Visitor #12)" + ); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/suggestions.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/suggestions.test.js new file mode 100644 index 0000000..76d04d7 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/suggestions.test.js @@ -0,0 +1,91 @@ +import { describe, test } from "@odoo/hoot"; +import { + click, + contains, + insertText, + openDiscuss, + start, + startServer, +} from "@mail/../tests/mail_test_helpers"; +import { Command, serverState } from "@web/../tests/web_test_helpers"; +import { defineLivechatModels } from "./livechat_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("Suggestions are shown after delimiter was used in text (::)", async () => { + const pyEnv = await startServer(); + pyEnv["mail.canned.response"].create({ + source: "hello", + substitution: "Hello dear customer, how may I help you?", + }); + const channelId = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ + partner_id: serverState.publicPartnerId, + livechat_member_type: "visitor", + }), + ], + }); + await start(); + await openDiscuss(channelId); + await insertText(".o-mail-Composer-input", "::"); + await contains(".o-mail-Composer-suggestion strong", { text: "hello" }); + await insertText(".o-mail-Composer-input", ")"); + await contains(".o-mail-Composer-suggestion strong", { count: 0 }); + await insertText(".o-mail-Composer-input", " ::"); + await contains(".o-mail-Composer-suggestion strong", { text: "hello" }); +}); + +test("Cannot mention other channels in a livechat", async () => { + const pyEnv = await startServer(); + const [channelId] = pyEnv["discuss.channel"].create([ + { + channel_type: "livechat", + channel_member_ids: [ + Command.create({ + partner_id: serverState.partnerId, + livechat_member_type: "agent", + }), + Command.create({ + partner_id: serverState.publicPartnerId, + livechat_member_type: "visitor", + }), + ], + }, + { + channel_type: "channel", + group_public_id: false, + name: "Link and Zelda", + }, + ]); + await start(); + await openDiscuss(channelId); + await insertText(".o-mail-Composer-input", "#"); + await contains(".o-mail-Composer-suggestion", { count: 0 }); +}); + +test("Internal user mention shows their live chat username", async () => { + const pyEnv = await startServer(); + pyEnv["res.partner"].write([serverState.partnerId], { user_livechat_username: "Batman" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ + partner_id: serverState.publicPartnerId, + livechat_member_type: "visitor", + }), + ], + }); + pyEnv["res.users"]._applyComputesAndValidate(); + await start(); + await openDiscuss(channelId); + await insertText(".o-mail-Composer-input", "@"); + await click('.o-mail-Composer-suggestion:contains(Mitchell Admin "Batman")'); + await contains(".o-mail-Composer-input:value(@Batman)"); + await click(".o-mail-Composer button[title='Send']:enabled"); + await contains(".o-mail-Message a.o_mail_redirect", { text: "@Batman" }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/thread_icon_patch.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/thread_icon_patch.test.js new file mode 100644 index 0000000..6dc20ae --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/thread_icon_patch.test.js @@ -0,0 +1,38 @@ +import { contains, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers"; +import { withGuest } from "@mail/../tests/mock_server/mail_mock_server"; +import { describe, test } from "@odoo/hoot"; +import { Command, serverState } from "@web/../tests/web_test_helpers"; + +import { rpc } from "@web/core/network/rpc"; +import { defineLivechatModels } from "./livechat_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("Public website visitor is typing", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor 20" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(channelId); + await contains(".o-mail-DiscussContent-header .o-mail-ThreadIcon .fa.fa-circle-o"); + const channel = pyEnv["discuss.channel"].search_read([["id", "=", channelId]])[0]; + // simulate receive typing notification from livechat visitor "is typing" + withGuest(guestId, () => + rpc("/discuss/channel/notify_typing", { + is_typing: true, + channel_id: channel.id, + }) + ); + await contains(".o-mail-DiscussContent-header .o-discuss-Typing-icon"); + await contains( + ".o-mail-DiscussContent-header .o-discuss-Typing-icon[title='Visitor 20 is typing...']" + ); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/thread_model_patch.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/thread_model_patch.test.js new file mode 100644 index 0000000..32ccc0d --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/thread_model_patch.test.js @@ -0,0 +1,117 @@ +import { + click, + contains, + insertText, + openDiscuss, + start, + startServer, + triggerHotkey, +} from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { Command, serverState, withUser } from "@web/../tests/web_test_helpers"; +import { defineLivechatModels } from "./livechat_test_helpers"; + +import { rpc } from "@web/core/network/rpc"; +import { press } from "@odoo/hoot-dom"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("Thread name unchanged when inviting new users", async () => { + const pyEnv = await startServer(); + const userId = pyEnv["res.users"].create({ name: "James" }); + pyEnv["res.partner"].create({ + name: "James", + user_ids: [userId], + }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(channelId); + await contains(".o-mail-DiscussContent-threadName[title='Visitor #20']"); + await click("button[title='Invite People']"); + await click("input", { + parent: [".o-discuss-ChannelInvitation-selectable", { text: "James" }], + }); + await click("button:enabled", { text: "Invite" }); + await contains(".o-discuss-ChannelInvitation", { count: 0 }); + await click("button[title='Members']"); + await contains(".o-discuss-ChannelMember", { text: "James" }); + await contains(".o-mail-DiscussContent-threadName[title='Visitor #20']"); +}); + +test("Can set a custom name to livechat conversation", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(channelId); + await click(".o-mail-DiscussSidebar-item:contains('Visitor #20')"); + await contains(".o-mail-DiscussContent-threadName[title='Visitor #20']"); + await insertText(".o-mail-DiscussContent-threadName", "New Name", { replace: true }); + await triggerHotkey("Enter"); + await contains(".o-mail-DiscussContent-threadName[title='New Name']"); + await contains(".o-mail-DiscussSidebar-item:contains('New Name')"); +}); + +test("Display livechat custom username if defined", async () => { + const pyEnv = await startServer(); + pyEnv["res.partner"].write(serverState.partnerId, { + user_livechat_username: "livechat custom username", + }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(channelId); + await insertText(".o-mail-Composer-input", "hello"); + await press("Enter"); + await contains(".o-mail-Message-author", { text: "livechat custom username" }); +}); + +test("Display livechat custom name in typing status", async () => { + const pyEnv = await startServer(); + const userId = pyEnv["res.users"].create({ name: "James" }); + const partnerId = pyEnv["res.partner"].create({ + name: "James", + user_ids: [userId], + user_livechat_username: "livechat custom username", + }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: partnerId, livechat_member_type: "agent" }), + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: partnerId, + }); + await start(); + await openDiscuss(channelId); + await withUser(userId, () => + rpc("/discuss/channel/notify_typing", { + channel_id: channelId, + is_typing: true, + }) + ); + await contains(".o-discuss-Typing", { text: "livechat custom username is typing..." }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/chatbot_step_type_clear_only.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/chatbot_step_type_clear_only.js new file mode 100644 index 0000000..5071659 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/chatbot_step_type_clear_only.js @@ -0,0 +1,40 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; + +registry.category("web_tour.tours").add("change_chatbot_step_type", { + steps: () => [ + { + content: "Open an existing script", + trigger: ".o_field_cell[data-tooltip='Clear Answer Test Bot']", + run: "click", + }, + { + content: "Open first step", + trigger: '.o_row_draggable .o_field_cell:contains("Question")', + run: "click", + }, + { + content: "Change step type to 'text'", + trigger: 'div[name="step_type"] input', + run: "click", + }, + { + trigger: '.dropdown-item:contains("Text")', + run: "click", + }, + { + content: "Verify answers cleared", + trigger: ".btn-primary:contains('Save')", + run: "click", + }, + { + trigger: ".o_form_button_save", + run: "click", + }, + { + // Ensure form is properly saved, in which case the save button is hidden. + trigger: ".o_form_button_save:not(:visible)", + }, + ], +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_channel_creation_tour.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_channel_creation_tour.js index 0537a73..e0fd50e 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_channel_creation_tour.js +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_channel_creation_tour.js @@ -1,32 +1,32 @@ -/** @odoo-module */ - -import tour from "web_tour.tour"; +import { registry } from "@web/core/registry"; const requestChatSteps = [ { - trigger: ".o_livechat_button", + trigger: ".o-livechat-root:shadow .o-livechat-LivechatButton", run: "click", }, { - trigger: ".o_thread_window", + trigger: ".o-livechat-root:shadow .o-mail-ChatWindow", }, ]; -tour.register("im_livechat_request_chat", { test: true }, requestChatSteps); +registry.category("web_tour.tours").add("im_livechat_request_chat", { + steps: () => requestChatSteps, +}); -tour.register("im_livechat_request_chat_and_send_message", { test: true }, [ - ...requestChatSteps, - { - trigger: ".o_composer_text_field", - run: "text Hello, I need help please !", - }, - { - trigger: '.o_composer_text_field', - run() { - $(".o_composer_text_field").trigger($.Event("keydown", { which: 13 })); +registry.category("web_tour.tours").add("im_livechat_request_chat_and_send_message", { + steps: () => [ + ...requestChatSteps, + { + trigger: ".o-livechat-root:shadow .o-mail-Composer-input", + run: "edit Hello, I need help please !", }, - }, - { - trigger: ".o_thread_message:contains('Hello, I need help')", - }, -]); + { + trigger: ".o-livechat-root:shadow .o-mail-Composer-input", + run: "press Enter", + }, + { + trigger: ".o-livechat-root:shadow .o-mail-Message:contains('Hello, I need help')", + }, + ], +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_chatbot_steps_sequence_tour.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_chatbot_steps_sequence_tour.js index c168bce..bf351f9 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_chatbot_steps_sequence_tour.js +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_chatbot_steps_sequence_tour.js @@ -1,130 +1,99 @@ -/** @odoo-module */ +import { registry } from "@web/core/registry"; +import { stepUtils } from "@web_tour/tour_utils"; -import tour from "web_tour.tour"; - -const commonSteps = [tour.stepUtils.showAppsMenuItem(), { - trigger: '.o_app[data-menu-xmlid="im_livechat.menu_livechat_root"]', -}, { - trigger: 'button[data-menu-xmlid="im_livechat.livechat_config"]', -}, { - trigger: 'a[data-menu-xmlid="im_livechat.chatbot_config"]', -}, { - trigger: '.o_list_button_add', -}, { - trigger: 'input[id="title"]', - run: 'text Test Chatbot Sequence' -}, { - trigger: 'div[name="script_step_ids"] .o_field_x2many_list_row_add a' -}, { - trigger: 'textarea#message', - run: 'text Step 1' -}, { - trigger: 'button:contains("Save & New")' -}, { - trigger: 'tr:contains("Step 1")', - in_modal: false, - run: () => {} -}, { - trigger: 'textarea#message', - run: 'text Step 2' -}, { - trigger: 'button:contains("Save & New")' -}, { - trigger: 'tr:contains("Step 2")', - in_modal: false, - run: () => {} -}, { - trigger: 'textarea#message', - run: 'text Step 3' -}]; +function createChatbotSteps(...stepMessages) { + return [ + { + trigger: "div[name='script_step_ids'] .o_field_x2many_list_row_add a", + run: "click", + }, + ...stepMessages + .map((message) => [ + { + trigger: ".modal .odoo-editor-editable", + run: `editor ${message}`, + }, + { + trigger: `.modal .odoo-editor-editable:contains(${message})`, + }, + { + trigger: ".modal button:contains(Save & New)", + run: "click", + }, + { + trigger: `tr:contains(${message})`, + }, + { + trigger: ".modal .odoo-editor-editable:empty", + }, + ]) + .flat(), + { + trigger: ".modal-footer button:contains(Discard)", + run: "click", + }, + ]; +} +const commonSteps = [ + stepUtils.showAppsMenuItem(), + { + trigger: '.o_app[data-menu-xmlid="im_livechat.menu_livechat_root"]', + run: "click", + }, + { + trigger: 'button[data-menu-xmlid="im_livechat.livechat_config"]', + run: "click", + }, + { + trigger: 'a[data-menu-xmlid="im_livechat.chatbot_config"]', + run: "click", + }, + { + trigger: ".o_list_button_add", + run: "click", + }, + { + trigger: 'input[id="title_0"]', + run: "edit Test Chatbot Sequence", + }, + ...createChatbotSteps("Step 1", "Step 2", "Step 3"), +]; /** * Simply create a few steps in order to check the sequences. */ - tour.register('im_livechat_chatbot_steps_sequence_tour', { - test: true, - url: '/web', -}, [ - ...commonSteps, { - trigger: 'button:contains("Save & Close")' -}, { - trigger: 'body.o_web_client:not(.modal-open)', - run() {}, -}, ...tour.stepUtils.discardForm() -]); +registry.category("web_tour.tours").add("im_livechat_chatbot_steps_sequence_tour", { + url: "/odoo", + steps: () => [ + ...commonSteps, + { + trigger: "body.o_web_client:not(.modal-open)", + }, + ], +}); /** * Same as above, with an extra drag&drop at the end. */ -tour.register('im_livechat_chatbot_steps_sequence_with_move_tour', { - test: true, - url: '/web', -}, [ - ...commonSteps, { - trigger: 'button:contains("Save & New")' -}, { - trigger: 'tr:contains("Step 3")', - in_modal: false, - run: () => {} -}, { - trigger: 'textarea#message', - run: 'text Step 4' -}, { - trigger: 'button:contains("Save & New")' -}, { - trigger: 'tr:contains("Step 4")', - in_modal: false, - run: () => {} -}, { - trigger: 'textarea#message', - run: 'text Step 5' -}, { - trigger: 'button:contains("Save & Close")' -}, { - trigger: 'body.o_web_client:not(.modal-open)', - run: () => {} -}, { - trigger: 'tr:contains("Step 5") .o_row_handle', - run: () => { - // move 'step 5' between 'step 1' and 'step 2' - const from = document.querySelector('div[name="script_step_ids"] tr:nth-child(5) .o_row_handle'); - const fromPosition = from.getBoundingClientRect(); - fromPosition.x += from.offsetWidth / 2; - fromPosition.y += from.offsetHeight / 2; - - const to = document.querySelector('div[name="script_step_ids"] tr:nth-child(2) .o_row_handle'); - from.dispatchEvent(new Event("mouseenter", { bubbles: true })); - from.dispatchEvent(new MouseEvent("mousedown", { - bubbles: true, - which: 1, - button: 0, - clientX: fromPosition.x, - clientY: fromPosition.y})); - from.dispatchEvent(new MouseEvent("mousemove", { - bubbles: true, - which: 1, - button: 0, - // dragging is only enabled when the mouse have moved from at least 10 pixels from the original position - clientX: fromPosition.x + 20, - clientY: fromPosition.y + 20, - })); - to.dispatchEvent(new Event("mouseenter", { bubbles: true })); - from.dispatchEvent(new Event("mouseup", { bubbles: true })); - } -}, { - trigger: 'div[name="script_step_ids"] .o_field_x2many_list_row_add a' -}, { - trigger: 'textarea#message', - run: 'text Step 6' -}, { - trigger: 'button:contains("Save & Close")' -}, { - trigger: 'body.o_web_client:not(.modal-open)', - run: () => {} -}, { - trigger: 'tr:contains("Step 6")', - in_modal: false, - run: () => {} -}, ...tour.stepUtils.discardForm(), -]); +registry.category("web_tour.tours").add("im_livechat_chatbot_steps_sequence_with_move_tour", { + url: "/odoo", + steps: () => [ + ...commonSteps, + ...createChatbotSteps("Step 4", "Step 5"), + { + trigger: "body.o_web_client:not(.modal-open)", + }, + { + trigger: 'div[name="script_step_ids"] tr:nth-child(5) .o_row_handle', + run: 'drag_and_drop(div[name="script_step_ids"] tr:nth-child(2))', + }, + ...createChatbotSteps("Step 6"), + { + trigger: "body.o_web_client:not(.modal-open)", + }, + { + trigger: 'tr:contains("Step 6")', + }, + ], +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_history_back_and_forth.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_history_back_and_forth.js new file mode 100644 index 0000000..2bb8800 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_history_back_and_forth.js @@ -0,0 +1,39 @@ +import { delay } from "@web/core/utils/concurrency"; +import { registry } from "@web/core/registry"; + +registry.category("web_tour.tours").add("im_livechat_history_back_and_forth_tour", { + steps: () => [ + { + trigger: "button.o_switch_view.o_list", + run: "click", + }, + { + trigger: ".o_data_cell:contains(Visitor)", + run: "click", + }, + { + trigger: ".o-mail-DiscussContent-threadName[title='Visitor']", + async run() { + await delay(1000); + history.back(); + }, + }, + { + trigger: ".o_data_cell:contains(Visitor)", + async run() { + await delay(0); + history.forward(); + }, + }, + { + trigger: ".o-mail-DiscussContent-threadName[title='Visitor']", + async run() { + await delay(1000); + history.back(); + }, + }, + { + trigger: ".o_data_cell:contains(Visitor)", + }, + ], +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_looking_for_help_real_time_update_tour.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_looking_for_help_real_time_update_tour.js new file mode 100644 index 0000000..b2ce777 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_looking_for_help_real_time_update_tour.js @@ -0,0 +1,56 @@ +import { registry } from "@web/core/registry"; + +/** + * @param {"list" | "kanban"} viewType + * @returns {import("@web_tour/tour_service/tour_service").TourStep[]} + */ +function getSteps(viewType) { + let bobChatId; + return [ + { + trigger: ".o_control_panel .active:contains(Looking for Help)", + }, + { + trigger: + viewType === "list" + ? ".o_list_table:has(.o_data_row:contains(bob_looking_for_help))" + : ".o_kanban_renderer:has(.o_kanban_record [name=livechat_agent_partner_ids] [aria-label^=bob_looking_for_help])", + async run() { + const { orm } = odoo.__WOWL_DEBUG__.root.env.services; + [bobChatId] = await orm.search("discuss.channel", [ + ["livechat_status", "=", "need_help"], + ["livechat_agent_partner_ids.name", "like", "bob_looking_for_help%"], + ]); + await orm.write("discuss.channel", [bobChatId], { + livechat_status: "in_progress", + }); + }, + }, + { + trigger: + viewType === "list" + ? ".o_list_table:not(:has(.o_data_row:contains(bob_looking_for_help)))" + : ".o_kanban_renderer:not(:has(.o_kanban_record [name=livechat_agent_partner_ids] [aria-label^=bob_looking_for_help]))", + async run() { + const { orm } = odoo.__WOWL_DEBUG__.root.env.services; + await orm.write("discuss.channel", [bobChatId], { + livechat_status: "need_help", + }); + }, + }, + { + trigger: + viewType === "list" + ? ".o_list_table:has(.o_data_row:contains(bob_looking_for_help))" + : ".o_kanban_renderer:has(.o_kanban_record [name=livechat_agent_partner_ids] [aria-label^=bob_looking_for_help])", + }, + ]; +} +registry.category("web_tour.tours").add("im_livechat.looking_for_help_list_real_time_update_tour", { + steps: () => getSteps("list"), +}); +registry + .category("web_tour.tours") + .add("im_livechat.looking_for_help_kanban_real_time_update_tour", { + steps: () => getSteps("kanban"), + }); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_looking_for_help_tags_real_time_update_tour.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_looking_for_help_tags_real_time_update_tour.js new file mode 100644 index 0000000..30e0626 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_looking_for_help_tags_real_time_update_tour.js @@ -0,0 +1,57 @@ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; + +let bobChatId; +let tagId; +registry.category("web_tour.tours").add("im_livechat.looking_for_help_tags_real_time_update_tour", { + steps: () => [ + { + trigger: ".o_control_panel .active:contains(Looking for Help)", + }, + { + trigger: ".o_optional_columns_dropdown_toggle", + run: "click", + }, + { + trigger: '.o-dropdown-item input[name="livechat_conversation_tag_ids"]', + run: "click", + }, + { + trigger: ".o_optional_columns_dropdown_toggle", + run: "click", + }, + { + trigger: ".o_list_table:has(.o_data_row:contains(bob_looking_for_help))", + }, + { + trigger: '.o_data_cell[name="livechat_conversation_tag_ids"]:not(:has(.o_tag))', + async run() { + const { orm } = odoo.__WOWL_DEBUG__.root.env.services; + [bobChatId] = await orm.search("discuss.channel", [ + ["livechat_status", "=", "need_help"], + ["livechat_agent_partner_ids.name", "like", "bob_looking_for_help%"], + ]); + [tagId] = await orm.create("im_livechat.conversation.tag", [{ name: "Discuss" }]); + // Simulate other user adding a tag + await rpc("/im_livechat/conversation/update_tags", { + channel_id: bobChatId, + tag_ids: [tagId], + method: "ADD", + }); + }, + }, + { + trigger: + '.o_data_cell[name="livechat_conversation_tag_ids"]:has(.o_tag:contains(Discuss))', + async run() { + // Simulate other user removing a tag + await rpc("/im_livechat/conversation/update_tags", { + channel_id: bobChatId, + tag_ids: [tagId], + method: "DELETE", + }); + }, + }, + { trigger: '.o_data_cell[name="livechat_conversation_tag_ids"]:not(:has(.o_tag))' }, + ], +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_report_pivot_redirect_tour.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_report_pivot_redirect_tour.js new file mode 100644 index 0000000..e1c4577 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_report_pivot_redirect_tour.js @@ -0,0 +1,37 @@ +import { registry } from "@web/core/registry"; + +function makePivotRedirectTourSteps(singleRecordName, multiRecordName) { + return [ + { + content: "Click on a cell with a single related record", + trigger: `.o_pivot table tbody tr:has(th:contains(${singleRecordName})) td:eq(0)`, + run: "click", + }, + { + trigger: ".o-mail-Discuss", + content: "Verify redirection to the single record view", + }, + { + content: "Go back to the pivot view", + trigger: ".o_back_button", + run: "click", + }, + { + content: "Click on a cell with a multiple related records", + trigger: `.o_pivot table tbody tr:has(th:contains(${multiRecordName})) td:eq(0)`, + run: "click", + }, + { + trigger: ".o_list_view", + content: "Verify redirection to the list view for multiple records", + }, + ]; +} + +registry.category("web_tour.tours").add("im_livechat_agents_report_pivot_redirect_tour", { + steps: () => makePivotRedirectTourSteps("test 1", "test 2"), +}); + +registry.category("web_tour.tours").add("im_livechat_sessions_report_pivot_redirect_tour", { + steps: () => makePivotRedirectTourSteps("operator_1", "operator_2"), +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_session_history_open.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_session_history_open.js new file mode 100644 index 0000000..edf6eef --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/im_livechat_session_history_open.js @@ -0,0 +1,62 @@ +import { whenReady } from "@odoo/owl"; + +import { registry } from "@web/core/registry"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; + +let firstChannelId; +registry.category("web_tour.tours").add("im_livechat_session_history_open", { + steps: () => [ + { + trigger: "body", + async run() { + await whenReady(); + const busService = odoo.__WOWL_DEBUG__.root.env.services.bus_service; + patchWithCleanup(busService, { + addChannel(channel) { + document.body.classList.add(`o-bus-channel-${channel}`); + return super.addChannel(...arguments); + }, + deleteChannel(channel) { + document.body.classList.remove(`o-bus-channel-${channel}`); + return super.deleteChannel(...arguments); + }, + }); + }, + }, + { + trigger: ".o_switch_view[data-tooltip='List']", + run: "click", + }, + { + trigger: ".o_data_cell:contains('test 2')", + run: "click", + }, + { + trigger: ".o-mail-Message-content:contains('Test Channel 2 Msg')", + async run({ waitFor }) { + firstChannelId = + odoo.__WOWL_DEBUG__.root.env.services.action.currentController.state.resId; + await waitFor(`body.o-bus-channel-discuss\\.channel_${firstChannelId}`, { + timeout: 3000, + }); + }, + }, + { + trigger: ".oi-chevron-right", + run: "click", + }, + { + trigger: ".o-mail-Message-content:contains('Test Channel 1 Msg')", + async run({ waitFor }) { + await waitFor(`body:not(.o-bus-channel-discuss\\.channel_${firstChannelId})`, { + timeout: 3000, + }); + const channelId = + odoo.__WOWL_DEBUG__.root.env.services.action.currentController.state.resId; + await waitFor(`body.o-bus-channel-discuss\\.channel_${channelId}`, { + trimeout: 3000, + }); + }, + }, + ], +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/looking_for_help_discuss_category_tour.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/looking_for_help_discuss_category_tour.js new file mode 100644 index 0000000..479ebef --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/looking_for_help_discuss_category_tour.js @@ -0,0 +1,38 @@ +import { registry } from "@web/core/registry"; + +registry.category("web_tour.tours").add("im_livechat.looking_for_help_discuss_category_tour", { + steps: () => [ + { + // Two live chats are looking for help, they are both in the "Looking for help" category. + trigger: + ".o-mail-DiscussSidebarCategory-livechatNeedHelp + .o-mail-DiscussSidebarChannel-container:contains(Visitor Accounting) + .o-mail-DiscussSidebarChannel-container:contains(Visitor Sales)", + }, + { + trigger: ".o-mail-DiscussSidebarChannel:contains(Sales) .o-mail-starred", + }, + { + trigger: + ".o-mail-DiscussSidebarChannel:contains(Accounting):not(:has(.o-mail-starred))", + }, + { + trigger: ".o-mail-DiscussSidebarChannel:contains(Accounting)", + run: "hover && click [title='Chat Actions']", + }, + { + trigger: + ".o-mail-DiscussSidebar:has(.o-mail-DiscussSidebarChannel:contains(Accounting))", + }, + { + trigger: "button[name='livechat-status']", + run: "hover", + }, + { + trigger: ".o-livechat-LivechatStatusSelection-Label:contains(In progress)", + run: "click", + }, + { + trigger: + ".o-mail-DiscussSidebar:not(:has(.o-mail-DiscussSidebarChannel:contains(Accounting)))", + }, + ], +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/support/im_livechat_basic_tour.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/support/im_livechat_basic_tour.js new file mode 100644 index 0000000..247be61 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/support/im_livechat_basic_tour.js @@ -0,0 +1,16 @@ +import { registry } from "@web/core/registry"; + +registry.category("web_tour.tours").add("im_livechat.basic_tour", { + steps: () => [ + { + trigger: ".channel_name:contains(Support Channel)", + }, + { + trigger: ".o-livechat-root:shadow .o-livechat-LivechatButton", + run: "click", + }, + { + trigger: ".o-livechat-root:shadow .o-mail-ChatWindow", + }, + ], +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/support/im_livechat_meeting_view_tour.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/support/im_livechat_meeting_view_tour.js new file mode 100644 index 0000000..f3fa024 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/tours/support/im_livechat_meeting_view_tour.js @@ -0,0 +1,44 @@ +import { registry } from "@web/core/registry"; + +registry.category("web_tour.tours").add("im_livechat.meeting_view_tour", { + steps: () => [ + { + trigger: ".o-livechat-root:shadow .o-livechat-LivechatButton", + run: "click", + }, + { + trigger: ".o-livechat-root:shadow .o-mail-Thread[data-transient]", + }, + { + trigger: ".o-livechat-root:shadow .o-mail-Composer-input", + run: "edit Hello!", + }, + { + trigger: ".o-livechat-root:shadow .o-mail-Composer-input", + run: "press Enter", + }, + { + trigger: ".o-livechat-root:shadow [title='Join Call']", + run: "click", + }, + { + trigger: ".o-livechat-root:shadow .o-discuss-Call [title='Fullscreen']", + run: "click", + }, + { + trigger: ".o-livechat-root:shadow .o-mail-Meeting", + }, + { + trigger: ".o-livechat-root:shadow .o-mail-MeetingSideActions [name^='more-action:'] ", + run: "click", + }, + { + trigger: ".o-livechat-root:shadow [name='call-settings']", + run: "click", + }, + { + trigger: + ".o-livechat-root:shadow .o-mail-DiscussContent-panelContainer .o-mail-ActionPanel-header:contains('voice settings')", + }, + ], +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/transcript_sender.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/transcript_sender.test.js new file mode 100644 index 0000000..2e0680d --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/transcript_sender.test.js @@ -0,0 +1,31 @@ +import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers"; +import { contains, focus, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { press } from "@odoo/hoot-dom"; +import { Command, serverState } from "@web/../tests/web_test_helpers"; +import { serializeDate, today } from "@web/core/l10n/dates"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("agent can send conversation after livechat ends", async () => { + const pyEnv = await startServer(); + const demoPartnerId = pyEnv["res.partner"].create({ + name: "Awesome partner", + email: "awesome@example.com", + }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ partner_id: demoPartnerId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_end_dt: serializeDate(today()), + livechat_operator_id: serverState.partnerId, + }); + await start(); + await openDiscuss(channelId); + await focus("input[placeholder='mail@example.com']", { value: "awesome@example.com" }); + await press("Enter"); + await contains(".form-text", { text: "The conversation was sent." }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/translation.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/translation.test.js new file mode 100644 index 0000000..fa00348 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/translation.test.js @@ -0,0 +1,54 @@ +import { describe, test } from "@odoo/hoot"; +import { click, contains, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers"; +import { Command, serverState } from "@web/../tests/web_test_helpers"; +import { defineLivechatModels } from "./livechat_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("message translation in livechat (agent is member)", async () => { + const pyEnv = await startServer(); + const channelId = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ + guest_id: pyEnv["mail.guest"].create({ name: "Mario" }), + livechat_member_type: "visitor", + }), + ], + }); + pyEnv["mail.message"].create({ + body: "Mai mettere l'ananas sulla pizza!", + model: "discuss.channel", + res_id: channelId, + }); + await start(); + await openDiscuss(channelId); + await contains(".o-mail-Message"); + await click("[title='Expand']"); + await contains(".o-dropdown-item:contains('Translate')"); +}); + +test("message translation in livechat (agent is not member)", async () => { + const pyEnv = await startServer(); + const channelId = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ + guest_id: pyEnv["mail.guest"].create({ name: "Mario" }), + livechat_member_type: "visitor", + }), + ], + }); + pyEnv["mail.message"].create({ + body: "Mai mettere l'ananas sulla pizza!", + model: "discuss.channel", + res_id: channelId, + }); + await start(); + await openDiscuss(channelId); + await contains(".o-mail-Message"); + await click("[title='Expand']"); + await contains(".o-dropdown-item:contains('Translate')"); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/visitor_disconnection.test.js b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/visitor_disconnection.test.js new file mode 100644 index 0000000..c947e65 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/static/tests/visitor_disconnection.test.js @@ -0,0 +1,76 @@ +import { Command, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers"; +import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers"; +import { + contains, + setupChatHub, + start, + startServer, + click, +} from "@mail/../tests/mail_test_helpers"; +import { describe, test } from "@odoo/hoot"; +import { mockDate } from "@odoo/hoot-mock"; +import { Store } from "@mail/core/common/store_service"; +import { Thread } from "@mail/core/common/thread"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +test("Visitor going offline shows disconnection banner to operator", async () => { + patchWithCleanup(Store, { IM_STATUS_DEBOUNCE_DELAY: 0 }); + patchWithCleanup(Thread.prototype, { + setup() { + super.setup(); + this.IM_STATUS_DELAY = 0; + }, + }); + const pyEnv = await startServer(); + pyEnv["res.users"].write([serverState.userId], { + group_ids: pyEnv["res.groups"] + .search_read([["id", "=", serverState.groupLivechatId]]) + .map(({ id }) => id), + }); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor", im_status: "online" }); + const livechatChannelId = pyEnv["im_livechat.channel"].create({ + name: "HR", + user_ids: [serverState.userId], + }); + const channel_id = pyEnv["discuss.channel"].create({ + channel_type: "livechat", + channel_member_ids: [ + Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + livechat_channel_id: livechatChannelId, + livechat_operator_id: serverState.partnerId, + create_uid: serverState.publicUserId, + }); + setupChatHub({ opened: [channel_id] }); + await start(); + await contains(".o-mail-ChatWindow"); + mockDate("2025-01-01 12:00:00", +1); + pyEnv["mail.guest"].write(guestId, { im_status: "offline" }); + pyEnv["bus.bus"]._sendone(guestId, "bus.bus/im_status_updated", { + partner_id: false, + guest_id: guestId, + im_status: "offline", + }); + await contains(".o-livechat-VisitorDisconnected", { + text: "Visitor is disconnected since 1:00 PM", + }); + mockDate("2025-01-02 12:00:00", +1); + await click("button[title*='Fold']"); + await click(".o-mail-ChatBubble"); + await contains(".o-livechat-VisitorDisconnected", { + text: "Visitor is disconnected since yesterday at 1:00 PM", + }); + mockDate("2025-01-05 12:00:00", +1); + await click("button[title*='Fold']"); + await click(".o-mail-ChatBubble"); + await contains(".o-livechat-VisitorDisconnected", { text: `Visitor is disconnected` }); + pyEnv["bus.bus"]._sendone(guestId, "bus.bus/im_status_updated", { + partner_id: false, + guest_id: guestId, + im_status: "online", + }); + await contains(".o-livechat-VisitorDisconnected", { count: 0 }); +}); diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/__init__.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/__init__.py index c74b8ea..78a1804 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/__init__.py +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/__init__.py @@ -2,9 +2,23 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import chatbot_common +from . import test_call from . import test_chatbot_form_ui from . import test_chatbot_internals -from . import test_get_mail_channel +from . import test_digest +from . import test_discuss_channel +from . import test_cors_livechat +from . import test_get_discuss_channel +from . import test_get_operator +from . import test_im_livechat_calls +from . import test_im_livechat_channel from . import test_im_livechat_report from . import test_im_livechat_support_page +from . import test_js +from . import test_member_history from . import test_message +from . import test_upload_attachment +from . import test_transcript +from . import test_res_users +from . import test_session_views +from . import test_user_livechat_username diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/chatbot_common.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/chatbot_common.py index 23f8b0e..524ecaf 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/chatbot_common.py +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/chatbot_common.py @@ -1,14 +1,14 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.tests import common -class ChatbotCase(common.TransactionCase): +class ChatbotCase(common.HttpCase): @classmethod def setUpClass(cls): super(ChatbotCase, cls).setUpClass() + cls.maxDiff = None cls.chatbot_script = cls.env['chatbot.script'].create({ 'title': 'Testing Bot', @@ -38,8 +38,9 @@ class ChatbotCase(common.TransactionCase): cls.step_dispatch_buy_software, cls.step_dispatch_pricing, cls.step_dispatch_operator, + cls.step_dispatch_documentation, ] = cls.env['chatbot.script.answer'].sudo().create([{ - 'name': 'I want to buy the software', + 'name': 'I\'d like to buy the software', 'script_step_id': cls.step_dispatch.id, }, { 'name': 'Pricing Question', @@ -47,6 +48,9 @@ class ChatbotCase(common.TransactionCase): }, { 'name': "I want to speak with an operator", 'script_step_id': cls.step_dispatch.id, + }, { + 'name': "Other & Documentation", + 'script_step_id': cls.step_dispatch.id, }]) [ @@ -56,6 +60,7 @@ class ChatbotCase(common.TransactionCase): cls.step_forward_operator, cls.step_no_one_available, cls.step_no_operator_dispatch, + cls.step_documentation_validated, ] = ChatbotScriptStep.create([{ 'step_type': 'text', 'message': 'For any pricing question, feel free ton contact us at pricing@mycompany.com', @@ -86,6 +91,11 @@ class ChatbotCase(common.TransactionCase): 'message': 'So... What can I do to help you?', 'triggering_answer_ids': [(4, cls.step_dispatch_operator.id)], 'chatbot_script_id': cls.chatbot_script.id, + }, { + 'step_type': 'text', + 'message': 'Please find documentation at https://www.odoo.com/documentation/latest/', + 'triggering_answer_ids': [(4, cls.step_dispatch_documentation.id)], + 'chatbot_script_id': cls.chatbot_script.id, }]) cls.step_no_operator_just_leaving = cls.env['chatbot.script.answer'].sudo().create({ @@ -133,13 +143,33 @@ class ChatbotCase(common.TransactionCase): })] }) - @classmethod - def _post_answer_and_trigger_next_step(cls, mail_channel, answer, chatbot_script_answer=False): - mail_message = mail_channel.message_post(body=answer) + def _post_answer_and_trigger_next_step( + self, discuss_channel, body=None, email=None, chatbot_script_answer=None + ): + data = self.make_jsonrpc_request( + "/mail/message/post", + { + "thread_model": "discuss.channel", + "thread_id": discuss_channel.id, + "post_data": { + "body": body or email or chatbot_script_answer.name, + "message_type": "comment", + "subtype_xmlid": "mail.mt_comment", + }, + }, + ) + if email: + self.make_jsonrpc_request( + "/chatbot/step/validate_email", {"channel_id": discuss_channel.id} + ) if chatbot_script_answer: - cls.env['chatbot.message'].search([ - ('mail_message_id', '=', mail_message.id) - ], limit=1).user_script_answer_id = chatbot_script_answer.id - # sudo: chatbot.script.step - members of a channel can access the current chatbot step - next_step = mail_channel.chatbot_current_step_id.sudo()._process_answer(mail_channel, mail_message.body) - next_step._process_step(mail_channel) + message = self.env["mail.message"].browse(data["message_id"]) + self.make_jsonrpc_request( + "/chatbot/answer/save", + { + "channel_id": discuss_channel.id, + "message_id": message.id, + "selected_answer_id": chatbot_script_answer.id, + }, + ) + self.make_jsonrpc_request("/chatbot/step/trigger", {"channel_id": discuss_channel.id}) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/common.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/common.py index 9c23f1e..294059d 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/common.py +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/common.py @@ -1,18 +1,23 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tests.common import TransactionCase +from odoo import Command, fields +from odoo.tests.common import HttpCase, new_test_user +from odoo.addons.bus.tests.common import BusCase -class TestImLivechatCommon(TransactionCase): +class TestImLivechatCommon(HttpCase, BusCase): @classmethod def setUpClass(cls): super().setUpClass() + cls.password = 'Pl1bhD@2!kXZ' cls.operators = cls.env['res.users'].create([{ 'name': 'Michel', 'login': 'michel', + 'password': cls.password, 'livechat_username': "Michel Operator", 'email': 'michel@example.com', + 'group_ids': cls.env.ref('im_livechat.im_livechat_group_user'), }, { 'name': 'Paul', 'login': 'paul' @@ -42,7 +47,58 @@ class TestImLivechatCommon(TransactionCase): def setUp(self): super().setUp() - def get_available_users(_): - return self.operators + def _compute_available_operator_ids(channel_self): + for record in channel_self: + record.available_operator_ids = record.user_ids - self.patch(type(self.env['im_livechat.channel']), '_get_available_users', get_available_users) + self.patch(type(self.env['im_livechat.channel']), '_compute_available_operator_ids', _compute_available_operator_ids) + + +class TestGetOperatorCommon(HttpCase): + def setUp(self): + super().setUp() + self.operator_id = 0 + + def _create_conversation(self, livechat, operator, in_call=False): + channel = self.env["discuss.channel"].create( + { + "name": "Visitor 1", + "channel_type": "livechat", + "livechat_channel_id": livechat.id, + "livechat_operator_id": operator.partner_id.id, + "channel_member_ids": [Command.create({"partner_id": operator.partner_id.id})], + "last_interest_dt": fields.Datetime.now(), + } + ) + channel.with_user(operator).message_post(body="Hello, how can I help you?") + if in_call: + member = self.env["discuss.channel.member"].search( + [("partner_id", "=", operator.partner_id.id), ("channel_id", "=", channel.id)] + ) + self.env["discuss.channel.rtc.session"].sudo().create( + {"channel_id": channel.id, "channel_member_id": member.id} + ) + return channel + + def _create_operator(self, lang_code=None, country_code=None, expertises=None): + self.env["res.lang"].with_context(active_test=False).search( + [("code", "=", lang_code)] + ).sudo().active = True + operator = new_test_user( + self.env(su=True), + login=f"operator_{lang_code or country_code}_{self.operator_id}", + groups="im_livechat.im_livechat_group_user", + ) + operator.res_users_settings_id.livechat_expertise_ids = expertises + operator.partner_id = self.env["res.partner"].create( + { + "name": f"Operator {lang_code or country_code}", + "lang": lang_code, + "country_id": self.env["res.country"].search([("code", "=", country_code)]).id + if country_code + else None, + } + ) + self.env["mail.presence"]._update_presence(operator) + self.operator_id += 1 + return operator diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_call.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_call.py new file mode 100644 index 0000000..b3a01ea --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_call.py @@ -0,0 +1,39 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import new_test_user, tagged, HttpCase, JsonRpcException + + +@tagged("post_install", "-at_install") +class TestCall(HttpCase): + def test_visitor_cannot_start_call(self): + self.authenticate(None, None) + operator = self.env["res.users"].create({"name": "Operator", "login": "operator"}) + self.env["mail.presence"]._update_presence(operator) + livechat_channel = self.env["im_livechat.channel"].create( + {"name": "Test Livechat Channel", "user_ids": [operator.id]} + ) + for pseudo_user in [ + new_test_user(self.env, "portal_user", groups="base.group_portal"), + self.env["mail.guest"].create({"name": "Guest"}), + ]: + user = pseudo_user if pseudo_user._name == "res.users" else self.env["res.users"] + guest = pseudo_user if pseudo_user._name == "mail.guest" else self.env["mail.guest"] + if user: + self.authenticate(user.login, user.login) + else: + self.authenticate(None, None) + if guest: + self.opener.cookies[guest._cookie_name] = guest._format_auth_cookie() + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "anonymous_name": "Visitor", + "channel_id": livechat_channel.id, + "persisted": True, + }, + ) + with self.assertRaises(JsonRpcException, msg="werkzeug.exceptions.NotFound"): + self.make_jsonrpc_request( + "/mail/rtc/channel/join_call", + {"channel_id": data["channel_id"]}, + ) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_chatbot_form_ui.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_chatbot_form_ui.py index c9749bf..b1d35b3 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_chatbot_form_ui.py +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_chatbot_form_ui.py @@ -5,28 +5,27 @@ from odoo import tests from odoo.addons.base.tests.common import HttpCaseWithUserDemo -@tests.tagged('post_install', '-at_install') +@tests.tagged("post_install", "-at_install") class TestLivechatChatbotFormUI(HttpCaseWithUserDemo): def test_chatbot_steps_sequence_ui(self): """ As sequences are *critical* for the chatbot_script script, let us a run a little tour that creates a few steps, then verify sequences are properly applied. """ self.start_tour( - '/web', + '/odoo', 'im_livechat_chatbot_steps_sequence_tour', login='admin', - step_delay=1000 ) chatbot_script = self.env['chatbot.script'].search([('title', '=', 'Test Chatbot Sequence')]) self.assertEqual(len(chatbot_script.script_step_ids), 3) - self.assertEqual(chatbot_script.script_step_ids[0].message, 'Step 1') + self.assertEqual(chatbot_script.script_step_ids[0].message, "

Step 1

") self.assertEqual(chatbot_script.script_step_ids[0].sequence, 0) - self.assertEqual(chatbot_script.script_step_ids[1].message, 'Step 2') + self.assertEqual(chatbot_script.script_step_ids[1].message, "

Step 2

") self.assertEqual(chatbot_script.script_step_ids[1].sequence, 1) - self.assertEqual(chatbot_script.script_step_ids[2].message, 'Step 3') + self.assertEqual(chatbot_script.script_step_ids[2].message, "

Step 3

") self.assertEqual(chatbot_script.script_step_ids[2].sequence, 2) def test_chatbot_steps_sequence_with_move_ui(self): @@ -36,10 +35,9 @@ class TestLivechatChatbotFormUI(HttpCaseWithUserDemo): move records around. """ self.start_tour( - '/web', + '/odoo', 'im_livechat_chatbot_steps_sequence_with_move_tour', login='admin', - step_delay=1000 ) chatbot_script = self.env['chatbot.script'].search([('title', '=', 'Test Chatbot Sequence')]) @@ -49,15 +47,15 @@ class TestLivechatChatbotFormUI(HttpCaseWithUserDemo): # during the test, we create the steps normally and then move 'Step 5' # in second position -> check order is correct - self.assertEqual(chatbot_script.script_step_ids[0].message, 'Step 1') + self.assertEqual(chatbot_script.script_step_ids[0].message, "

Step 1

") self.assertEqual(chatbot_script.script_step_ids[0].sequence, 0) - self.assertEqual(chatbot_script.script_step_ids[1].message, 'Step 5') + self.assertEqual(chatbot_script.script_step_ids[1].message, "

Step 5

") self.assertEqual(chatbot_script.script_step_ids[1].sequence, 1) - self.assertEqual(chatbot_script.script_step_ids[2].message, 'Step 2') + self.assertEqual(chatbot_script.script_step_ids[2].message, "

Step 2

") self.assertEqual(chatbot_script.script_step_ids[2].sequence, 2) - self.assertEqual(chatbot_script.script_step_ids[3].message, 'Step 3') + self.assertEqual(chatbot_script.script_step_ids[3].message, "

Step 3

") self.assertEqual(chatbot_script.script_step_ids[3].sequence, 3) - self.assertEqual(chatbot_script.script_step_ids[4].message, 'Step 4') + self.assertEqual(chatbot_script.script_step_ids[4].message, "

Step 4

") self.assertEqual(chatbot_script.script_step_ids[4].sequence, 4) - self.assertEqual(chatbot_script.script_step_ids[5].message, 'Step 6') + self.assertEqual(chatbot_script.script_step_ids[5].message, "

Step 6

") self.assertEqual(chatbot_script.script_step_ids[5].sequence, 5) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_chatbot_internals.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_chatbot_internals.py index 54687e6..1c33f78 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_chatbot_internals.py +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_chatbot_internals.py @@ -1,11 +1,17 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from freezegun import freeze_time + +from odoo import Command, fields from odoo.addons.im_livechat.tests import chatbot_common -from odoo.exceptions import ValidationError +from odoo.tests.common import JsonRpcException, new_test_user, tagged +from odoo.tools.misc import mute_logger +from odoo.addons.mail.tests.common import freeze_all_time, MailCommon +from odoo.addons.mail.tools.discuss import Store -class ChatbotCase(chatbot_common.ChatbotCase): +@tagged("post_install", "-at_install") +class ChatbotCase(MailCommon, chatbot_common.ChatbotCase): def test_chatbot_duplicate(self): """ In this test we make sure that 'triggering_answer_ids' are correctly duplicated and @@ -27,12 +33,12 @@ class ChatbotCase(chatbot_common.ChatbotCase): self.assertNotEqual(step_email_copy, self.step_email) self.assertEqual(len(step_email_copy.triggering_answer_ids), 1) - self.assertEqual(step_email_copy.triggering_answer_ids.name, 'I want to buy the software') + self.assertEqual(step_email_copy.triggering_answer_ids.name, 'I\'d like to buy the software') self.assertNotEqual(step_email_copy.triggering_answer_ids, self.step_dispatch_buy_software) def test_chatbot_is_forward_operator_child(self): self.assertEqual([step.is_forward_operator_child for step in self.chatbot_script.script_step_ids], - [False, False, False, False, False, False, False, True, True, True, False, False, False, False], + [False, False, False, False, False, False, False, True, True, False, True, False, False, False, False], "Steps 'step_no_one_available', 'step_no_operator_dispatch', 'step_just_leaving'" "should be flagged as forward operator child.") @@ -40,28 +46,28 @@ class ChatbotCase(chatbot_common.ChatbotCase): self.chatbot_script.script_step_ids.invalidate_recordset(['is_forward_operator_child']) self.assertEqual([step.is_forward_operator_child for step in self.chatbot_script.script_step_ids], - [False, False, False, False, False, False, False, True, False, False, False, False, False, False], + [False, False, False, False, False, False, False, True, False, False, False, False, False, False, False], "Only step 'step_no_one_available' should be flagged as forward operator child.") def test_chatbot_steps(self): - channel_info = self.livechat_channel._open_livechat_mail_channel( - anonymous_name='Test Visitor', chatbot_script=self.chatbot_script) - mail_channel = self.env['mail.channel'].browse(channel_info['id']) + data = self.make_jsonrpc_request("/im_livechat/get_session", { + 'chatbot_script_id': self.chatbot_script.id, + 'channel_id': self.livechat_channel.id, + }) + discuss_channel = self.env["discuss.channel"].browse(data["channel_id"]) - self.assertEqual(mail_channel.chatbot_current_step_id, self.step_dispatch) + self.assertEqual(discuss_channel.chatbot_current_step_id, self.step_dispatch) self._post_answer_and_trigger_next_step( - mail_channel, - self.step_dispatch_buy_software.name, - chatbot_script_answer=self.step_dispatch_buy_software + discuss_channel, chatbot_script_answer=self.step_dispatch_buy_software ) - self.assertEqual(mail_channel.chatbot_current_step_id, self.step_email) + self.assertEqual(discuss_channel.chatbot_current_step_id, self.step_email) - with self.assertRaises(ValidationError, msg="Should raise an error since it's not a valid email"): - self._post_answer_and_trigger_next_step(mail_channel, 'test') + with self.assertRaises(JsonRpcException, msg='odoo.exceptions.ValidationError'), mute_logger("odoo.http"): + self._post_answer_and_trigger_next_step(discuss_channel, email="test") - self._post_answer_and_trigger_next_step(mail_channel, 'test@example.com') - self.assertEqual(mail_channel.chatbot_current_step_id, self.step_email_validated) + self._post_answer_and_trigger_next_step(discuss_channel, email="test@example.com") + self.assertEqual(discuss_channel.chatbot_current_step_id, self.step_email_validated) def test_chatbot_steps_sequence(self): """ Ensure sequence is correct when creating chatbots and adding steps to an existing one. @@ -105,3 +111,428 @@ class ChatbotCase(chatbot_common.ChatbotCase): welcome_steps = self.chatbot_script._get_welcome_steps() self.assertEqual(len(welcome_steps), 1) self.assertEqual(welcome_steps, self.chatbot_script.script_step_ids[0]) + + def test_chatbot_not_invited_to_rtc_calls(self): + with freeze_all_time(): + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "channel_id": self.livechat_channel.id, + "chatbot_script_id": self.chatbot_script.id, + }, + ) + discuss_channel = ( + self.env["discuss.channel"].sudo().browse(data["channel_id"]) + ) + self.assertEqual(discuss_channel.livechat_operator_id, self.chatbot_script.operator_partner_id) + discuss_channel._add_members(users=self.env.user) + self_member = discuss_channel.channel_member_ids.filtered(lambda m: m.is_self) + bot_member = discuss_channel.channel_member_ids.filtered( + lambda m: m.partner_id == self.chatbot_script.operator_partner_id + ) + guest_member = discuss_channel.channel_member_ids.filtered(lambda m: bool(m.guest_id)) + self.env["mail.presence"]._update_presence(guest_member.guest_id) + self_member._rtc_join_call() + self.assertTrue(guest_member.rtc_inviting_session_id) + self.assertFalse(bot_member.rtc_inviting_session_id) + + @freeze_time("2020-03-22 10:42:06") + def test_forward_to_specific_operator(self): + """Test _forward_operator takes into account the given users as candidates.""" + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "channel_id": self.livechat_channel.id, + "chatbot_script_id": self.chatbot_script.id, + }, + ) + discuss_channel = ( + self.env["discuss.channel"].sudo().browse(data["channel_id"]) + ) + discuss_channel._forward_human_operator(self.step_forward_operator) + self.assertEqual( + discuss_channel.livechat_operator_id, self.chatbot_script.operator_partner_id + ) + self.assertEqual(discuss_channel.name, "Testing Bot") + + member_bot = discuss_channel.channel_member_ids.filtered( + lambda m: m.partner_id == self.chatbot_script.operator_partner_id + ) + member_bot_data = { + "create_date": fields.Datetime.to_string(member_bot.create_date), + "fetched_message_id": False, + "id": member_bot.id, + "livechat_member_type": "bot", + "last_seen_dt": False, + "partner_id": member_bot.partner_id.id, + "seen_message_id": False, + "channel_id": {"id": discuss_channel.id, "model": "discuss.channel"}, + } + + def get_forward_op_bus_params(): + messages = self.env["mail.message"].search([], order="id desc", limit=3) + # only data relevant to the test are asserted for simplicity + transfer_message_data = Store(bus_channel=discuss_channel).add(messages[1]).get_result() + transfer_message_data["mail.message"][0].update( + { + "author_id": self.chatbot_script.operator_partner_id.id, + "body": ["markup", "

I will transfer you to a human.

"], + # thread not renamed yet at this step + "default_subject": "Testing Bot", + "record_name": "Testing Bot", + } + ) + transfer_message_data["mail.thread"][0]["display_name"] = "Testing Bot" + joined_message_data = Store(bus_channel=discuss_channel).add(messages[0]).get_result() + joined_message_data["mail.message"][0].update( + { + "author_id": self.chatbot_script.operator_partner_id.id, + "body": [ + "markup", + ( + '
' + ), + ], + # thread not renamed yet at this step + "default_subject": "Testing Bot", + "record_name": "Testing Bot", + } + ) + joined_message_data["mail.thread"][0]["display_name"] = "Testing Bot" + member_emp = discuss_channel.channel_member_ids.filtered( + lambda m: m.partner_id == self.partner_employee + ) + # data in-between join and leave + channel_data_join = ( + Store(bus_channel=member_emp._bus_channel()).add(discuss_channel).get_result() + ) + channel_data_join["discuss.channel"][0]["invited_member_ids"] = [["ADD", []]] + channel_data_join["discuss.channel"][0]["rtc_session_ids"] = [["ADD", []]] + channel_data_join["discuss.channel"][0]["livechat_outcome"] = "no_agent" + channel_data_join["discuss.channel"][0]["chatbot"]["currentStep"]["message"] = messages[1].id + channel_data_join["discuss.channel"][0]["chatbot"]["steps"][0]["message"] = messages[1].id + channel_data_join["discuss.channel"][0]["livechat_operator_id"] = self.chatbot_script.operator_partner_id.id + channel_data_join["discuss.channel"][0]["member_count"] = 3 + channel_data_join["discuss.channel"][0]["name"] = "Testing Bot" + channel_data_join["discuss.channel.member"].insert(0, member_bot_data) + channel_data_join["discuss.channel.member"][2]["fetched_message_id"] = False + channel_data_join["discuss.channel.member"][2]["last_seen_dt"] = False + channel_data_join["discuss.channel.member"][2]["seen_message_id"] = False + channel_data_join["discuss.channel.member"][2]["unpin_dt"] = False + del channel_data_join["res.partner"][1] + channel_data_join["res.partner"].insert( + 0, + { + "active": False, + "avatar_128_access_token": self.chatbot_script.operator_partner_id._get_avatar_128_access_token(), + "country_id": False, + "id": self.chatbot_script.operator_partner_id.id, + "im_status": "im_partner", + "im_status_access_token": self.chatbot_script.operator_partner_id._get_im_status_access_token(), + "is_public": False, + "mention_token": self.chatbot_script.operator_partner_id._get_mention_token(), + "name": "Testing Bot", + "user_livechat_username": False, + "write_date": fields.Datetime.to_string( + self.chatbot_script.operator_partner_id.write_date + ), + }, + ) + channel_data = Store().add(discuss_channel).get_result() + channel_data["discuss.channel"][0]["message_needaction_counter_bus_id"] = 0 + channel_data_emp = Store().add(discuss_channel.with_user(self.user_employee)).get_result() + channel_data_emp["discuss.channel"][0]["message_needaction_counter_bus_id"] = 0 + channel_data_emp["discuss.channel.member"][1]["message_unread_counter_bus_id"] = 0 + channel_data = Store().add(discuss_channel).get_result() + channel_data["discuss.channel"][0]["message_needaction_counter_bus_id"] = 0 + channels, message_items = ( + [ + (self.cr.dbname, "discuss.channel", discuss_channel.id), + (self.cr.dbname, "res.partner", self.partner_employee.id), + (self.cr.dbname, "discuss.channel", discuss_channel.id), + (self.cr.dbname, "discuss.channel", discuss_channel.id), + (self.cr.dbname, "discuss.channel", discuss_channel.id), + (self.cr.dbname, "discuss.channel", discuss_channel.id), + (self.cr.dbname, "discuss.channel", discuss_channel.id), + (self.cr.dbname, "res.partner", self.partner_employee.id), + (self.cr.dbname, "res.partner", self.env.user.partner_id.id), + ], + [ + { + "type": "discuss.channel/new_message", + "payload": { + "data": transfer_message_data, + "id": discuss_channel.id, + }, + }, + { + "type": "discuss.channel/joined", + "payload": { + "channel_id": discuss_channel.id, + "invite_to_rtc_call": False, + "data": channel_data_join, + "invited_by_user_id": self.env.user.id, + }, + }, + { + "type": "discuss.channel/new_message", + "payload": { + "data": joined_message_data, + "id": discuss_channel.id, + }, + }, + { + "type": "mail.record/insert", + "payload": { + "discuss.channel": [{"id": discuss_channel.id, "member_count": 3}], + "discuss.channel.member": [ + { + "create_date": fields.Datetime.to_string( + member_emp.create_date + ), + "fetched_message_id": False, + "id": member_emp.id, + "livechat_member_type": "agent", + "last_seen_dt": fields.Datetime.to_string( + member_emp.last_seen_dt + ), + "partner_id": self.partner_employee.id, + "seen_message_id": False, + "channel_id": { + "id": discuss_channel.id, + "model": "discuss.channel", + }, + } + ], + "res.country": [ + {"code": "BE", "id": self.env.ref("base.be").id, "name": "Belgium"} + ], + "res.partner": self._filter_partners_fields( + { + "active": True, + "avatar_128_access_token": self.partner_employee._get_avatar_128_access_token(), + "country_id": self.env.ref("base.be").id, + "id": self.partner_employee.id, + "im_status": "offline", + "im_status_access_token": self.partner_employee._get_im_status_access_token(), + "is_public": False, + "mention_token": self.partner_employee._get_mention_token(), + "name": "Ernest Employee", + "user_livechat_username": False, + "write_date": fields.Datetime.to_string( + self.partner_employee.write_date + ), + } + ), + }, + }, + { + "type": "mail.record/insert", + "payload": { + "discuss.channel": [ + { + "channel_member_ids": [["DELETE", [member_bot.id]]], + "id": discuss_channel.id, + "member_count": 2, + } + ] + }, + }, + { + "type": "mail.record/insert", + "payload": { + "discuss.channel": [ + { + "id": discuss_channel.id, + "livechat_operator_id": self.partner_employee.id, + "name": "OdooBot Ernest Employee", + }, + ], + "res.partner": self._filter_partners_fields( + { + "avatar_128_access_token": self.partner_employee._get_avatar_128_access_token(), + "id": self.partner_employee.id, + "name": "Ernest Employee", + "user_livechat_username": False, + "write_date": fields.Datetime.to_string( + self.partner_employee.write_date + ), + } + ), + }, + }, + {"type": "mail.record/insert", "payload": channel_data_emp}, + {"type": "mail.record/insert", "payload": channel_data}, + ], + ) + + return (channels, message_items) + with self.assertBus(get_params=get_forward_op_bus_params): + discuss_channel._forward_human_operator(self.step_forward_operator, users=self.user_employee) + self.assertEqual(discuss_channel.name, "OdooBot Ernest Employee") + self.assertEqual(discuss_channel.livechat_operator_id, self.partner_employee) + self.assertEqual(discuss_channel.livechat_outcome, "no_answer") + self.assertTrue( + discuss_channel.channel_member_ids.filtered( + lambda m: m.partner_id == self.partner_employee + and m.livechat_member_type == "agent" + ) + ) + + def test_chatbot_multiple_rules_on_same_url(self): + bob_user = new_test_user( + self.env, login="bob_user", groups="im_livechat.im_livechat_group_user,base.group_user" + ) + chatbot_no_operator = self.env["chatbot.script"].create( + { + "title": "Chatbot operators not available", + "script_step_ids": [ + Command.create( + { + "step_type": "text", + "message": "I'm shown because there is no operator available", + } + ) + ], + } + ) + chatbot_operator = self.env["chatbot.script"].create( + { + "title": "Chatbot operators available", + "script_step_ids": [ + Command.create( + { + "step_type": "text", + "message": "I'm shown because there is an operator available", + } + ) + ], + } + ) + self.livechat_channel.user_ids += bob_user + self.livechat_channel.rule_ids = self.env["im_livechat.channel.rule"].create( + [ + { + "channel_id": self.livechat_channel.id, + "chatbot_script_id": chatbot_no_operator.id, + "chatbot_enabled_condition": "only_if_no_operator", + "regex_url": "/", + "sequence": 1, + }, + { + "channel_id": self.livechat_channel.id, + "chatbot_script_id": chatbot_operator.id, + "regex_url": "/", + "sequence": 2, + }, + ] + ) + self.assertFalse(self.livechat_channel.available_operator_ids) + self.assertEqual( + self.env["im_livechat.channel.rule"] + .match_rule(self.livechat_channel.id, "/") + .chatbot_script_id, + chatbot_no_operator, + ) + self.env["mail.presence"]._update_presence(bob_user) + # Force the recomputation of `available_operator_ids` after bob becomes online + self.livechat_channel.invalidate_recordset(["available_operator_ids"]) + self.assertTrue(self.livechat_channel.available_operator_ids) + self.assertEqual( + self.env["im_livechat.channel.rule"] + .match_rule(self.livechat_channel.id, "/") + .chatbot_script_id, + chatbot_operator, + ) + + def test_chatbot_enabled_condition(self): + cases = [ + # condition - operator_available - expected_result + ("only_if_no_operator", False, True), + ("only_if_no_operator", True, False), + ("only_if_operator", True, True), + ("only_if_operator", False, False), + ("always", False, True), + ("always", True, True), + ] + for condition, operator_available, expected_result in cases: + self.livechat_channel.user_ids.unlink() + if operator_available: + operator_user = new_test_user( + self.env, + login=f"operator_user_{condition}_{operator_available}_{expected_result}", + groups="im_livechat.im_livechat_group_user,base.group_user", + ) + self.env["mail.presence"]._update_presence(operator_user) + self.livechat_channel.user_ids = operator_user + self.livechat_channel.rule_ids = self.env["im_livechat.channel.rule"].create( + { + "channel_id": self.livechat_channel.id, + "chatbot_script_id": self.chatbot_script.id, + "chatbot_enabled_condition": condition, + "regex_url": "/", + "sequence": 1, + } + ) + matching_rule = ( + self.env["im_livechat.channel.rule"].match_rule(self.livechat_channel.id, "/") + or self.env["im_livechat.channel.rule"] + ) + self.assertEqual( + matching_rule.chatbot_script_id, + self.chatbot_script if expected_result else self.env["chatbot.script"], + f"Condition: {condition}, Operator available: {operator_available}, Expected result: {expected_result}", + ) + + def test_chatbot_member_type(self): + """Ensure livechat_member_type are correctly set when using chatbot with a logged in user.""" + self.authenticate(self.user_employee.login, self.user_employee.login) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "chatbot_script_id": self.chatbot_script.id, + "channel_id": self.livechat_channel.id, + }, + ) + discuss_channel = self.env["discuss.channel"].browse(data["channel_id"]) + self.assertEqual( + discuss_channel.channel_member_ids.mapped("livechat_member_type"), + ["bot", "visitor"], + ) + + def test_chatbot_clear_answers_on_step_type_change(self): + chatbot = self.env['chatbot.script'].create({ + 'title': 'Clear Answer Test Bot', + 'script_step_ids': [Command.create({ + 'step_type': 'question_selection', + 'message': 'What do you want to do?', + 'answer_ids': [ + Command.create({'name': 'Buy'}), + Command.create({'name': 'Support'}), + ] + })] + }) + step = chatbot.script_step_ids[0] + answers = {a.name: a for a in step.answer_ids} + [step_2, step_3] = self.env['chatbot.script.step'].create([ + { + 'chatbot_script_id': chatbot.id, + 'step_type': 'text', + 'message': 'Great! Let me help you with buying.', + 'sequence': 2, + 'triggering_answer_ids': [Command.set(answers['Buy'].ids)], + }, + { + 'chatbot_script_id': chatbot.id, + 'step_type': 'text', + 'message': 'Sure! I can assist you with support.', + 'sequence': 3, + 'triggering_answer_ids': [Command.set(answers['Support'].ids)], + }, + ]) + action = self.env.ref('im_livechat.chatbot_script_action') + self.start_tour(f"/odoo/action-{action.id}", 'change_chatbot_step_type', login='admin') + self.assertFalse(step.answer_ids, "Answers were not cleared after step_type was changed.") + self.assertFalse(step_2.triggering_answer_ids, "Step 2 still has stale triggering answers.") + self.assertFalse(step_3.triggering_answer_ids, "Step 3 still has stale triggering answers.") diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_cors_livechat.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_cors_livechat.py new file mode 100644 index 0000000..72656f3 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_cors_livechat.py @@ -0,0 +1,84 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import tagged, HttpCase, JsonRpcException + + +@tagged("post_install", "-at_install") +class TestCorsLivechat(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.operator = cls.env["res.users"].create( + { + "name": "Operator", + "login": "operator", + } + ) + cls.env["mail.presence"]._update_presence(cls.operator) + cls.livechat_channel = cls.env["im_livechat.channel"].create( + {"name": "Test Livechat Channel", "user_ids": [cls.operator.id]} + ) + + def test_ignore_user_cookie(self): + self.authenticate("admin", "admin") + data = self.make_jsonrpc_request( + "/im_livechat/cors/get_session", + { + "channel_id": self.livechat_channel.id, + "persisted": True, + }, + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + self.assertEqual(channel.channel_member_ids[0].partner_id, self.operator.partner_id) + self.assertFalse(channel.channel_member_ids[1].partner_id) + self.assertTrue(channel.channel_member_ids[1].guest_id) + + def test_ignore_guest_cookie(self): + guest = self.env["mail.guest"].create({"name": "Visitor"}) + data = self.make_jsonrpc_request( + "/im_livechat/cors/get_session", + { + "channel_id": self.livechat_channel.id, + "persisted": True, + }, + cookies={guest._cookie_name: f'{guest.id}{guest._cookie_separator}{guest.access_token}'} + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + channel_guest = channel.channel_member_ids.filtered(lambda member: member.guest_id).guest_id + self.assertNotEqual(channel_guest, guest) + + def test_access_routes_with_valid_guest_token(self): + data = self.make_jsonrpc_request( + "/im_livechat/cors/get_session", + { + "channel_id": self.livechat_channel.id, + "persisted": True, + }, + ) + self.authenticate(None, None) + self.make_jsonrpc_request( + "/im_livechat/cors/channel/messages", + { + "guest_token": data["store_data"]["Store"]["guest_token"], + "channel_id": data["channel_id"], + }, + ) + + def test_access_denied_for_wrong_channel(self): + data = self.make_jsonrpc_request( + "/im_livechat/cors/get_session", + { + "channel_id": self.livechat_channel.id, + "persisted": True, + }, + ) + guest = self.env["mail.guest"].create({"name": "Visitor"}) + self.authenticate(None, None) + with self.assertRaises(JsonRpcException, msg="werkzeug.exceptions.NotFound"): + self.make_jsonrpc_request( + "/im_livechat/cors/channel/messages", + { + "guest_token": guest.access_token, + "channel_id": data["channel_id"], + }, + ) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_digest.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_digest.py new file mode 100644 index 0000000..3545f90 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_digest.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.digest.tests.common import TestDigestCommon +from odoo.tools import mute_logger +from odoo.tests import tagged + + +@tagged('post_install', '-at_install') +class TestLiveChatDigest(TestDigestCommon): + + @classmethod + @mute_logger('odoo.models.unlink') + def setUpClass(cls): + super().setUpClass() + + other_partner = cls.env['res.partner'].create({'name': 'Other Partner'}) + + cls.channels = cls.env['discuss.channel'].create([{ + 'name': 'Channel 1', + 'livechat_operator_id': cls.env.user.partner_id.id, + 'channel_type': 'livechat', + }, { + 'name': 'Channel 2', + 'livechat_operator_id': cls.env.user.partner_id.id, + 'channel_type': 'livechat', + }, { + 'name': 'Channel 3', + 'livechat_operator_id': other_partner.id, + 'channel_type': 'livechat', + }]) + + cls.env['rating.rating'].search([]).unlink() + + cls.env['rating.rating'].create([{ + 'rated_partner_id': cls.env.user.partner_id.id, + 'res_id': cls.channels[0].id, + 'res_model_id': cls.env['ir.model']._get('discuss.channel').id, + 'consumed': True, + 'rating': 5, + }, { + 'rated_partner_id': cls.env.user.partner_id.id, + 'res_id': cls.channels[0].id, + 'res_model_id': cls.env['ir.model']._get('discuss.channel').id, + 'consumed': True, + 'rating': 0, + }, { + 'rated_partner_id': cls.env.user.partner_id.id, + 'res_id': cls.channels[0].id, + 'res_model_id': cls.env['ir.model']._get('discuss.channel').id, + 'consumed': True, + 'rating': 3, + }, { + 'rated_partner_id': cls.env.user.partner_id.id, + 'res_id': cls.channels[0].id, + 'res_model_id': cls.env['ir.model']._get('discuss.channel').id, + 'consumed': True, + 'rating': 3, + }]) + + def test_kpi_livechat_rating_value(self): + # 1/3 of the ratings have 5/5 note (0 are ignored) + self.assertEqual(round(self.digest_1.kpi_livechat_rating_value, 2), 33.33) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_discuss_channel.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_discuss_channel.py new file mode 100644 index 0000000..50f44a2 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_discuss_channel.py @@ -0,0 +1,322 @@ +import json +from datetime import timedelta +from freezegun import freeze_time +from markupsafe import Markup + +from odoo import Command, fields +from odoo.tests import new_test_user, tagged, users +from odoo.addons.im_livechat.tests.common import TestImLivechatCommon, TestGetOperatorCommon +from odoo.addons.mail.tests.common import MailCase + + +@tagged("-at_install", "post_install") +class TestDiscussChannel(TestImLivechatCommon, TestGetOperatorCommon, MailCase): + def test_unfollow_from_non_member_does_not_close_livechat(self): + bob_user = new_test_user( + self.env, "bob_user", groups="base.group_user,im_livechat.im_livechat_group_manager" + ) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", {"channel_id": self.livechat_channel.id} + ) + chat = self.env["discuss.channel"].browse(data["channel_id"]) + self.assertFalse(chat.livechat_end_dt) + chat.with_user(bob_user).action_unfollow() + self.assertFalse(chat.livechat_end_dt) + chat.with_user(chat.livechat_operator_id.main_user_id).action_unfollow() + self.assertTrue(chat.livechat_end_dt) + + def test_human_operator_failure_states(self): + data = self.make_jsonrpc_request( + "/im_livechat/get_session", {"channel_id": self.livechat_channel.id} + ) + chat = self.env["discuss.channel"].browse(data["channel_id"]) + self.assertFalse(chat.chatbot_current_step_id) # assert there is no chatbot + self.assertEqual(chat.livechat_failure, "no_answer") + chat.with_user(chat.livechat_operator_id.main_user_id).message_post( + body="I am here to help!", + message_type="comment", + subtype_xmlid="mail.mt_comment", + ) + self.assertEqual(chat.livechat_failure, "no_failure") + + def test_chatbot_failure_states(self): + chatbot_script = self.env["chatbot.script"].create({"title": "Testing Bot"}) + self.livechat_channel.rule_ids = [(0, 0, {"chatbot_script_id": chatbot_script.id})] + self.env["chatbot.script.step"].create({ + "step_type": "forward_operator", + "message": "I will transfer you to a human.", + "chatbot_script_id": chatbot_script.id, + }) + bob_operator = new_test_user( + self.env, "bob_user", groups="im_livechat.im_livechat_group_user" + ) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"chatbot_script_id": chatbot_script.id, "channel_id": self.livechat_channel.id}, + ) + chat = self.env["discuss.channel"].browse(data["channel_id"]) + self.assertTrue(chat.chatbot_current_step_id) # assert there is a chatbot + self.assertEqual(chat.livechat_failure, "no_failure") + self.livechat_channel.user_ids = False # remove operators so forwarding will fail + chat._forward_human_operator(chat.chatbot_current_step_id) + self.assertEqual(chat.livechat_failure, "no_agent") + self.livechat_channel.user_ids += bob_operator + self.assertTrue(self.livechat_channel.available_operator_ids) + chat._forward_human_operator(chat.chatbot_current_step_id) + self.assertEqual(chat.livechat_operator_id, bob_operator.partner_id) + self.assertEqual(chat.livechat_failure, "no_answer") + chat.with_user(bob_operator).message_post( + body="I am here to help!", + message_type="comment", + subtype_xmlid="mail.mt_comment", + ) + self.assertEqual(chat.livechat_failure, "no_failure") + + def test_livechat_description_sync_to_internal_user_bus(self): + """Test the description of a livechat conversation is sent to the internal user bus.""" + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"channel_id": self.livechat_channel.id}, + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + with self.assertBus( + [(self.cr.dbname, "discuss.channel", channel.id, "internal_users")], + [ + { + "type": "mail.record/insert", + "payload": { + "discuss.channel": [ + { + "id": channel.id, + "description": "Description of the conversation", + } + ] + }, + } + ], + ): + channel.description = "Description of the conversation" + + def test_livechat_note_sync_to_internal_user_bus(self): + """Test that a livechat note is sent to the internal user bus.""" + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"channel_id": self.livechat_channel.id}, + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + with self.assertBus( + [(self.cr.dbname, "discuss.channel", channel.id, "internal_users")], + [ + { + "type": "mail.record/insert", + "payload": { + "discuss.channel": [ + { + "id": channel.id, + "livechat_note": [ + "markup", + "

This is a note for the internal user.

", + ], + } + ] + }, + } + ], + ): + channel.livechat_note = "This is a note for the internal user." + + def test_livechat_status_sync_to_internal_user_bus(self): + """Test that a livechat status is sent to the internal user bus.""" + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"channel_id": self.livechat_channel.id}, + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + with self.assertBus( + [(self.cr.dbname, "discuss.channel", channel.id, "internal_users")], + [ + { + "type": "mail.record/insert", + "payload": { + "discuss.channel": [ + { + "id": channel.id, + "livechat_status": "waiting", + } + ] + }, + } + ], + ): + channel.livechat_status = "waiting" + + def test_livechat_status_switch_on_operator_joined_batch(self): + """Test that the livechat status switches to 'in_progress' when an operator joins multiple channels in a batch, + and ensure re-adding the same member does not change the status.""" + channel_1 = self.env["discuss.channel"].create({ + "name": "Livechat Channel 1", + "channel_type": "livechat", + "livechat_operator_id": self.operators[0].partner_id.id, + }) + channel_2 = self.env["discuss.channel"].create({ + "name": "Livechat Channel 2", + "channel_type": "livechat", + "livechat_operator_id": self.operators[0].partner_id.id, + }) + bob_operator = new_test_user(self.env, "bob_user", groups="im_livechat.im_livechat_group_user") + channel_1.livechat_status = "need_help" + channel_2.livechat_status = "need_help" + self.assertEqual(channel_1.livechat_status, "need_help") + self.assertEqual(channel_2.livechat_status, "need_help") + self.assertFalse(channel_1.livechat_end_dt) + self.assertFalse(channel_2.livechat_end_dt) + + # Add the operator to both channels in a batch, which should switch their status to 'in_progress' + (channel_1 | channel_2).with_user(channel_1.livechat_operator_id.main_user_id).add_members( + partner_ids=bob_operator.partner_id.ids + ) + self.assertEqual(channel_1.livechat_status, "in_progress") + self.assertEqual(channel_2.livechat_status, "in_progress") + + # Re-add the same operator and ensure the status does not change + channel_1.livechat_status = "need_help" + self.assertEqual(channel_1.livechat_status, "need_help") + channel_1.with_user(channel_1.livechat_operator_id.main_user_id).add_members( + partner_ids=bob_operator.partner_id.ids + ) + self.assertEqual(channel_1.livechat_status, "need_help") + + def test_join_livechat_needing_help(self): + bob = self._create_operator() + john = self._create_operator() + jane = self._create_operator() + livechat_channel = self.env["im_livechat.channel"].create( + {"name": "Livechat Channel", "user_ids": (bob + jane + john).ids}, + ) + chat = self._create_conversation(livechat_channel, bob) + chat.livechat_status = "need_help" + has_joined = chat.with_user(john).livechat_join_channel_needing_help() + self.assertTrue(has_joined) + self.assertIn(john.partner_id, chat.channel_member_ids.partner_id) + self.assertEqual(chat.livechat_status, "in_progress") + has_joined = chat.with_user(jane).livechat_join_channel_needing_help() + self.assertFalse(has_joined) + self.assertNotIn(jane.partner_id, chat.channel_member_ids.partner_id) + + @users("michel") + def test_livechat_conversation_history(self): + """Test livechat conversation history formatting""" + def _convert_attachment_to_html(attachment): + attachment_data = { + "id": attachment.id, + "access_token": attachment.access_token, + "checksum": attachment.checksum, + "extension": "txt", + "mimetype": attachment.mimetype, + "filename": attachment.display_name, + "url": attachment.url, + } + return Markup( + "
", + ) % json.dumps({"fileData": attachment_data}) + + channel = self.env["discuss.channel"].create( + { + "name": "test", + "channel_type": "livechat", + "livechat_operator_id": self.operators[0].partner_id.id, + "channel_member_ids": [ + Command.create({"partner_id": self.operators[0].partner_id.id}), + Command.create({"partner_id": self.visitor_user.partner_id.id}), + ], + } + ) + attachment1 = self.env["ir.attachment"].create({"name": "test.txt"}) + attachment2 = self.env["ir.attachment"].with_user(self.visitor_user).create({"name": "test2.txt"}) + channel.message_post(body="Operator Here", message_type="comment") + channel.message_post(body="", message_type="comment", attachment_ids=[attachment1.id]) + channel.with_user(self.visitor_user).message_post(body="Visitor Here", message_type="comment") + channel.with_user(self.visitor_user).message_post(body="", message_type="comment", attachment_ids=[attachment2.id]) + channel.message_post(body="Some notification", message_type="notification") + channel_history = channel.with_user(self.visitor_user)._get_channel_history() + self.assertEqual( + channel_history, + "
Michel Operator:
Operator Here
%(attachment_1)s
" + "
Rajesh:
Visitor Here
%(attachment_2)s
" + % { + "attachment_1": _convert_attachment_to_html(attachment1), + "attachment_2": _convert_attachment_to_html(attachment2), + }, + ) + + def test_gc_bot_sessions_after_one_day_inactivity(self): + chatbot_script = self.env["chatbot.script"].create({"title": "Testing Bot"}) + self.livechat_channel.rule_ids = [Command.create({"chatbot_script_id": chatbot_script.id})] + self.env["chatbot.script.step"].create({ + "chatbot_script_id": chatbot_script.id, + "message": "Hello joey, how you doing?", + "step_type": "text", + }) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "anonymous_name": "Thomas", + "chatbot_script_id": chatbot_script.id, + "channel_id": self.livechat_channel.id, + }, + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + with freeze_time(fields.Datetime.to_string(fields.Datetime.now() + timedelta(hours=23))): + self.assertFalse(channel.livechat_end_dt) + with freeze_time(fields.Datetime.to_string(fields.Datetime.now() + timedelta(days=1))): + channel._gc_bot_only_ongoing_sessions() + self.assertTrue(channel.livechat_end_dt) + + def test_expertises_added_from_discuss_are_kept(self): + bob = self._create_operator() + jane = self._create_operator() + dog_expertise = self.env["im_livechat.expertise"].create({"name": "Dog"}) + operator_expertise_ids = dog_expertise + chatbot_script = self.env["chatbot.script"].create({"title": "Testing Bot"}) + self.env["chatbot.script.step"].create( + [ + { + "chatbot_script_id": chatbot_script.id, + "message": "Hello, how can I help you?", + "step_type": "free_input_single", + }, + { + "chatbot_script_id": chatbot_script.id, + "operator_expertise_ids": operator_expertise_ids, + "step_type": "forward_operator", + }, + ] + ) + self.livechat_channel.user_ids = jane + self.livechat_channel.rule_ids = [Command.create({"chatbot_script_id": chatbot_script.id})] + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "chatbot_script_id": chatbot_script.id, + "channel_id": self.livechat_channel.id, + }, + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + self.make_jsonrpc_request( + "/chatbot/step/trigger", + {"channel_id": channel.id, "chatbot_script_id": chatbot_script.id}, + ) + self.assertIn(jane.partner_id, channel.livechat_agent_history_ids.partner_id) + self.assertEqual(channel.livechat_expertise_ids, operator_expertise_ids) + cat_expertise = self.env["im_livechat.expertise"].create({"name": "Cat"}) + self.authenticate(jane.login, jane.login) + self.make_jsonrpc_request( + "/im_livechat/conversation/write_expertises", + { + "channel_id": channel.id, + "orm_commands": [Command.link(cat_expertise.id)], + }, + ) + self.assertEqual(channel.livechat_expertise_ids, operator_expertise_ids | cat_expertise) + channel._add_members(users=bob) + self.assertEqual(channel.livechat_expertise_ids, operator_expertise_ids | cat_expertise) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_get_discuss_channel.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_get_discuss_channel.py new file mode 100644 index 0000000..7976773 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_get_discuss_channel.py @@ -0,0 +1,435 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta +from freezegun import freeze_time +from unittest.mock import patch, PropertyMock + +from odoo import fields +from odoo.addons.im_livechat.tests.common import TestImLivechatCommon +from odoo.addons.mail.tests.common import MailCommon +from odoo.tests import new_test_user, tagged + + +@tagged("post_install", "-at_install") +class TestGetDiscussChannel(TestImLivechatCommon, MailCommon): + def test_get_discuss_channel(self): + """For a livechat with 5 available operators, we open 5 channels 5 times (25 channels total). + For every 5 channels opening, we check that all operators were assigned. + """ + + for _i in range(5): + discuss_channels = self._open_livechat_discuss_channel() + channel_operator_ids = [ + channel_info["livechat_operator_id"] for channel_info in discuss_channels + ] + self.assertTrue(all(partner_id in channel_operator_ids for partner_id in self.operators.mapped('partner_id').ids)) + + def test_channel_get_livechat_visitor_info(self): + self.maxDiff = None + belgium = self.env.ref('base.be') + test_user = self.env['res.users'].create({'name': 'Roger', 'login': 'roger', 'password': self.password, 'country_id': belgium.id}) + + # ensure visitor info are correct with anonymous + operator = self.operators[0] + with patch('odoo.http.GeoIP.country_code', new_callable=PropertyMock(return_value=belgium.code)): + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "previous_operator_id": operator.partner_id.id, + "channel_id": self.livechat_channel.id, + }, + )["store_data"] + channel_info = data["discuss.channel"][0] + self.assertEqual(channel_info["name"], "Visitor Michel Operator") + self.assertEqual(channel_info["country_id"], belgium.id) + self.assertEqual(data["res.country"], [{"code": "BE", "id": belgium.id, "name": "Belgium"}]) + + # ensure persona info are hidden (in particular email and real name when livechat username is present) + channel = self.env["discuss.channel"].browse(channel_info["id"]) + guest = channel.channel_member_ids.guest_id[0] + self.assertEqual( + data["mail.guest"], + [ + { + "avatar_128_access_token": guest._get_avatar_128_access_token(), + "country_id": belgium.id, + "id": guest.id, + "im_status": "offline", + "im_status_access_token": guest._get_im_status_access_token(), + "name": "Visitor", + "offline_since": False, + "write_date": fields.Datetime.to_string(guest.write_date), + }, + ], + ) + self.assertEqual( + data["res.partner"], + self._filter_partners_fields( + { + "active": False, + "avatar_128_access_token": self.partner_root._get_avatar_128_access_token(), + "id": self.user_root.partner_id.id, + "im_status": "bot", + "im_status_access_token": self.partner_root._get_im_status_access_token(), + "is_company": False, + "main_user_id": self.user_root.id, + "name": "OdooBot", + "write_date": fields.Datetime.to_string(self.user_root.partner_id.write_date), + }, + { + "active": True, + "avatar_128_access_token": operator.partner_id._get_avatar_128_access_token(), + "country_id": False, + "id": operator.partner_id.id, + "im_status": "offline", + "im_status_access_token": operator.partner_id._get_im_status_access_token(), + "is_public": False, + "mention_token": operator.partner_id._get_mention_token(), + "user_livechat_username": "Michel Operator", + "write_date": fields.Datetime.to_string(operator.write_date), + }, + ), + ) + self.assertEqual( + data["res.users"], + self._filter_users_fields( + { + "id": self.user_root.id, + "partner_id": self.partner_root.id, + "share": False, + }, + ), + ) + # ensure visitor info are correct with real user + self.authenticate(test_user.login, self.password) + data = self.make_jsonrpc_request('/im_livechat/get_session', { + 'previous_operator_id': operator.partner_id.id, + 'channel_id': self.livechat_channel.id, + })["store_data"] + channel_info = data["discuss.channel"][0] + self.assertEqual(channel_info["name"], "Roger Michel Operator") + self.assertEqual(channel_info["country_id"], belgium.id) + self.assertEqual(data["res.country"], [{"code": "BE", "id": belgium.id, "name": "Belgium"}]) + operator_member_domain = [ + ('channel_id', '=', channel_info['id']), + ('partner_id', '=', operator.partner_id.id), + ] + operator_member = self.env['discuss.channel.member'].search(operator_member_domain) + visitor_member_domain = [ + ('channel_id', '=', channel_info['id']), + ('partner_id', '=', test_user.partner_id.id), + ] + visitor_member = self.env['discuss.channel.member'].search(visitor_member_domain) + self.assertEqual( + data["res.partner"], + self._filter_partners_fields( + { + "active": False, + "avatar_128_access_token": self.partner_root._get_avatar_128_access_token(), + "email": "odoobot@example.com", + "id": self.user_root.partner_id.id, + "im_status": "bot", + "im_status_access_token": self.partner_root._get_im_status_access_token(), + "is_company": False, + "main_user_id": self.user_root.id, + "name": "OdooBot", + "write_date": fields.Datetime.to_string(self.user_root.partner_id.write_date), + }, + { + "active": True, + "avatar_128_access_token": test_user.partner_id._get_avatar_128_access_token(), + "country_id": belgium.id, + "id": test_user.partner_id.id, + "im_status": "offline", + "im_status_access_token": test_user.partner_id._get_im_status_access_token(), + "is_public": False, + "main_user_id": test_user.id, + "mention_token": test_user.partner_id._get_mention_token(), + "name": "Roger", + "email": test_user.partner_id.email, + "offline_since": False, + "user_livechat_username": False, + "write_date": fields.Datetime.to_string(test_user.write_date), + }, + { + "active": True, + "avatar_128_access_token": operator.partner_id._get_avatar_128_access_token(), + "country_id": False, + "id": operator.partner_id.id, + "im_status": "offline", + "im_status_access_token": operator.partner_id._get_im_status_access_token(), + "is_public": False, + "mention_token": operator.partner_id._get_mention_token(), + "user_livechat_username": "Michel Operator", + "write_date": fields.Datetime.to_string(operator.write_date), + }, + ), + ) + self.assertEqual( + data["res.users"], + self._filter_users_fields( + { + "id": self.user_root.id, + "employee_ids": [], + "partner_id": self.partner_root.id, + "share": False, + }, + { + "id": test_user.id, + "is_admin": False, + "is_livechat_manager": False, + "notification_type": "email", + "partner_id": test_user.partner_id.id, + "signature": ["markup", str(test_user.signature)], + "share": False, + }, + ), + ) + self.assertEqual( + data["discuss.channel.member"], + [ + { + "create_date": fields.Datetime.to_string(operator_member.create_date), + "fetched_message_id": False, + "id": operator_member.id, + "livechat_member_type": "agent", + "last_seen_dt": False, + "partner_id": operator.partner_id.id, + "seen_message_id": False, + "channel_id": {"id": channel_info["id"], "model": "discuss.channel"}, + }, + { + "create_date": fields.Datetime.to_string(visitor_member.create_date), + "custom_channel_name": False, + "custom_notifications": False, + "fetched_message_id": False, + "id": visitor_member.id, + "livechat_member_type": "visitor", + "last_interest_dt": fields.Datetime.to_string(visitor_member.last_interest_dt), + "last_seen_dt": False, + "message_unread_counter": 0, + "message_unread_counter_bus_id": self.env["bus.bus"]._bus_last_id() - 2, + "mute_until_dt": False, + "new_message_separator": 0, + "partner_id": test_user.partner_id.id, + "rtc_inviting_session_id": False, + "seen_message_id": False, + "unpin_dt": False, + "channel_id": {"id": channel_info["id"], "model": "discuss.channel"}, + }, + ], + ) + self.assertEqual(data["res.country"], [{"code": "BE", "id": belgium.id, "name": "Belgium"}]) + # ensure visitor info are correct when operator is testing themselves + operator = self.operators[0] + self.authenticate(operator.login, self.password) + data = self.make_jsonrpc_request('/im_livechat/get_session', { + 'previous_operator_id': operator.partner_id.id, + 'channel_id': self.livechat_channel.id, + })["store_data"] + channel_info = data["discuss.channel"][0] + operator_member_domain = [ + ('channel_id', '=', channel_info['id']), + ('partner_id', '=', operator.partner_id.id), + ] + operator_member = self.env['discuss.channel.member'].search(operator_member_domain) + self.assertEqual(channel_info['livechat_operator_id'], operator.partner_id.id) + self.assertEqual(channel_info["name"], "Michel Michel Operator") + self.assertEqual(channel_info['country_id'], False) + self.assertEqual( + data["res.partner"], + self._filter_partners_fields( + { + "active": False, + "avatar_128_access_token": self.partner_root._get_avatar_128_access_token(), + "email": "odoobot@example.com", + "id": self.user_root.partner_id.id, + "im_status": "bot", + "im_status_access_token": self.partner_root._get_im_status_access_token(), + "is_company": False, + "main_user_id": self.user_root.id, + "name": "OdooBot", + "write_date": fields.Datetime.to_string(self.user_root.partner_id.write_date), + }, + { + "active": True, + "avatar_128_access_token": operator.partner_id._get_avatar_128_access_token(), + "country_id": False, + "id": operator.partner_id.id, + "im_status": "offline", + "im_status_access_token": operator.partner_id._get_im_status_access_token(), + "is_public": False, + "main_user_id": operator.id, + "mention_token": operator.partner_id._get_mention_token(), + "name": "Michel", + "email": operator.email, + "user_livechat_username": "Michel Operator", + "write_date": fields.Datetime.to_string(operator.partner_id.write_date), + }, + ), + ) + self.assertEqual( + data["discuss.channel.member"], + [ + { + "create_date": fields.Datetime.to_string(operator_member.create_date), + "custom_channel_name": False, + "custom_notifications": False, + "fetched_message_id": False, + "id": operator_member.id, + "livechat_member_type": "agent", + "last_interest_dt": fields.Datetime.to_string(operator_member.last_interest_dt), + "last_seen_dt": False, + "message_unread_counter": 0, + "message_unread_counter_bus_id": self.env["bus.bus"]._bus_last_id() - 2, + "mute_until_dt": False, + "new_message_separator": 0, + "partner_id": operator.partner_id.id, + "rtc_inviting_session_id": False, + "seen_message_id": False, + "unpin_dt": fields.Datetime.to_string(operator_member.unpin_dt), + "channel_id": {"id": channel_info["id"], "model": "discuss.channel"}, + }, + ], + ) + self.assertEqual( + data["res.users"], + self._filter_users_fields( + { + "id": self.user_root.id, + "employee_ids": [], + "partner_id": self.partner_root.id, + "share": False, + }, + { + "id": operator.id, + "is_admin": False, + "is_livechat_manager": False, + "notification_type": "email", + "partner_id": operator.partner_id.id, + "share": False, + "signature": ["markup", str(operator.signature)], + }, + ), + ) + + def _open_livechat_discuss_channel(self): + discuss_channels = [] + for _i in range(5): + data = self.make_jsonrpc_request( + "/im_livechat/get_session", {"channel_id": self.livechat_channel.id} + ) + discuss_channels.append( + next( + filter( + lambda c: c["id"] == data["channel_id"], + data["store_data"]["discuss.channel"], + ) + ) + ) + # send a message to mark this channel as 'active' + self.env["discuss.channel"].browse(data["channel_id"]).message_post(body="cc") + return discuss_channels + + def test_channel_not_pinned_for_operator_before_first_message(self): + operator = self.operators[0] + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "channel_id": self.livechat_channel.id, + "previous_operator_id": operator.partner_id.id, + }, + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + member = channel.with_user(operator).self_member_id + self.assertEqual(member.partner_id, operator.partner_id, "operator should be member of channel") + self.assertFalse(member.is_pinned, "channel should not be pinned for operator initially") + channel.message_post(body="cc", message_type="comment") + self.assertTrue(member.is_pinned, "channel should be pinned for operator after visitor sent a message") + self.authenticate(operator.login, self.password) + data = self.make_jsonrpc_request("/mail/data", {"fetch_params": ["channels_as_member"]}) + channel_ids = [channel["id"] for channel in data["discuss.channel"]] + self.assertIn(channel.id, channel_ids, "channel should be fetched by operator on new page") + + def test_read_channel_unpined_for_operator_after_one_day(self): + data = self.make_jsonrpc_request( + "/im_livechat/get_session", {"channel_id": self.livechat_channel.id} + ) + member_of_operator = self.env["discuss.channel.member"].search( + [ + ("channel_id", "=", data["channel_id"]), + ("partner_id", "in", self.operators.partner_id.ids), + ] + ) + message = self.env["discuss.channel"].browse(data["channel_id"]).message_post(body="cc", message_type="comment") + member_of_operator._mark_as_read(message.id) + with freeze_time(fields.Datetime.to_string(fields.Datetime.now() + timedelta(days=1))): + member_of_operator._gc_unpin_livechat_sessions() + self.assertFalse(member_of_operator.is_pinned, "read channel should be unpinned after one day") + self.assertTrue(member_of_operator.channel_id.livechat_end_dt) + + def test_unread_channel_not_unpined_for_operator_after_autovacuum(self): + data = self.make_jsonrpc_request( + "/im_livechat/get_session", {"channel_id": self.livechat_channel.id} + ) + member_of_operator = self.env["discuss.channel.member"].search( + [ + ("channel_id", "=", data["channel_id"]), + ("partner_id", "in", self.operators.partner_id.ids), + ] + ) + self.env["discuss.channel"].browse(data["channel_id"]).message_post(body="cc", message_type="comment") + with freeze_time(fields.Datetime.to_string(fields.Datetime.now() + timedelta(days=1))): + member_of_operator._gc_unpin_livechat_sessions() + self.assertTrue(member_of_operator.is_pinned, "unread channel should not be unpinned after autovacuum") + self.assertFalse(member_of_operator.channel_id.livechat_end_dt) + + def test_livechat_manager_can_invite_anyone(self): + channel = self.env["discuss.channel"].create( + { + "channel_type": "livechat", + "livechat_operator_id": self.operators[2].partner_id.id, + "name": "test", + } + ) + other_member = channel.with_user(self.operators[0])._add_members(users=self.operators[1]) + self.assertEqual(other_member.partner_id, self.operators[1].partner_id) + self_member = channel.with_user(self.operators[0])._add_members(users=self.operators[0]) + self.assertEqual(self_member.partner_id, self.operators[0].partner_id) + + def test_livechat_operator_can_see_all_livechat_conversations_and_members(self): + bob_user = new_test_user( + self.env, "bob_user", groups="base.group_user,im_livechat.im_livechat_group_user" + ) + livechat_session = self.env["discuss.channel"].create( + { + "channel_type": "livechat", + "livechat_operator_id": self.operators[0].partner_id.id, + "name": "test", + } + ) + livechat_session.with_user(self.operators[0])._add_members(users=self.operators[1]) + self.assertEqual( + self.env["discuss.channel"].with_user(bob_user).search([("id", "=", livechat_session.id)]), + livechat_session + ) + self.assertEqual( + self.env["discuss.channel.member"].with_user(bob_user).search([("channel_id", "=", livechat_session.id)]), + livechat_session.channel_member_ids + ) + + def test_user_prevails_over_guest_when_creating_member(self): + test_user = new_test_user(self.env, "meow_user") + guest = self.env["mail.guest"].create({"name": "Guest"}) + self.authenticate(test_user.login, test_user.password) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"channel_id": self.livechat_channel.id}, + cookies={guest._cookie_name: guest._format_auth_cookie()}, + ) + channel_members = self.env["discuss.channel"].browse(data["channel_id"]).channel_member_ids + agent = channel_members.filtered(lambda member: member.livechat_member_type == "agent") + visitor = channel_members.filtered(lambda member: member.livechat_member_type == "visitor") + self.assertEqual(len(agent), 1) + self.assertEqual(len(visitor), 1) + self.assertEqual(visitor.partner_id, test_user.partner_id) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_get_mail_channel.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_get_mail_channel.py deleted file mode 100644 index 81efd7a..0000000 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_get_mail_channel.py +++ /dev/null @@ -1,165 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import timedelta -from freezegun import freeze_time - -from odoo import fields -from odoo.addons.im_livechat.tests.common import TestImLivechatCommon - - -class TestGetMailChannel(TestImLivechatCommon): - def test_get_mail_channel(self): - """For a livechat with 5 available operators, we open 5 channels 5 times (25 channels total). - For every 5 channels opening, we check that all operators were assigned. - """ - - for i in range(5): - mail_channels = self._open_livechat_mail_channel() - channel_operators = [channel_info['operator_pid'] for channel_info in mail_channels] - channel_operator_ids = [channel_operator[0] for channel_operator in channel_operators] - self.assertTrue(all(partner_id in channel_operator_ids for partner_id in self.operators.mapped('partner_id').ids)) - - def test_channel_get_livechat_visitor_info(self): - belgium = self.env.ref('base.be') - public_user = self.env.ref('base.public_user') - test_user = self.env['res.users'].create({'name': 'Roger', 'login': 'roger', 'country_id': belgium.id}) - - # ensure visitor info are correct with anonymous - operator = self.operators[0] - channel_info = self.livechat_channel.with_user(public_user)._open_livechat_mail_channel(anonymous_name='Visitor 22', previous_operator_id=operator.partner_id.id, country_id=belgium.id) - self.assertEqual(channel_info['channel']['anonymous_name'], "Visitor 22") - self.assertEqual(channel_info['channel']['anonymous_country'], {'code': 'BE', 'id': belgium.id, 'name': 'Belgium'}) - - # ensure member info are hidden (in particular email and real name when livechat username is present) - # shape of channelMembers is [('insert', data...)], [0][1] accesses the data - self.assertEqual(sorted(map(lambda m: m['persona']['partner'], channel_info['channel']['channelMembers'][0][1]), key=lambda m: m['id']), sorted([{ - 'active': True, - 'country': [('clear',)], - 'id': operator.partner_id.id, - 'is_public': False, - 'user_livechat_username': 'Michel Operator', - }, { - 'active': False, - 'id': public_user.partner_id.id, - 'is_public': True, - 'name': 'Public user', - }], key=lambda m: m['id'])) - - # ensure visitor info are correct with real user - channel_info = self.livechat_channel.with_user(test_user)._open_livechat_mail_channel(anonymous_name='whatever', previous_operator_id=operator.partner_id.id, user_id=test_user.id) - self.assertFalse(channel_info['channel']['anonymous_name']) - self.assertEqual(channel_info['channel']['anonymous_country'], [('clear',)]) - self.assertEqual(channel_info['channel']['channelMembers'], [('insert', [ - { - 'channel': {'id': channel_info['id']}, - 'id': self.env['mail.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', '=', operator.partner_id.id)]).id, - 'persona': { - 'partner': { - 'active': True, - 'country': [('clear',)], - 'id': operator.partner_id.id, - 'is_public': False, - 'user_livechat_username': 'Michel Operator', - }, - }, - }, - { - 'channel': {'id': channel_info['id']}, - 'id': self.env['mail.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', '=', test_user.partner_id.id)]).id, - 'persona': { - 'partner': { - 'active': True, - 'country': { - 'code': 'BE', - 'id': belgium.id, - 'name': 'Belgium', - }, - 'id': test_user.partner_id.id, - 'is_public': False, - 'name': 'Roger', - }, - }, - }, - ])]) - - # ensure visitor info are correct when operator is testing themselves - operator = self.operators[0] - channel_info = self.livechat_channel.with_user(operator)._open_livechat_mail_channel(anonymous_name='whatever', previous_operator_id=operator.partner_id.id, user_id=operator.id) - self.assertEqual(channel_info['operator_pid'], (operator.partner_id.id, "Michel Operator")) - self.assertFalse(channel_info['channel']['anonymous_name']) - self.assertEqual(channel_info['channel']['anonymous_country'], [('clear',)]) - self.assertEqual(channel_info['channel']['channelMembers'], [('insert', [ - { - 'channel': {'id': channel_info['id']}, - 'id': self.env['mail.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', '=', operator.partner_id.id)]).id, - 'persona': { - 'partner': { - 'active': True, - 'country': [('clear',)], - 'id': operator.partner_id.id, - 'is_public': False, - 'user_livechat_username': 'Michel Operator', - }, - }, - }, - ])]) - - def _open_livechat_mail_channel(self): - mail_channels = [] - - for i in range(5): - mail_channel = self.livechat_channel._open_livechat_mail_channel('Anonymous') - mail_channels.append(mail_channel) - # send a message to mark this channel as 'active' - self.env['mail.channel'].browse(mail_channel['id']).message_post(body='cc') - - return mail_channels - - def test_channel_not_pinned_for_operator_before_first_message(self): - public_user = self.env.ref('base.public_user') - channel_info = self.livechat_channel.with_user(public_user)._open_livechat_mail_channel(anonymous_name='whatever') - operator_channel_member = self.env['mail.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', 'in', self.operators.partner_id.ids)]) - self.assertEqual(len(operator_channel_member), 1, "operator should be member of channel") - self.assertFalse(operator_channel_member.is_pinned, "channel should not be pinned for operator initially") - self.env['mail.channel'].browse(channel_info['id']).message_post(body='cc') - self.assertTrue(operator_channel_member.is_pinned, "channel should be pinned for operator after visitor sent a message") - self.assertIn(channel_info['id'], operator_channel_member.partner_id._get_channels_as_member().ids, "channel should be fetched by operator on new page") - - def test_operator_livechat_username(self): - """Ensures the operator livechat_username is returned by `_channel_fetch_message`, which is - the method called by the public route displaying chat history.""" - public_user = self.env.ref('base.public_user') - operator = self.operators[0] - operator.write({ - 'email': 'michel@example.com', - 'livechat_username': 'Michel at your service', - }) - channel_info = self.livechat_channel.with_user(public_user).sudo()._open_livechat_mail_channel(anonymous_name='whatever') - channel = self.env['mail.channel'].browse(channel_info['id']) - channel.with_user(operator).message_post(body='Hello', message_type='comment', subtype_xmlid='mail.mt_comment') - message_formats = channel.with_user(public_user).sudo()._channel_fetch_message() - self.assertEqual(len(message_formats), 1) - self.assertEqual(message_formats[0]['author']['id'], operator.partner_id.id) - self.assertEqual(message_formats[0]['author']['user_livechat_username'], operator.livechat_username) - self.assertFalse(message_formats[0].get('email_from'), "should not send email_from to livechat user") - - def test_read_channel_unpined_for_operator_after_one_day(self): - public_user = self.env.ref('base.public_user') - channel_info = self.livechat_channel.with_user(public_user)._open_livechat_mail_channel(anonymous_name='visitor') - member_of_operator = self.env['mail.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', 'in', self.operators.partner_id.ids)]) - message = self.env['mail.channel'].browse(channel_info['id']).message_post(body='cc') - member_of_operator.channel_id.with_user(self.operators.filtered( - lambda operator: operator.partner_id == member_of_operator.partner_id - ))._channel_seen(message.id) - with freeze_time(fields.Datetime.to_string(fields.datetime.now() + timedelta(days=1))): - member_of_operator._gc_unpin_livechat_sessions() - self.assertFalse(member_of_operator.is_pinned, "read channel should be unpinned after one day") - - def test_unread_channel_not_unpined_for_operator_after_autovacuum(self): - public_user = self.env.ref('base.public_user') - channel_info = self.livechat_channel.with_user(public_user)._open_livechat_mail_channel(anonymous_name='visitor') - member_of_operator = self.env['mail.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', 'in', self.operators.partner_id.ids)]) - self.env['mail.channel'].browse(channel_info['id']).message_post(body='cc') - with freeze_time(fields.Datetime.to_string(fields.datetime.now() + timedelta(days=1))): - member_of_operator._gc_unpin_livechat_sessions() - self.assertTrue(member_of_operator.is_pinned, "unread channel should not be unpinned after autovacuum") diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_get_operator.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_get_operator.py new file mode 100644 index 0000000..5388f09 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_get_operator.py @@ -0,0 +1,484 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta +from unittest.mock import patch + +import odoo +from odoo import Command, fields +from odoo.addons.im_livechat.tests.common import TestGetOperatorCommon +from odoo.addons.mail.tests.common import MailCommon, freeze_all_time +from odoo.tests.common import users + + +@odoo.tests.tagged("-at_install", "post_install") +class TestGetOperator(MailCommon, TestGetOperatorCommon): + def setUp(self): + super().setUp() + random_choice_patch = patch("random.choice", lambda arr: arr[0]) + self.startPatcher(random_choice_patch) + + def test_get_by_lang(self): + fr_operator = self._create_operator("fr_FR") + en_operator = self._create_operator("en_US") + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [fr_operator.id, en_operator.id], + } + ) + self.assertEqual(fr_operator, livechat_channel._get_operator(lang="fr_FR")) + self.assertEqual(en_operator, livechat_channel._get_operator(lang="en_US")) + + def test_get_by_lang_both_operator_active(self): + fr_operator = self._create_operator("fr_FR") + en_operator = self._create_operator("en_US") + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [fr_operator.id, en_operator.id], + } + ) + self._create_conversation(livechat_channel, fr_operator) + self._create_conversation(livechat_channel, en_operator) + self._create_conversation(livechat_channel, en_operator) + self.assertEqual(en_operator, livechat_channel._get_operator(lang="en_US")) + + def test_get_by_lang_no_operator_matching_lang(self): + fr_operator = self._create_operator("fr_FR") + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [fr_operator.id], + } + ) + self.assertEqual(fr_operator, livechat_channel._get_operator(lang="en_US")) + + def test_get_by_country(self): + fr_operator = self._create_operator(country_code="FR") + en_operator = self._create_operator(country_code="US") + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [fr_operator.id, en_operator.id], + } + ) + self.assertEqual( + fr_operator, + livechat_channel._get_operator(country_id=self.env["res.country"].search([("code", "=", "FR")]).id), + ) + self.assertEqual( + en_operator, + livechat_channel._get_operator(country_id=self.env["res.country"].search([("code", "=", "US")]).id), + ) + + def test_get_by_country_no_operator_matching_country(self): + fr_operator = self._create_operator(country_code="FR") + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [fr_operator.id], + } + ) + self.assertEqual( + fr_operator, + livechat_channel._get_operator(country_id=self.env["res.country"].search([("code", "=", "US")]).id), + ) + + def test_get_by_lang_and_country_prioritize_lang(self): + fr_operator = self._create_operator("fr_FR", "FR") + en_operator = self._create_operator("en_US", "US") + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [fr_operator.id, en_operator.id], + } + ) + self.assertEqual( + fr_operator, + livechat_channel._get_operator( + lang="fr_FR", country_id=self.env["res.country"].search([("code", "=", "US")]).id + ), + ) + self.assertEqual( + en_operator, + livechat_channel._get_operator( + lang="en_US", country_id=self.env["res.country"].search([("code", "=", "FR")]).id + ), + ) + + def test_operator_in_call_no_more_than_two_chats(self): + first_operator = self._create_operator("fr_FR", "FR") + second_operator = self._create_operator("fr_FR", "FR") + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [first_operator.id, second_operator.id], + } + ) + with freeze_all_time(): + self._create_conversation(livechat_channel, first_operator) + self._create_conversation(livechat_channel, first_operator) + # Previous operator is not in a call so it should be available, even if + # he already has two ongoing chats. + self.assertEqual( + first_operator, livechat_channel._get_operator(previous_operator_id=first_operator.partner_id.id) + ) + self._create_conversation(livechat_channel, first_operator, in_call=True) + # Previous operator is in a call so it should not be available anymore. + self.assertEqual( + second_operator, livechat_channel._get_operator(previous_operator_id=first_operator.partner_id.id) + ) + + def test_priority_by_number_of_chat(self): + first_operator = self._create_operator() + second_operator = self._create_operator() + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [first_operator.id, second_operator.id], + } + ) + with freeze_all_time(): + self._create_conversation(livechat_channel, first_operator) + self._create_conversation(livechat_channel, second_operator) + self._create_conversation(livechat_channel, second_operator) + self.assertEqual(first_operator, livechat_channel._get_operator()) + + def test_in_call_operator_not_prioritized(self): + first_operator = self._create_operator() + second_operator = self._create_operator() + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [first_operator.id, second_operator.id], + } + ) + self._create_conversation(livechat_channel, first_operator, in_call=True) + self._create_conversation(livechat_channel, second_operator) + self.assertEqual(second_operator, livechat_channel._get_operator()) + + def test_priority_by_number_of_chat_with_call_limit_not_exceeded(self): + first_operator = self._create_operator() + second_operator = self._create_operator() + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [first_operator.id, second_operator.id], + } + ) + with freeze_all_time(): + self._create_conversation(livechat_channel, first_operator, in_call=True) + self._create_conversation(livechat_channel, second_operator) + self._create_conversation(livechat_channel, second_operator) + self.assertEqual(first_operator, livechat_channel._get_operator()) + + def test_priority_by_number_of_chat_all_operators_exceed_limit(self): + first_operator = self._create_operator() + second_operator = self._create_operator() + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [first_operator.id, second_operator.id], + } + ) + with freeze_all_time(): + self._create_conversation(livechat_channel, first_operator, in_call=True) + self._create_conversation(livechat_channel, first_operator) + self._create_conversation(livechat_channel, second_operator, in_call=True) + self._create_conversation(livechat_channel, second_operator) + self._create_conversation(livechat_channel, second_operator) + self.assertEqual(first_operator, livechat_channel._get_operator()) + + def test_get_by_expertise(self): + dog_expert = self.env["im_livechat.expertise"].create({"name": "dog"}) + cat_expert = self.env["im_livechat.expertise"].create({"name": "cat"}) + operator_dog = self._create_operator(expertises=dog_expert) + operator_car = self._create_operator(expertises=cat_expert) + all_operators = operator_dog + operator_car + pets_support = self.env["im_livechat.channel"].create( + {"name": "Pets", "user_ids": all_operators.ids} + ) + self.assertEqual(operator_dog, pets_support._get_operator(expertises=dog_expert)) + self.assertEqual(operator_car, pets_support._get_operator(expertises=cat_expert)) + + def test_get_by_expertise_amongst_same_language(self): + dog_expert = self.env["im_livechat.expertise"].create({"name": "dog"}) + cat_expert = self.env["im_livechat.expertise"].create({"name": "cat"}) + operator_fr_dog = self._create_operator("fr_FR", expertises=dog_expert) + operator_fr_cat = self._create_operator("fr_FR", expertises=cat_expert) + operator_fr_dog_cat = self._create_operator("fr_FR", expertises=dog_expert + cat_expert) + operator_en_dog = self._create_operator("en_US", expertises=dog_expert) + operator_en_cat = self._create_operator("en_US", expertises=cat_expert) + all_operators = ( + operator_fr_dog + + operator_fr_cat + + operator_fr_dog_cat + + operator_en_dog + + operator_en_cat + ) + pets_support = self.env["im_livechat.channel"].create( + {"name": "Pets", "user_ids": all_operators.ids} + ) + self.assertEqual( + operator_fr_dog, pets_support._get_operator(lang="fr_FR", expertises=dog_expert) + ) + self.assertEqual( + operator_en_cat, pets_support._get_operator(lang="en_US", expertises=cat_expert) + ) + self.assertEqual( + operator_fr_dog_cat, pets_support._get_operator(lang="fr_FR", expertises=dog_expert + cat_expert) + ) + self.assertEqual( + operator_en_dog, pets_support._get_operator(lang="en_US", expertises=dog_expert + cat_expert) + ) + + @users("employee") + def test_max_sessions_mode_limited(self): + """Test operator is not available when they reached the livechat channel limit.""" + operator = self._create_operator() + livechat_channel_data = { + "name": "Livechat Channel", + "user_ids": operator, + "max_sessions_mode": "limited", + "max_sessions": 2, + } + livechat_channel = self.env["im_livechat.channel"].sudo().create(livechat_channel_data) + self.assertEqual(livechat_channel.available_operator_ids, operator) + self._create_conversation(livechat_channel, operator) + self.assertEqual(livechat_channel.available_operator_ids, operator) + self._create_conversation(livechat_channel, operator) + self.assertFalse(livechat_channel.available_operator_ids) + + @users("employee") + def test_max_sessions_mode_limited_multi_operators(self): + """Test second operator is available when first operator reached the livechat channel + limit.""" + operator_1 = self._create_operator() + operator_2 = self._create_operator() + livechat_channel_data = { + "name": "Livechat Channel", + "user_ids": operator_1 + operator_2, + "max_sessions_mode": "limited", + "max_sessions": 2, + } + livechat_channel = self.env["im_livechat.channel"].sudo().create(livechat_channel_data) + self._create_conversation(livechat_channel, operator_1) + self.assertEqual(livechat_channel.available_operator_ids, operator_1 + operator_2) + self._create_conversation(livechat_channel, operator_1) + self.assertEqual(livechat_channel.available_operator_ids, operator_2) + self._create_conversation(livechat_channel, operator_2) + self.assertEqual(livechat_channel.available_operator_ids, operator_2) + + @users("employee") + def test_block_assignment_during_call(self): + """Test operator is not available when they are in call, even below the livechat channel + limit.""" + operator = self._create_operator() + livechat_channel_data = { + "name": "Livechat Channel", + "user_ids": operator, + "block_assignment_during_call": True, + } + livechat_channel = self.env["im_livechat.channel"].sudo().create(livechat_channel_data) + with freeze_all_time(): + self._create_conversation(livechat_channel, operator, in_call=True) + self.assertFalse(livechat_channel.available_operator_ids) + + @users("employee") + def test_max_sessions_mode_multi_channel(self): + """Test operator is available in second channel even when they reached the livechat channel + limit on the first channel.""" + operator = self._create_operator() + livechat_channels_data = [ + { + "name": "Livechat Channel", + "user_ids": [operator.id], + "max_sessions_mode": "limited", + "max_sessions": 2, + }, + { + "name": "Livechat Channel", + "user_ids": [operator.id], + }, + ] + livechat_channels = self.env["im_livechat.channel"].sudo().create(livechat_channels_data) + self._create_conversation(livechat_channels[0], operator) + self._create_conversation(livechat_channels[0], operator) + self.assertFalse(livechat_channels[0].available_operator_ids) + self.assertEqual(livechat_channels[1].available_operator_ids, operator) + + @users("employee") + def test_operator_max(self): + operator = self._create_operator() + livechat_channels_data = [ + { + "name": "Livechat Channel", + "user_ids": [operator.id], + "max_sessions_mode": "limited", + "max_sessions": 2, + }, + { + "name": "Livechat Channel", + "user_ids": [operator.id], + }, + ] + livechat_channels = self.env["im_livechat.channel"].sudo().create(livechat_channels_data) + self._create_conversation(livechat_channels[1], operator) + self._create_conversation(livechat_channels[1], operator) + self.assertEqual(livechat_channels[0].available_operator_ids, operator) + + @users("employee") + def test_operator_expired_channel(self): + operator = self._create_operator() + livechat_channel_data = { + "name": "Livechat Channel", + "user_ids": [operator.id], + "max_sessions_mode": "limited", + "max_sessions": 1, + } + livechat_channel = self.env["im_livechat.channel"].sudo().create(livechat_channel_data) + channel_data = { + "name": "Visitor 1", + "channel_type": "livechat", + "livechat_channel_id": livechat_channel.id, + "livechat_operator_id": operator.partner_id.id, + "channel_member_ids": [Command.create({"partner_id": operator.partner_id.id})], + "last_interest_dt": fields.Datetime.now() - timedelta(minutes=4), + } + channel = self.env["discuss.channel"].create(channel_data) + self.assertFalse(livechat_channel.available_operator_ids) + channel.write({"last_interest_dt": fields.Datetime.now() - timedelta(minutes=20)}) + self.assertEqual(livechat_channel.available_operator_ids, operator) + + @users("employee") + def test_non_member_operator_availability(self): + """Test the availability of an operator not member of any livechat channel is properly + computed when explicitly passing them to _get_available_operators_by_livechat_channel.""" + operator = self._create_operator() + livechat_channels_data = [ + { + "name": "Livechat Channel 1", + "max_sessions_mode": "limited", + "max_sessions": 2, + }, + { + "name": "Livechat Channel 2", + }, + ] + livechat_channels = self.env["im_livechat.channel"].sudo().create(livechat_channels_data) + self.assertFalse(livechat_channels[0].available_operator_ids) + self.assertFalse(livechat_channels[1].available_operator_ids) + self.assertEqual( + livechat_channels._get_available_operators_by_livechat_channel(operator), + { + livechat_channels[0]: operator, + livechat_channels[1]: operator, + }, + ) + self._create_conversation(livechat_channels[0], operator) + self._create_conversation(livechat_channels[0], operator) + self.assertEqual( + livechat_channels._get_available_operators_by_livechat_channel(operator), + { + livechat_channels[0]: self.env["res.users"], + livechat_channels[1]: operator, + }, + ) + operator.presence_ids.status = "offline" + self.assertEqual( + livechat_channels._get_available_operators_by_livechat_channel(operator), + { + livechat_channels[0]: self.env["res.users"], + livechat_channels[1]: self.env["res.users"], + }, + ) + + @users("employee") + def test_get_non_member_operator(self): + """Test _get_operator works with a given list of operators that are not members of the + livechat channel""" + operator_1 = self._create_operator(lang_code="fr_FR") + operator_2 = self._create_operator(lang_code="en_US") + all_operators = operator_1 + operator_2 + livechat_channel_data = {"name": "Livechat Channel 2"} + livechat_channel = self.env["im_livechat.channel"].sudo().create(livechat_channel_data) + self.assertFalse(livechat_channel._get_operator()) + self.assertFalse( + livechat_channel._get_operator(previous_operator_id=operator_1.partner_id.id) + ) + self.assertEqual(livechat_channel._get_operator(users=all_operators), operator_1) + self.assertEqual( + livechat_channel._get_operator(previous_operator_id=operator_2.partner_id.id, users=all_operators), + operator_2, + ) + self.assertEqual( + livechat_channel._get_operator(lang="en_US", users=all_operators), operator_2 + ) + + def test_buffer_time_multi_operator(self): + first_operator = self._create_operator() + second_operator = self._create_operator() + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [first_operator.id, second_operator.id], + } + ) + now = fields.Datetime.now() + with freeze_all_time(now + timedelta(minutes=-3)): + self._create_conversation(livechat_channel, second_operator) + with freeze_all_time(now): + self._create_conversation(livechat_channel, first_operator) + self.assertEqual(second_operator, livechat_channel._get_operator()) + with freeze_all_time(now + timedelta(seconds=121)): + self.assertEqual(first_operator, livechat_channel._get_operator()) + + def test_bypass_buffer_time_when_impossible_selection(self): + first_operator = self._create_operator() + second_operator = self._create_operator() + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [first_operator.id, second_operator.id], + } + ) + now = fields.Datetime.now() + with freeze_all_time(now): + self._create_conversation(livechat_channel, first_operator) + self._create_conversation(livechat_channel, second_operator) + self.assertEqual(first_operator, livechat_channel._get_operator()) + + def test_operator_freed_after_chat_ends(self): + first_operator = self._create_operator() + second_operator = self._create_operator() + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [first_operator.id, second_operator.id], + } + ) + self.assertEqual(first_operator, livechat_channel._get_operator()) + chat = self._create_conversation(livechat_channel, first_operator) + self.assertEqual(second_operator, livechat_channel._get_operator()) + chat.livechat_end_dt = fields.Datetime.now() + chat.flush_recordset(["livechat_end_dt"]) + self.assertEqual(first_operator, livechat_channel._get_operator()) + + def test_agent_availability_not_affected_by_custom_im_status(self): + agent = self._create_operator() + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [agent.id], + } + ) + self.assertEqual(agent.presence_ids.status, "online") + self.assertEqual(agent, livechat_channel._get_operator()) + agent.manual_im_status = "busy" + self.assertEqual(agent, livechat_channel._get_operator()) + agent.manual_im_status = "away" + self.assertEqual(agent, livechat_channel._get_operator()) + agent.manual_im_status = "offline" + self.assertEqual(agent, livechat_channel._get_operator()) + agent.presence_ids.status = "away" + self.assertEqual(self.env["res.users"], livechat_channel._get_operator()) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_calls.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_calls.py new file mode 100644 index 0000000..f74be7b --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_calls.py @@ -0,0 +1,29 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from functools import wraps +from unittest.mock import patch + +from odoo.tests.common import tagged +from odoo.addons.im_livechat.controllers.main import LivechatController +from odoo.addons.im_livechat.tests.common import TestImLivechatCommon + + +@tagged("post_install", "-at_install") +class TestImLivechatCalls(TestImLivechatCommon): + def test_meeting_view(self): + og_get_session = LivechatController.get_session + + def _patched_get_session(*args, **kwargs): + result = og_get_session(*args, **kwargs) + if kwargs["persisted"]: + self.env.flush_all() + channel = self.env["discuss.channel"].search([("id", "=", result["channel_id"])]) + agent = channel.channel_member_ids.filtered(lambda m: m.partner_id) + agent.sudo()._rtc_join_call() + return result + + with patch.object(LivechatController, "get_session", wraps(og_get_session)(_patched_get_session)): + self.start_tour( + f"/im_livechat/support/{self.livechat_channel.id}", + "im_livechat.meeting_view_tour", + ) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_channel.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_channel.py new file mode 100644 index 0000000..3153761 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_channel.py @@ -0,0 +1,116 @@ +from datetime import timedelta + +from odoo.tests import new_test_user, tagged +from odoo.exceptions import AccessError, ValidationError +from odoo.addons.im_livechat.tests.common import TestImLivechatCommon +from odoo.addons.im_livechat.tests.test_get_operator import TestGetOperator +from odoo.fields import Command, Datetime + + +@tagged("-at_install", "post_install") +class TestImLivechatChannel(TestImLivechatCommon, TestGetOperator): + def test_user_cant_join_livechat_channel(self): + bob_user = new_test_user(self.env, "bob_user", groups="base.group_user") + with self.assertRaises(AccessError): + self.livechat_channel.with_user(bob_user).action_join() + + def test_operator_join_leave_livechat_channel(self): + bob_operator = new_test_user( + self.env, "bob_user", groups="base.group_user,im_livechat.im_livechat_group_user" + ) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + self.livechat_channel.with_user(bob_operator).action_join() + self.assertIn(bob_operator, self.livechat_channel.user_ids) + self.livechat_channel.with_user(bob_operator).action_quit() + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + + def test_leave_livechat_channels_when_operator_access_revoked(self): + bob_operator = new_test_user( + self.env, "bob_user", groups="im_livechat.im_livechat_group_user" + ) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + self.livechat_channel.with_user(bob_operator).action_join() + self.assertIn(bob_operator, self.livechat_channel.user_ids) + livechat_operator_group = self.env.ref("im_livechat.im_livechat_group_user") + bob_operator.write({ + "group_ids": [Command.unlink(livechat_operator_group.id)], + }) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + bob_operator.write({ + "group_ids": [Command.link(livechat_operator_group.id)], + }) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + + def test_leave_livechat_channels_when_manager_access_revoked(self): + bob_operator = new_test_user( + self.env, "bob_user", groups="im_livechat.im_livechat_group_manager" + ) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + self.livechat_channel.with_user(bob_operator).action_join() + self.assertIn(bob_operator, self.livechat_channel.user_ids) + livechat_manager_group = self.env.ref("im_livechat.im_livechat_group_manager") + bob_operator.write({ + "group_ids": [Command.unlink(livechat_manager_group.id)], + }) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + bob_operator.write({ + "group_ids": [Command.link(livechat_manager_group.id)], + }) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + + def test_leave_livechat_channels_when_operator_removed_from_group(self): + bob_operator = new_test_user( + self.env, "bob_user", groups="im_livechat.im_livechat_group_user" + ) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + self.livechat_channel.with_user(bob_operator).action_join() + self.assertIn(bob_operator, self.livechat_channel.user_ids) + livechat_operator_group = self.env.ref("im_livechat.im_livechat_group_user") + livechat_operator_group.write({ + "user_ids": [Command.unlink(bob_operator.id)], + }) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + livechat_operator_group.write({ + "user_ids": [Command.link(bob_operator.id)], + }) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + + def test_leave_livechat_channels_when_manager_removed_from_group(self): + bob_operator = new_test_user( + self.env, "bob_user", groups="im_livechat.im_livechat_group_manager" + ) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + self.livechat_channel.with_user(bob_operator).action_join() + self.assertIn(bob_operator, self.livechat_channel.user_ids) + livechat_manager_group = self.env.ref("im_livechat.im_livechat_group_manager") + livechat_manager_group.write({ + "user_ids": [Command.unlink(bob_operator.id)], + }) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + livechat_manager_group.write({ + "user_ids": [Command.link(bob_operator.id)], + }) + self.assertNotIn(bob_operator, self.livechat_channel.user_ids) + + def test_review_link(self): + with self.assertRaises(ValidationError): + self.livechat_channel.review_link = "javascript:alert('hello')" + with self.assertRaises(ValidationError): + self.livechat_channel.review_link = "https://" + self.livechat_channel.review_link = "https://www.odoo.com" + self.assertEqual(self.livechat_channel.review_link, "https://www.odoo.com") + + def test_ongoing_session_count(self): + self.authenticate(None, None) + john = self._create_operator("fr_FR") + livechat_channel = self.env["im_livechat.channel"].create( + {"name": "Livechat Channel", "user_ids": [john.id]}, + ) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"channel_id": livechat_channel.id}, + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + self.assertEqual(livechat_channel.ongoing_session_count, 1) + channel.livechat_end_dt = Datetime.now() - timedelta(minutes=2) + self.assertEqual(livechat_channel.ongoing_session_count, 0) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_report.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_report.py index 9fc5434..9e14a50 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_report.py +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_report.py @@ -1,55 +1,177 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import datetime, timedelta +from freezegun import freeze_time from unittest.mock import patch +from odoo import Command from odoo.addons.im_livechat.tests.common import TestImLivechatCommon +from odoo.tests.common import new_test_user, tagged +@tagged("post_install", "-at_install") class TestImLivechatReport(TestImLivechatCommon): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.env['mail.channel'].search([('livechat_channel_id', '!=', False)]).unlink() + def setUp(self): + super().setUp() + self.env['discuss.channel'].search([('livechat_channel_id', '!=', False)]).unlink() - with patch.object(type(cls.env['im_livechat.channel']), '_get_available_users', lambda _: cls.operators): - channel_id = cls.livechat_channel._open_livechat_mail_channel('Anonymous')['id'] + def _compute_available_operator_ids(channel_self): + for record in channel_self: + record.available_operator_ids = self.operators + with ( + patch.object( + type(self.env["im_livechat.channel"]), + "_compute_available_operator_ids", + _compute_available_operator_ids, + ), + freeze_time("2023-03-17 06:05:54"), + ): + channel_id = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"channel_id": self.livechat_channel.id}, + )["channel_id"] - channel = cls.env['mail.channel'].browse(channel_id) - cls.operator = channel.livechat_operator_id + channel = self.env['discuss.channel'].browse(channel_id) + self.operator = channel.livechat_operator_id - cls._create_message(channel, cls.visitor_user.partner_id, '2023-03-17 06:05:54') - cls._create_message(channel, cls.operator, '2023-03-17 08:15:54') - cls._create_message(channel, cls.operator, '2023-03-17 08:45:54') + self._create_message(channel, self.visitor_user.partner_id, '2023-03-17 06:06:59') + self._create_message(channel, self.operator, '2023-03-17 08:15:54') + self._create_message(channel, self.operator, '2023-03-17 08:45:54') # message with the same record id, but with a different model # should not be taken into account for statistics - partner_message = cls._create_message(channel, cls.operator, '2023-03-17 05:05:54') - partner_message |= cls._create_message(channel, cls.operator, '2023-03-17 09:15:54') + partner_message = self._create_message(channel, self.operator, '2023-03-17 05:05:54') + partner_message |= self._create_message(channel, self.operator, '2023-03-17 09:15:54') partner_message.model = 'res.partner' - cls.env['mail.message'].flush_model() + + with freeze_time("2023-03-17 09:20:54"): + self.make_jsonrpc_request( + "/im_livechat/visitor_leave_session", + {"channel_id": channel_id} + ) + self.env['mail.message'].flush_model() def test_im_livechat_report_channel(self): report = self.env['im_livechat.report.channel'].search([('livechat_channel_id', '=', self.livechat_channel.id)]) self.assertEqual(len(report), 1, 'Should have one channel report for this live channel') # We have those messages, ordered by creation; # 05:05:54: wrong model - # 06:05:54: visitor message + # 06:05:54: session create + # 06:06:59: visitor message # 08:15:54: operator first answer # 08:45:54: operator second answer # 09:15:54: wrong model - # So the duration of the session is: (08:45:54 - 06:05:54) = 2h40 = 9600 seconds + # So the duration of the session is: (09:20:54 - 06:05:54) = 3h15 = 195 minutes # The time to answer of this session is: (08:15:54 - 06:05:54) = 2h10 = 7800 seconds - self.assertEqual(int(report.time_to_answer), 7800) - self.assertEqual(int(report.duration), 9600) + self.assertEqual(report.time_to_answer, 7800 / 3600) + self.assertEqual(int(report.duration), 195) def test_im_livechat_report_operator(self): - result = self.env['im_livechat.report.operator'].read_group([], ['time_to_answer:avg', 'duration:avg'], []) + result = self.env["im_livechat.report.channel"].formatted_read_group([], aggregates=["time_to_answer:avg", "duration:avg"]) self.assertEqual(len(result), 1) - self.assertEqual(int(result[0]['time_to_answer']), 7800) - self.assertEqual(int(result[0]['duration']), 9600) + self.assertEqual(result[0]["time_to_answer:avg"], 7800 / 3600) + self.assertEqual(int(result[0]['duration:avg']), 195) + channel = self.env["discuss.channel"].search([("livechat_channel_id", "=", self.livechat_channel.id)]) + rated_channel = channel.copy({"rating_last_value": 5}) + self._create_message(rated_channel, self.operator, "2023-03-18 11:00:00") + result = self.env["im_livechat.report.channel"].formatted_read_group([], aggregates=["rating:avg"]) + self.assertEqual(result[0]["rating:avg"], 5, "Rating average should be 5, excluding unrated sessions") @classmethod def _create_message(cls, channel, author, date): with patch.object(cls.env.cr, 'now', lambda: date): return channel.message_post(author_id=author.id, body=f'Message {date}') + + def test_redirect_to_form_from_pivot(self): + operator_1 = new_test_user(self.env, login="operator_1", groups="im_livechat.im_livechat_group_manager") + operator_2 = new_test_user(self.env, login="operator_2") + livechat_channel = self.env["im_livechat.channel"].create( + {"name": "Support", "user_ids": [operator_1.id, operator_2.id]} + ) + [partner_1, partner_2] = self.env["res.partner"].create([{"name": "test 1"}, {"name": "test 2"}]) + [channel_1, channel_2, channel_3] = self.env["discuss.channel"].create( + [{ + "name": "test 1", + "channel_type": "livechat", + "livechat_channel_id": livechat_channel.id, + "livechat_operator_id": operator_1.partner_id.id, + "channel_member_ids": [Command.create({"partner_id": partner_1.id})], + }, + { + "name": "test 2", + "channel_type": "livechat", + "livechat_channel_id": livechat_channel.id, + "livechat_operator_id": operator_2.partner_id.id, + "channel_member_ids": [Command.create({"partner_id": partner_2.id})], + }, + { + "name": "test 3", + "channel_type": "livechat", + "livechat_channel_id": livechat_channel.id, + "livechat_operator_id": operator_2.partner_id.id, + "channel_member_ids": [Command.create({"partner_id": partner_2.id})], + }] + ) + self._create_message(channel_1, operator_1.partner_id, "2025-06-26 10:05:00") + self._create_message(channel_2, operator_2.partner_id, "2025-06-26 10:15:00") + self._create_message(channel_3, operator_2.partner_id, "2025-06-26 10:25:00") + agent_report_action = self.env.ref("im_livechat.im_livechat_agent_history_action") + session_report_action = self.env.ref("im_livechat.im_livechat_report_channel_action") + self.start_tour( + f"/odoo/action-{agent_report_action.id}?view_type=pivot", + "im_livechat_agents_report_pivot_redirect_tour", + login="operator_1", + ) + self.start_tour( + f"/odoo/action-{session_report_action.id}?view_type=pivot", + "im_livechat_sessions_report_pivot_redirect_tour", + login="operator_1", + ) + + def test_day_of_week_ordered_by_lang_week_start(self): + agent = new_test_user(self.env, login="test_agent", groups="im_livechat.im_livechat_group_manager") + livechat_channel = self.env["im_livechat.channel"].create( + {"name": "Test Support", "user_ids": [agent.id]} + ) + today_dt = datetime.now() + for i in range(1, 8): + date = today_dt + timedelta(days=i) + with freeze_time(date): + channel_id = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"anonymous_name": f"Visitor_{i}", "channel_id": livechat_channel.id}, + )["channel_id"] + channel = self.env["discuss.channel"].browse(channel_id) + self._create_message(channel, channel.livechat_operator_id, date) + en_lang = self.env["res.lang"]._activate_lang("en_US") + report_model = self.env["im_livechat.report.channel"].with_user(agent) + result = report_model.formatted_read_group(domain=[], groupby=["day_number"]) + expected_order = ["0", "1", "2", "3", "4", "5", "6"] + actual_order = [group["day_number"] for group in result] + self.assertEqual(actual_order, expected_order) + en_lang.week_start = "6" + result = report_model.formatted_read_group(domain=[], groupby=["day_number"]) + expected_order = ["6", "0", "1", "2", "3", "4", "5"] + actual_order = [group["day_number"] for group in result] + self.assertEqual( + actual_order, + expected_order, + f"Days should be ordered starting from Saturday. Got {actual_order}, expected {expected_order}" + ) + + def test_time_to_answer_does_not_count_messages_after_close(self): + with freeze_time("2025-05-20 06:00:00") as frozen_time: + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"channel_id": self.livechat_channel.id}, + ) + chat = self.env["discuss.channel"].browse(data["channel_id"]) + frozen_time.tick(delta=timedelta(minutes=1)) + chat.message_post(body="Question", message_type="comment") + self._create_message(chat, self.env.user.partner_id, datetime.now()) + frozen_time.tick(delta=timedelta(minutes=5)) + self.make_jsonrpc_request("/im_livechat/visitor_leave_session", {"channel_id": chat.id}) + frozen_time.tick(delta=timedelta(minutes=5)) + self._create_message(chat, chat.livechat_operator_id, datetime.now()) + report = self.env["im_livechat.report.channel"].search([("channel_id", "=", chat.id)]) + self.assertEqual(report.time_to_answer, 0.0) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_support_page.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_support_page.py index fe08c73..a756cc4 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_support_page.py +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_im_livechat_support_page.py @@ -1,30 +1,25 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from unittest.mock import patch + import odoo -from odoo.tests import HttpCase +from odoo.addons.im_livechat.controllers.main import LivechatController +from odoo.addons.im_livechat.tests.common import TestGetOperatorCommon -@odoo.tests.tagged('-at_install', 'post_install') -class TestImLivechatSupportPage(HttpCase): +@odoo.tests.tagged("-at_install", "post_install") +class TestImLivechatSupportPage(TestGetOperatorCommon): def test_load_modules(self): - """Checks that all javascript modules load correctly on the livechat support page""" + operator = self._create_operator() + livechat_channel = self.env["im_livechat.channel"].create( + {"name": "Support Channel", "user_ids": [operator.id]} + ) + self.start_tour(f"/im_livechat/support/{livechat_channel.id}", "im_livechat.basic_tour") - # Give some time to the assets to load to prevent fetch - # interrupt errors then ensures all the assets are loaded. - check_js_modules = """ - setTimeout(() => { - const { missing, failed, unloaded } = odoo.__DEBUG__.jsModules; - if ([missing, failed, unloaded].some(arr => arr.length)) { - console.error("Couldn't load all JS modules.", JSON.stringify({ missing, failed, unloaded })); - } else { - console.log("test successful"); - } - Object.assign(console, { - log: () => {}, - error: () => {}, - warn: () => {}, - }); - }, 1000); - - """ - self.browser_js("/im_livechat/support/1", code=check_js_modules, ready="odoo.__DEBUG__.didLogInfo") + def test_load_modules_cors(self): + operator = self._create_operator() + livechat_channel = self.env["im_livechat.channel"].create( + {"name": "Support Channel", "user_ids": [operator.id]} + ) + with patch.object(LivechatController, "_is_cors_request", return_value=True): + self.start_tour(f"/im_livechat/support/{livechat_channel.id}", "im_livechat.basic_tour") diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_js.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_js.py new file mode 100644 index 0000000..6342755 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_js.py @@ -0,0 +1,17 @@ +import odoo +from odoo.addons.web.tests.test_js import unit_test_error_checker + + +@odoo.tests.tagged("post_install", "-at_install") +class ExternalTestSuite(odoo.tests.HttpCase): + def test_external_livechat(self): + # webclient external test suite + self.browser_js( + "/web/tests/livechat?headless&loglevel=2&preset=desktop", + "", + "", + login='admin', + timeout=1800, + success_signal="[HOOT] Test suite succeeded", + error_checker=unit_test_error_checker, + ) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_member_history.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_member_history.py new file mode 100644 index 0000000..d43b50b --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_member_history.py @@ -0,0 +1,154 @@ +from odoo.addons.im_livechat.tests import chatbot_common +from odoo.exceptions import ValidationError +from odoo.tests.common import tagged, new_test_user +from odoo.addons.im_livechat.tests.common import TestGetOperatorCommon + + +@tagged("post_install", "-at_install") +class TestLivechatMemberHistory(TestGetOperatorCommon, chatbot_common.ChatbotCase): + def test_get_session_create_history(self): + john = self._create_operator("fr_FR") + bob = self._create_operator("fr_FR") + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [bob.id], + }, + ) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", {"channel_id": livechat_channel.id} + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + self.assertEqual(len(channel.channel_member_ids.livechat_member_history_ids), 2) + self.assertEqual( + channel.channel_member_ids.livechat_member_history_ids.filtered( + lambda m: m.partner_id == bob.partner_id + ).livechat_member_type, + "agent", + ) + self.assertEqual( + channel.channel_member_ids.livechat_member_history_ids.filtered( + lambda m: m.partner_id != bob.partner_id + ).livechat_member_type, + "visitor", + ) + channel.add_members(partner_ids=john.partner_id.ids) + self.assertEqual(len(channel.channel_member_ids.livechat_member_history_ids), 3) + self.assertEqual( + channel.channel_member_ids.livechat_member_history_ids.filtered( + lambda m: m.partner_id == john.partner_id + ).livechat_member_type, + "agent", + ) + + def test_get_session_create_history_with_bot(self): + john = self._create_operator("fr_FR") + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "chatbot_script_id": self.chatbot_script.id, + "channel_id": self.livechat_channel.id, + }, + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + self.assertEqual(len(channel.channel_member_ids.livechat_member_history_ids), 2) + self.assertEqual( + channel.channel_member_ids.livechat_member_history_ids.filtered( + lambda m: m.partner_id == self.chatbot_script.operator_partner_id + ).livechat_member_type, + "bot", + ) + self.assertEqual( + channel.channel_member_ids.livechat_member_history_ids.filtered( + lambda m: m.partner_id != self.chatbot_script.operator_partner_id + ).livechat_member_type, + "visitor", + ) + channel.add_members(partner_ids=john.partner_id.ids) + self.assertEqual(len(channel.channel_member_ids.livechat_member_history_ids), 3) + self.assertEqual( + channel.channel_member_ids.livechat_member_history_ids.filtered( + lambda m: m.partner_id == john.partner_id + ).livechat_member_type, + "agent", + ) + + def test_marked_as_visitor_when_joining_after_log_in(self): + self.authenticate(None, None) + john = self._create_operator("fr_FR") + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": john.ids, + }, + ) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", {"channel_id": livechat_channel.id} + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + self.assertEqual(len(channel.channel_member_ids.livechat_member_history_ids), 2) + self.assertEqual( + channel.channel_member_ids.livechat_member_history_ids.filtered( + lambda m: m.partner_id == john.partner_id, + ).livechat_member_type, + "agent", + ) + guest_visitor_history = channel.channel_member_ids.livechat_member_history_ids.filtered( + lambda m: m.guest_id + ) + self.assertEqual(guest_visitor_history.livechat_member_type, "visitor") + visitor_user = new_test_user( + self.env, login="visitor_user", groups="im_livechat.im_livechat_group_manager" + ) + self.authenticate("visitor_user", "visitor_user") + data = self.make_jsonrpc_request( + "/discuss/channel/notify_typing", + {"channel_id": channel.id, "is_typing": True}, + cookies={ + guest_visitor_history.guest_id._cookie_name: guest_visitor_history.guest_id._format_auth_cookie() + }, + ) + self.assertEqual( + channel.channel_member_ids.livechat_member_history_ids.filtered( + lambda m: m.partner_id == visitor_user.partner_id, + ).livechat_member_type, + "visitor", + ) + + def test_can_only_create_history_for_livechats(self): + john = self._create_operator("fr_FR") + channel = self.env["discuss.channel"]._create_channel(name="General", group_id=None) + member = channel.add_members(partner_ids=john.partner_id.ids) + with self.assertRaises(ValidationError): + self.env["im_livechat.channel.member.history"].create({"member_id": member.id}).channel_id + + def test_update_history_on_second_join(self): + john = self._create_operator("fr_FR") + livechat_channel = self.env["im_livechat.channel"].create( + {"name": "Livechat Channel", "user_ids": [john.id]}, + ) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"channel_id": livechat_channel.id}, + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + og_history = channel.channel_member_ids.livechat_member_history_ids.filtered( + lambda m: m.partner_id == john.partner_id + ) + john_member = channel.channel_member_ids.filtered(lambda m: m.partner_id == john.partner_id) + self.assertEqual(og_history.livechat_member_type, "agent") + self.assertEqual(og_history.member_id, john_member) + channel.with_user(john).action_unfollow() + john_history = channel.channel_member_ids.livechat_member_history_ids.filtered( + lambda m: m.partner_id == john.partner_id + ) + self.assertFalse(john_history.member_id) + self.assertNotIn(john.partner_id, channel.channel_member_ids.partner_id) + channel._add_members(users=john) + self.assertIn(john.partner_id, channel.channel_member_ids.partner_id) + john_member = channel.channel_member_ids.filtered(lambda m: m.partner_id == john.partner_id) + john_history = channel.channel_member_ids.livechat_member_history_ids.filtered( + lambda m: m.partner_id == john.partner_id + ) + self.assertEqual(og_history, john_history) + self.assertEqual(john_member, john_history.member_id) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_message.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_message.py index 01b64eb..8f8e53d 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_message.py +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_message.py @@ -1,84 +1,139 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import Command +from freezegun import freeze_time +from markupsafe import Markup + +from odoo import Command, fields +from odoo.exceptions import AccessError from odoo.tests.common import users, tagged +from odoo.addons.mail.tests.common import MailCommon +from odoo.addons.mail.tools.discuss import Store from odoo.addons.im_livechat.tests.chatbot_common import ChatbotCase @tagged('post_install', '-at_install') -class TestImLivechatMessage(ChatbotCase): +class TestImLivechatMessage(ChatbotCase, MailCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._create_portal_user() + def setUp(self): super().setUp() + self.password = 'Pl1bhD@2!kXZ' self.users = self.env['res.users'].create([ { 'email': 'e.e@example.com', - 'groups_id': [Command.link(self.env.ref('base.group_user').id)], + 'group_ids': [Command.link(self.env.ref('base.group_user').id)], 'login': 'emp', + 'password': self.password, 'name': 'Ernest Employee', 'notification_type': 'inbox', 'odoobot_state': 'disabled', 'signature': '--\nErnest', }, - {'name': 'test1', 'login': 'test1', 'email': 'test1@example.com'}, + { + "email": "test1@example.com", + "login": "test1", + "name": "test1", + "password": self.password, + }, ]) + settings = self.env["res.users.settings"]._find_or_create_for_user(self.users[1]) + settings.livechat_username = "chuck" + self.maxDiff = None + + def test_update_username(self): + user = self.env['res.users'].create({ + 'name': 'User', + 'login': 'User', + 'password': self.password, + 'email': 'user@example.com', + 'livechat_username': 'edit me' + }) + with self.assertRaises(AccessError): + self.env['res.users'].with_user(user).check_access('write') + user.with_user(user).livechat_username = 'New username' + self.assertEqual(user.livechat_username, 'New username') - @users('emp') def test_chatbot_message_format(self): - user = self.env.user - channel_info = self.livechat_channel.with_user(user)._open_livechat_mail_channel( - anonymous_name='Test Chatbot', - previous_operator_id=self.chatbot_script.operator_partner_id.id, - chatbot_script=self.chatbot_script, - user_id=user.id + self.authenticate(self.users[0].login, self.password) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "channel_id": self.livechat_channel.id, + "chatbot_script_id": self.chatbot_script.id, + "persisted": True, + }, ) - mail_channel = self.env['mail.channel'].browse(channel_info['id']) + discuss_channel = self.env['discuss.channel'].browse(data["channel_id"]) self._post_answer_and_trigger_next_step( - mail_channel, + discuss_channel, self.step_dispatch_buy_software.name, chatbot_script_answer=self.step_dispatch_buy_software ) - chatbot_message = mail_channel.chatbot_message_ids.mail_message_id[-1:] - self.assertEqual(chatbot_message.message_format(), [{ - 'id': chatbot_message.id, - 'body': '

Can you give us your email please?

', - 'date': chatbot_message.date, - 'message_type': 'comment', - 'subtype_id': (self.env.ref('mail.mt_comment').id, 'Discussions'), - 'subject': False, - 'model': 'mail.channel', - 'res_id': mail_channel.id, - 'record_name': 'Testing Bot', - 'starred_partner_ids': [], - 'author': { - 'id': self.chatbot_script.operator_partner_id.id, - 'name': 'Testing Bot' - }, - 'guestAuthor': [('clear',)], - 'notifications': [], - 'attachment_ids': [], - 'trackingValues': [], - 'linkPreviews': [], - 'messageReactionGroups': [], - 'chatbot_script_step_id': self.step_email.id, - 'needaction_partner_ids': [], - 'history_partner_ids': [], - 'is_note': False, - 'is_discussion': True, - 'subtype_description': False, - 'is_notification': False, - 'recipients': [], - 'module_icon': '/mail/static/description/icon.png', - 'sms_ids': [] - }]) + chatbot_message = discuss_channel.chatbot_message_ids.mail_message_id[:1] + self.assertEqual( + Store().add(chatbot_message).get_result()["mail.message"], + [ + { + "attachment_ids": [], + "author_guest_id": False, + "author_id": self.chatbot_script.operator_partner_id.id, + "body": ["markup", "

Can you give us your email please?

"], + "chatbotStep": { + "message": chatbot_message.id, + "scriptStep": self.step_email.id, + }, + "create_date": fields.Datetime.to_string(chatbot_message.create_date), + "date": fields.Datetime.to_string(chatbot_message.date), + "default_subject": "Testing Bot", + "email_from": False, + "id": chatbot_message.id, + "incoming_email_cc": False, + "incoming_email_to": False, + "message_link_preview_ids": [], + "message_type": "comment", + "model": "discuss.channel", + "needaction": False, + "notification_ids": [], + "parent_id": False, + "partner_ids": [], + "pinned_at": False, + "rating_id": False, + "reactions": [], + "record_name": "Testing Bot", + "res_id": discuss_channel.id, + "scheduledDatetime": False, + "starred": False, + "thread": { + "id": discuss_channel.id, + "model": "discuss.channel", + }, + "subject": False, + "subtype_id": self.env.ref("mail.mt_comment").id, + "trackingValues": [], + "write_date": fields.Datetime.to_string(chatbot_message.write_date), + } + ], + ) @users('emp') - def test_message_format(self): + def test_message_to_store(self): im_livechat_channel = self.env['im_livechat.channel'].sudo().create({'name': 'support', 'user_ids': [Command.link(self.users[0].id)]}) - self.users[0].im_status = 'online' # make available for livechat (ignore leave) - channel_livechat_1 = self.env['mail.channel'].browse(im_livechat_channel._open_livechat_mail_channel(anonymous_name='anon 1', previous_operator_id=self.users[0].partner_id.id, user_id=self.users[1].id, country_id=self.env.ref('base.in').id)['id']) + self.env["mail.presence"]._update_presence(self.users[0]) + self.authenticate(self.users[1].login, self.password) + channel_livechat_1 = self.env["discuss.channel"].browse( + self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "previous_operator_id": self.users[0].partner_id.id, + "channel_id": im_livechat_channel.id, + }, + )["channel_id"] + ) record_rating = self.env['rating.rating'].create({ - 'res_model_id': self.env['ir.model']._get('mail.channel').id, + 'res_model_id': self.env['ir.model']._get('discuss.channel').id, 'res_id': channel_livechat_1.id, 'parent_res_model_id': self.env['ir.model']._get('im_livechat.channel').id, 'parent_res_id': im_livechat_channel.id, @@ -89,43 +144,212 @@ class TestImLivechatMessage(ChatbotCase): }) message = channel_livechat_1.message_post( author_id=record_rating.partner_id.id, - body=":%s/5%s" + body=Markup(":%s/5%s") % (record_rating.rating_image_url, record_rating.rating, record_rating.feedback), rating_id=record_rating.id, ) - self.assertEqual(message.message_format(), [{ - 'attachment_ids': [], - 'author': { - 'id': self.users[1].partner_id.id, - 'name': "test1", + self.assertEqual( + Store().add(message).get_result(), + { + "mail.message": self._filter_messages_fields( + { + "attachment_ids": [], + "author_guest_id": False, + "author_id": self.users[1].partner_id.id, + "body": ["markup", message.body], + "date": fields.Datetime.to_string(message.date), + "write_date": fields.Datetime.to_string(message.write_date), + "create_date": fields.Datetime.to_string(message.create_date), + "id": message.id, + "default_subject": "test1 Ernest Employee", + "email_from": '"test1" ', + "incoming_email_cc": False, + "incoming_email_to": False, + "message_link_preview_ids": [], + "message_type": "notification", + "reactions": [], + "model": "discuss.channel", + "needaction": False, + "notification_ids": [], + "thread": {"id": channel_livechat_1.id, "model": "discuss.channel"}, + "parent_id": False, + "partner_ids": [], + "pinned_at": False, + "rating_id": record_rating.id, + "record_name": "test1 Ernest Employee", + "res_id": channel_livechat_1.id, + "scheduledDatetime": False, + "starred": False, + "subject": False, + "subtype_id": self.env.ref("mail.mt_note").id, + "trackingValues": [], + }, + ), + "mail.message.subtype": [ + {"description": False, "id": self.env.ref("mail.mt_note").id} + ], + "mail.thread": self._filter_threads_fields( + { + "display_name": "test1 Ernest Employee", + "id": channel_livechat_1.id, + "model": "discuss.channel", + "module_icon": "/mail/static/description/icon.png", + "rating_avg": 5.0, + "rating_count": 1, + }, + ), + "rating.rating": [ + { + "id": record_rating.id, + "rating": 5.0, + "rating_image_url": record_rating.rating_image_url, + "rating_text": "top", + }, + ], + "res.partner": self._filter_partners_fields( + { + "avatar_128_access_token": self.users[ + 1 + ].partner_id._get_avatar_128_access_token(), + "id": self.users[1].partner_id.id, + "is_company": False, + "main_user_id": self.users[1].id, + "user_livechat_username": "chuck", + "write_date": fields.Datetime.to_string(self.users[1].write_date), + }, + ), + "res.users": self._filter_users_fields( + { + "id": self.users[1].id, + "partner_id": self.users[1].partner_id.id, + "share": False, + }, + ), }, - 'body': message.body, - 'date': message.date, - 'guestAuthor': [('clear',)], - 'history_partner_ids': [], - 'id': message.id, - 'is_discussion': False, - 'is_note': True, - 'is_notification': False, - 'linkPreviews': [], - 'message_type': 'notification', - 'messageReactionGroups': [], - 'model': 'mail.channel', - 'module_icon': '/mail/static/description/icon.png', - 'needaction_partner_ids': [], - 'notifications': [], - 'rating': { - 'id': record_rating.id, - 'ratingImageUrl': record_rating.rating_image_url, - 'ratingText': record_rating.rating_text, - }, - 'recipients': [], - 'record_name': "test1 Ernest Employee", - 'res_id': channel_livechat_1.id, - 'sms_ids': [], - 'starred_partner_ids': [], - 'subject': False, - 'subtype_description': False, - 'subtype_id': (self.env.ref('mail.mt_note').id, 'Note'), - 'trackingValues': [], - }]) + ) + + @users("portal_test") + @freeze_time("2020-03-22 10:42:06") + def test_feedback_message(self): + """Test posting a feedback message as a portal user, and ensure the proper bus + notifications are sent.""" + livechat_channel_vals = {"name": "support", "user_ids": [Command.link(self.users[0].id)]} + im_livechat_channel = self.env["im_livechat.channel"].sudo().create(livechat_channel_vals) + self.env["mail.presence"]._update_presence(self.users[0]) + self.authenticate(self.env.user.login, self.env.user.login) + channel = self.env["discuss.channel"].browse( + self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "previous_operator_id": self.users[0].partner_id.id, + "channel_id": im_livechat_channel.id, + }, + )["channel_id"] + ) + + def _get_feedback_bus(): + message = self.env["mail.message"].sudo().search([], order="id desc", limit=1) + rating = self.env["rating.rating"].sudo().search([], order="id desc", limit=1) + return ( + [ + # unread counter/new message separator (not asserted below) + (self.env.cr.dbname, "res.partner", self.env.user.partner_id.id), + # new_message + (self.env.cr.dbname, "discuss.channel", channel.id), + ], + [ + { + "type": "discuss.channel/new_message", + "payload": { + "data": { + "mail.message": self._filter_messages_fields( + { + "attachment_ids": [], + "author_guest_id": False, + "author_id": self.env.user.partner_id.id, + "body": [ + "markup", + '
Rating: rating
\nGood service
', + ], + "create_date": fields.Datetime.to_string( + message.create_date + ), + "date": fields.Datetime.to_string(message.date), + "default_subject": "Chell Gladys Ernest Employee", + "id": message.id, + "incoming_email_cc": False, + "incoming_email_to": False, + "message_link_preview_ids": [], + "message_type": "notification", + "model": "discuss.channel", + "parent_id": False, + "partner_ids": [], + "pinned_at": False, + "rating_id": rating.id, + "reactions": [], + "record_name": "Chell Gladys Ernest Employee", + "res_id": channel.id, + "scheduledDatetime": False, + "subject": False, + "subtype_id": self.env.ref("mail.mt_comment").id, + "thread": {"id": channel.id, "model": "discuss.channel"}, + "write_date": fields.Datetime.to_string(message.write_date), + }, + ), + "mail.message.subtype": [ + {"description": False, "id": self.env.ref("mail.mt_comment").id} + ], + "mail.thread": self._filter_threads_fields( + { + "display_name": "Chell Gladys Ernest Employee", + "id": channel.id, + "model": "discuss.channel", + "module_icon": "/mail/static/description/icon.png", + "rating_avg": 5.0, + "rating_count": 1, + }, + ), + "rating.rating": [ + { + "id": rating.id, + "rating": 5.0, + "rating_image_url": rating.rating_image_url, + "rating_text": "top", + }, + ], + "res.partner": self._filter_partners_fields( + { + "avatar_128_access_token": self.env.user.partner_id._get_avatar_128_access_token(), + "id": self.env.user.partner_id.id, + "is_company": False, + "main_user_id": self.env.user.id, + "name": "Chell Gladys", + "user_livechat_username": False, + "write_date": fields.Datetime.to_string( + self.env.user.write_date + ), + }, + ), + "res.users": self._filter_users_fields( + { + "id": self.env.user.id, + "partner_id": self.env.user.partner_id.id, + "share": True, + }, + ), + }, + "id": channel.id, + }, + }, + ], + ) + + with self.assertBus(get_params=_get_feedback_bus): + self.make_jsonrpc_request( + "/im_livechat/feedback", + { + "channel_id": channel.id, + "rate": 5, + "reason": "Good service", + }, + ) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_res_users.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_res_users.py new file mode 100644 index 0000000..4578db5 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_res_users.py @@ -0,0 +1,20 @@ +from odoo.tests import new_test_user +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestLiveChatResUsers(TransactionCase): + + def test_livechat_create_res_users(self): + access_user = new_test_user( + self.env, + login="admin_access", + name="admin_access", + groups="base.group_erp_manager,base.group_partner_manager", + ) + access_user.with_user(access_user.id).create({ + "login": "test_can_be_created", + "name": "test_can_be_created", + "livechat_username": False, + "livechat_lang_ids": [], + }) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_session_views.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_session_views.py new file mode 100644 index 0000000..10a1e46 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_session_views.py @@ -0,0 +1,156 @@ +from odoo import Command +from odoo.tests import new_test_user +from odoo.addons.im_livechat.tests.common import TestImLivechatCommon +from odoo.tests.common import users, tagged + + +@tagged("-at_install", "post_install") +class TestImLivechatSessionViews(TestImLivechatCommon): + def test_session_history_navigation_back_and_forth(self): + operator = new_test_user( + self.env, + login="operator", + groups="base.group_user,im_livechat.im_livechat_group_manager", + ) + self.env["mail.presence"]._update_presence(operator) + self.livechat_channel.user_ids |= operator + self.authenticate(None, None) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "channel_id": self.livechat_channel.id, + "previous_operator_id": operator.partner_id.id, + }, + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + channel.with_user(operator).message_post(body="Hello, how can I help you?") + self._reset_bus() + action = self.env.ref("im_livechat.discuss_channel_action_from_livechat_channel") + self.start_tour( + f"/odoo/livechat/{self.livechat_channel.id}/action-{action.id}", + "im_livechat_history_back_and_forth_tour", + login="operator", + ) + + @users("admin") + def test_form_view_embed_thread(self): + operator = new_test_user( + self.env, + login="operator", + groups="base.group_user,im_livechat.im_livechat_group_manager", + ) + [user_1, user_2] = self.env["res.partner"].create([{"name": "test 1"}, {"name": "test 2"}]) + [channel1, channel2] = self.env["discuss.channel"].create( + [ + { + "name": "test 1", + "channel_type": "livechat", + "livechat_channel_id": self.livechat_channel.id, + "livechat_operator_id": operator.partner_id.id, + "channel_member_ids": [Command.create({"partner_id": user_1.id})], + }, + { + "name": "test 2", + "channel_type": "livechat", + "livechat_channel_id": self.livechat_channel.id, + "livechat_operator_id": operator.partner_id.id, + "channel_member_ids": [Command.create({"partner_id": user_2.id})], + }, + ] + ) + channel1.message_post( + body="Test Channel 1 Msg", message_type="comment", subtype_xmlid="mail.mt_comment" + ) + channel2.message_post( + body="Test Channel 2 Msg", message_type="comment", subtype_xmlid="mail.mt_comment" + ) + action = self.env.ref("im_livechat.discuss_channel_action_from_livechat_channel") + self.start_tour( + f"/odoo/livechat/{self.livechat_channel.id}/action-{action.id}", + "im_livechat_session_history_open", + login="operator", + ) + + def test_partner_display_name(self): + user = new_test_user(self.env, login="agent", name="john") + company = self.env["res.partner"].create({"name": "TestCompany", "is_company": True}) + user.partner_id.parent_id = company.id + self.assertEqual( + user.with_context(im_livechat_hide_partner_company=True).partner_id.display_name, + "john", + ) + self.assertEqual(user.partner_id.display_name, "TestCompany, john") + + +class TestImLivechatLookingForHelpViews(TestImLivechatSessionViews): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.bob = new_test_user( + cls.env, + login="bob_looking_for_help", + groups="base.group_user,im_livechat.im_livechat_group_user", + ) + cls.livechat_channel.user_ids |= cls.bob + cls.looking_for_help_action = cls.env.ref( + "im_livechat.discuss_channel_looking_for_help_action" + ) + + def start_needhelp_session(self, guest_name=None): + self.authenticate(None, None) + cookies = {} + if guest_name: + guest = self.env["mail.guest"].create({"name": guest_name}) + cookies = {guest._cookie_name: guest._format_auth_cookie()} + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "channel_id": self.livechat_channel.id, + "previous_operator_id": self.bob.partner_id.id, + }, + cookies=cookies, + ) + chat = self.env["discuss.channel"].browse(data["channel_id"]) + chat.livechat_status = "need_help" + return chat + + def test_looking_for_help_list_real_time_update(self): + self.start_needhelp_session() + self.start_tour( + f"/odoo/action-{self.looking_for_help_action.id}", + "im_livechat.looking_for_help_list_real_time_update_tour", + login="bob_looking_for_help", + ) + + def test_looking_for_help_kanban_real_time_update(self): + self.start_needhelp_session() + self.start_tour( + f"/odoo/action-{self.looking_for_help_action.id}?view_type=kanban", + "im_livechat.looking_for_help_kanban_real_time_update_tour", + login="bob_looking_for_help", + ) + + def test_looking_for_help_tags_real_time_update(self): + self.start_needhelp_session() + self.start_tour( + f"/odoo/action-{self.looking_for_help_action.id}", + "im_livechat.looking_for_help_tags_real_time_update_tour", + login="bob_looking_for_help", + ) + + def test_looking_for_help_discuss_category(self): + self.env["discuss.channel"].search([("livechat_status", "=", "need_help")]).unlink() + agent = new_test_user(self.env, "agent", groups="im_livechat.im_livechat_group_user") + accounting_expertise, sales_expertise = self.env["im_livechat.expertise"].create( + [{"name": "Accounting"}, {"name": "Sales"}], + ) + agent.livechat_expertise_ids = sales_expertise + accounting_chat = self.start_needhelp_session(guest_name="Visitor Accounting") + accounting_chat.livechat_expertise_ids = accounting_expertise + sales_chat = self.start_needhelp_session(guest_name="Visitor Sales") + sales_chat.livechat_expertise_ids = sales_expertise + self._reset_bus() + self.start_tour( + "/odoo/discuss", "im_livechat.looking_for_help_discuss_category_tour", login="agent" + ) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_transcript.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_transcript.py new file mode 100644 index 0000000..e3afc6d --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_transcript.py @@ -0,0 +1,41 @@ +from odoo.tests.common import JsonRpcException, tagged +from odoo.tools import mute_logger +from odoo.addons.base.tests.common import HttpCaseWithUserDemo, HttpCaseWithUserPortal +from odoo.addons.im_livechat.tests.common import TestImLivechatCommon + + +@tagged("-at_install", "post_install") +class TestImLivechatTranscript(TestImLivechatCommon, HttpCaseWithUserDemo, HttpCaseWithUserPortal): + def test_download_transcript(self): + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"channel_id": self.livechat_channel.id}, + ) + res = self.url_open(f"/im_livechat/download_transcript/{data['channel_id']}") + self.assertEqual(res.status_code, 200) + self.assertEqual(res.headers["Content-Type"], "application/pdf") + + def test_download_transcript_non_member(self): + self.authenticate("demo", "demo") + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"channel_id": self.livechat_channel.id}, + ) + chat = self.env["discuss.channel"].browse(data["channel_id"]) + self.authenticate(None, None) + self.assertFalse(chat.is_member) + with mute_logger("odoo.http"): + res = self.url_open(f"/im_livechat/download_transcript/{data['channel_id']}") + self.assertEqual(res.status_code, 404) + + def test_email_transcript_portal_user(self): + self.authenticate(self.user_portal.login, self.user_portal.login) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + {"channel_id": self.livechat_channel.id}, + ) + with self.assertRaises(JsonRpcException, msg="werkzeug.exceptions.NotFound"): + self.make_jsonrpc_request( + "/im_livechat/email_livechat_transcript", + {"channel_id": data["channel_id"], "email": self.partner_portal.email}, + ) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_upload_attachment.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_upload_attachment.py new file mode 100644 index 0000000..34807fd --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_upload_attachment.py @@ -0,0 +1,37 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.tests.common import tagged, HttpCase +from odoo.tools import mute_logger, file_open + + +@tagged("post_install", "-at_install") +class TestUploadAttachment(HttpCase): + def test_visitor_cannot_upload_on_closed_livechat(self): + self.authenticate(None, None) + operator = self.env["res.users"].create({"name": "Operator", "login": "operator"}) + self.env["mail.presence"]._update_presence(operator) + livechat_channel = self.env["im_livechat.channel"].create( + {"name": "Test Livechat Channel", "user_ids": [operator.id]} + ) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "channel_id": livechat_channel.id, + "persisted": True, + }, + ) + self.make_jsonrpc_request( + "/im_livechat/visitor_leave_session", {"channel_id": data["channel_id"]} + ) + with mute_logger("odoo.http"), file_open("addons/web/__init__.py") as file: + response = self.url_open( + "/mail/attachment/upload", + { + "csrf_token": http.Request.csrf_token(self), + "thread_id": data["channel_id"], + "thread_model": "discuss.channel", + }, + files={"ufile": file}, + ) + self.assertEqual(response.status_code, 403) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_user_livechat_username.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_user_livechat_username.py new file mode 100644 index 0000000..335d490 --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tests/test_user_livechat_username.py @@ -0,0 +1,81 @@ +from odoo import fields +from odoo.tests.common import tagged +from odoo.addons.im_livechat.tests.common import TestGetOperatorCommon + + +@tagged("post_install", "-at_install") +class TestUserLivechatUsername(TestGetOperatorCommon): + def test_user_livechat_username_channel_invite_notification(self): + john = self._create_operator("fr_FR") + bob = self._create_operator("fr_FR") + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "Livechat Channel", + "user_ids": [bob.id], + } + ) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", {"channel_id": livechat_channel.id, "persisted": True} + ) + john.partner_id.user_livechat_username = "ELOPERADOR" + channel = self.env["discuss.channel"].browse(data["channel_id"]) + channel._add_members(users=john) + self.assertEqual( + channel.message_ids[-1].body, + f'
invited @ELOPERADOR to the channel
', + ) + + def test_user_livechat_username_reactions(self): + john = self._create_operator("fr_FR") + john.livechat_username = "ELOPERADOR" + livechat_channel = self.env["im_livechat.channel"].create( + {"name": "Livechat Channel", "user_ids": [john.id]} + ) + data = self.make_jsonrpc_request( + "/im_livechat/get_session", {"channel_id": livechat_channel.id} + ) + channel = self.env["discuss.channel"].browse(data["channel_id"]) + message = channel.message_post(body="Hello, How can I help you?") + session = self.authenticate(john.login, john.login) + data = self.make_jsonrpc_request( + "/mail/message/reaction", + {"action": "add", "content": "👍", "message_id": message.id}, + cookies={"session_id": session.sid}, + ) + self.assertEqual( + data["res.partner"][0], + { + "avatar_128_access_token": john.partner_id._get_avatar_128_access_token(), + "id": john.partner_id.id, + "user_livechat_username": "ELOPERADOR", + "write_date": fields.Datetime.to_string(john.partner_id.write_date), + }, + ) + + def test_user_livechat_username_in_store(self): + john = self._create_operator("fr_FR") + operator = self._create_operator("en_US") + john.livechat_username = "ELOPERADOR" + operator.group_ids = [ + fields.Command.link(self.env.ref("im_livechat.im_livechat_group_user").id), + ] + livechat_channel = self.env["im_livechat.channel"].create( + { + "name": "The channel", + "user_ids": [fields.Command.link(john.id), fields.Command.link(operator.id)], + } + ) + channel = self.env["discuss.channel"].with_user(operator).create( + { + "name": "Livechat session", + "channel_type": "livechat", + "livechat_operator_id": operator.partner_id.id, + "livechat_channel_id": livechat_channel.id, + } + ) + data = operator.partner_id.with_user(operator).search_for_channel_invite("fr_FR", channel.id)["store_data"] + john_data = next(filter(lambda partner: partner["id"] == john.partner_id.id, data["res.partner"])) + self.assertEqual( + john_data["user_livechat_username"], + "ELOPERADOR", + ) diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tools/__init__.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tools/__init__.py new file mode 100644 index 0000000..1d0b8cd --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tools/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import misc diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/tools/misc.py b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tools/misc.py new file mode 100644 index 0000000..1373bec --- /dev/null +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/tools/misc.py @@ -0,0 +1,30 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.http import request +from werkzeug.exceptions import NotFound + +def downgrade_to_public_user(): + """Replace the request user by the public one. All the cookies are removed + in order to ensure that the no user-specific data is kept in the request.""" + public_user = request.env.ref("base.public_user") + request.session.uid = None + request.update_env(user=public_user) + request.cookies = {} + + +def force_guest_env(guest_token, raise_if_not_found=True): + """Retrieve the guest from the given token and add it to the context. + The request user is then replaced by the public one. + + :param str guest_token: + :param bool raise_if_not_found: whether to raise if the guest cannot be + found from the token + :raise NotFound: if the guest cannot be found from the token and the + ``raise_if_not_found`` parameter is set to ``True`` + """ + downgrade_to_public_user() + guest = request.env["mail.guest"]._get_guest_from_token(guest_token) + if guest: + request.update_context(guest=guest) + elif raise_if_not_found: + raise NotFound() diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/views/chatbot_script_answer_views.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/views/chatbot_script_answer_views.xml index 31caf10..bf64e9f 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/views/chatbot_script_answer_views.xml +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/views/chatbot_script_answer_views.xml @@ -17,15 +17,15 @@ - chatbot.script.answer.view.tree + chatbot.script.answer.view.list chatbot.script.answer - + - + diff --git a/odoo-bringout-oca-ocb-im_livechat/im_livechat/views/chatbot_script_step_views.xml b/odoo-bringout-oca-ocb-im_livechat/im_livechat/views/chatbot_script_step_views.xml index ba2471e..2379f33 100644 --- a/odoo-bringout-oca-ocb-im_livechat/im_livechat/views/chatbot_script_step_views.xml +++ b/odoo-bringout-oca-ocb-im_livechat/im_livechat/views/chatbot_script_step_views.xml @@ -7,36 +7,37 @@
-