From d1bd94c55c944ce4baf7c20c613484fa89b305e7 Mon Sep 17 00:00:00 2001 From: david Date: Wed, 10 Aug 2022 16:19:47 +0200 Subject: [PATCH 01/21] [13.0][ADD] announcement: New module TT38174 --- announcement/README.rst | 121 +++++ announcement/__init__.py | 2 + announcement/__manifest__.py | 25 + announcement/i18n/announcement.pot | 304 +++++++++++ announcement/i18n/es.po | 308 ++++++++++++ announcement/models/__init__.py | 3 + announcement/models/announcement.py | 128 +++++ announcement/models/ir_http.py | 11 + announcement/models/res_users.py | 62 +++ announcement/readme/CONFIGURE.rst | 21 + announcement/readme/CONTRIBUTORS.rst | 4 + announcement/readme/DESCRIPTION.rst | 2 + announcement/readme/ROADMAP.rst | 3 + announcement/readme/USAGE.rst | 8 + .../security/announcement_security.xml | 50 ++ announcement/security/ir.model.access.csv | 4 + announcement/static/description/icon.png | Bin 0 -> 4887 bytes announcement/static/description/index.html | 471 ++++++++++++++++++ announcement/static/src/js/announcement.js | 222 +++++++++ .../static/src/js/announcement_dialog.js | 52 ++ announcement/static/src/xml/announcement.xml | 53 ++ .../static/src/xml/announcement_dialog.xml | 35 ++ announcement/templates/assets_backend.xml | 15 + announcement/views/announcement_views.xml | 195 ++++++++ announcement/wizards/__init__.py | 1 + .../wizards/read_announcement_wizard.py | 14 + .../wizards/read_announcement_wizard.xml | 14 + 27 files changed, 2128 insertions(+) create mode 100644 announcement/README.rst create mode 100644 announcement/__init__.py create mode 100644 announcement/__manifest__.py create mode 100644 announcement/i18n/announcement.pot create mode 100644 announcement/i18n/es.po create mode 100644 announcement/models/__init__.py create mode 100644 announcement/models/announcement.py create mode 100644 announcement/models/ir_http.py create mode 100644 announcement/models/res_users.py create mode 100644 announcement/readme/CONFIGURE.rst create mode 100644 announcement/readme/CONTRIBUTORS.rst create mode 100644 announcement/readme/DESCRIPTION.rst create mode 100644 announcement/readme/ROADMAP.rst create mode 100644 announcement/readme/USAGE.rst create mode 100644 announcement/security/announcement_security.xml create mode 100644 announcement/security/ir.model.access.csv create mode 100644 announcement/static/description/icon.png create mode 100644 announcement/static/description/index.html create mode 100644 announcement/static/src/js/announcement.js create mode 100644 announcement/static/src/js/announcement_dialog.js create mode 100644 announcement/static/src/xml/announcement.xml create mode 100644 announcement/static/src/xml/announcement_dialog.xml create mode 100644 announcement/templates/assets_backend.xml create mode 100644 announcement/views/announcement_views.xml create mode 100644 announcement/wizards/__init__.py create mode 100644 announcement/wizards/read_announcement_wizard.py create mode 100644 announcement/wizards/read_announcement_wizard.xml diff --git a/announcement/README.rst b/announcement/README.rst new file mode 100644 index 0000000000..8a1b3cfad5 --- /dev/null +++ b/announcement/README.rst @@ -0,0 +1,121 @@ +============ +Announcement +============ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--ux-lightgray.png?logo=github + :target: https://github.com/OCA/server-ux/tree/13.0/announcement + :alt: OCA/server-ux +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-ux-13-0/server-ux-13-0-announcement + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/250/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds popup announcements in the backend for targeted internal users. Those +announcements can contain rich format and a user read log is kept for everyone. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To create new announcements a user should be in the *Announcements Managers* group. +When your user has such permissions, this is the way to create an announcement: + +#. Go to *Discuss > Announcements* +#. Create a new one and define a title. This title will be shown in the announcement + header. +#. Define the announcement scope: + + - Specific users: manually select which users will see the announcement. + - User groups: users from the selected groups will be the ones to see the + announcement. +#. Define the announcement body. You can use rich formatting and event paste your + own html (editor in debug mode). +#. By default, the announcement will be archived. This is to prevent the announcement + to show up before time. +#. Once the announcement is ready, unarchive it going to the *Actions* menu an choosing + the *Unarchive* option. +#. Optionally you can set an announcement date to schedule the announcement. The + announcement won't show up until that date. +#. If the announcement doesn't make sense once a date is passed, you can set a due date. + From that date, the announcement won't be shown to anyone. + +Usage +===== + +When a user in the scope of active announcements logs in, those will popup. The user +has to mark them as read to continue working. If the announcement is set during the +user session, the announcement will be eventually prompted in the top bar on the right +part. The user click on the unread announcements icon (a speaker) and the announcements +will popup for the user to check them. + +Users can go *Discuss > Announcements* to check current and past announcements. +Announcement managers can also track which users have read the announcement. + +Known issues / Roadmap +====================== + +* It could be integrated in Discuss app to review past announcements. +* Log other information like geolocation, IP, browser agent, etc when marking + announcement as read. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `__: + + * Pedro M. Baeza + * David Vidal + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/server-ux `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/announcement/__init__.py b/announcement/__init__.py new file mode 100644 index 0000000000..aee8895e7a --- /dev/null +++ b/announcement/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/announcement/__manifest__.py b/announcement/__manifest__.py new file mode 100644 index 0000000000..354785ecba --- /dev/null +++ b/announcement/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2022 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Announcement", + "version": "13.0.1.0.0", + "summary": "Notify internal users about relevant organization stuff", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Server UX", + "website": "https://github.com/OCA/server-ux", + "depends": ["mail"], + "data": [ + "security/announcement_security.xml", + "security/ir.model.access.csv", + "views/announcement_views.xml", + "wizards/read_announcement_wizard.xml", + "templates/assets_backend.xml", + ], + "qweb": [ + "static/src/xml/announcement_dialog.xml", + "static/src/xml/announcement.xml", + ], + "installable": True, +} diff --git a/announcement/i18n/announcement.pot b/announcement/i18n/announcement.pot new file mode 100644 index 0000000000..e9df27591f --- /dev/null +++ b/announcement/i18n/announcement.pot @@ -0,0 +1,304 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * announcement +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_management_view_form +msgid "Read(s)" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__active +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_search +msgid "Active" +msgstr "" + +#. module: announcement +#. openerp-web +#: code:addons/announcement/static/src/xml/announcement.xml:0 +#, python-format +msgid "Activities" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__allowed_user_ids +msgid "Allowed User" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__allowed_users_count +msgid "Allowed Users Count" +msgstr "" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_form +msgid "Announce at" +msgstr "" + +#. module: announcement +#. openerp-web +#: code:addons/announcement/static/src/xml/announcement.xml:0 +#: model:ir.model.fields,field_description:announcement.field_announcement_log__announcement_id +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__announcement_id +#: model:ir.module.category,name:announcement.category_announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_form +#, python-format +msgid "Announcement" +msgstr "" + +#. module: announcement +#: model:ir.actions.act_window,name:announcement.action_announcement_log +msgid "Announcement Log" +msgstr "" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_log_view_tree +#: model_terms:ir.ui.view,arch_db:announcement.read_announcement_wizard_view_tree +msgid "Announcement Logs" +msgstr "" + +#. module: announcement +#: model:res.groups,name:announcement.announcemenent_manager +msgid "Announcement Manager" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__announcement_type +msgid "Announcement Type" +msgstr "" + +#. module: announcement +#. openerp-web +#: code:addons/announcement/static/src/xml/announcement.xml:0 +#: model:ir.actions.act_window,name:announcement.announcement_action +#: model:ir.ui.menu,name:announcement.menu_announcement_management +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_tree +#, python-format +msgid "Announcements" +msgstr "" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_search +msgid "Archived" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__content +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_form +msgid "Content" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__create_uid +#: model:ir.model.fields,field_description:announcement.field_announcement_log__create_uid +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__create_uid +msgid "Created by" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__create_date +#: model:ir.model.fields,field_description:announcement.field_announcement_log__create_date +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__create_date +msgid "Created on" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__display_name +#: model:ir.model.fields,field_description:announcement.field_announcement_log__display_name +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__display_name +msgid "Display Name" +msgstr "" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_management_view_form +msgid "Groups" +msgstr "" + +#. module: announcement +#: model:ir.model,name:announcement.model_ir_http +msgid "HTTP Routing" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__id +#: model:ir.model.fields,field_description:announcement.field_announcement_log__id +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__id +msgid "ID" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement____last_update +#: model:ir.model.fields,field_description:announcement.field_announcement_log____last_update +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard____last_update +msgid "Last Modified on" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__write_uid +#: model:ir.model.fields,field_description:announcement.field_announcement_log__write_uid +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__write_date +#: model:ir.model.fields,field_description:announcement.field_announcement_log__write_date +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__write_date +msgid "Last Updated on" +msgstr "" + +#. module: announcement +#: model:ir.model,name:announcement.model_announcement_log +msgid "Log user reads" +msgstr "" + +#. module: announcement +#. openerp-web +#: code:addons/announcement/static/src/js/announcement.js:0 +#, python-format +msgid "Mark as read" +msgstr "" + +#. module: announcement +#. openerp-web +#: code:addons/announcement/static/src/xml/announcement.xml:0 +#, python-format +msgid "No announcements." +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__notification_date +msgid "Notification Date" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__notification_expiry_date +msgid "Notification Expiry Date" +msgstr "" + +#. module: announcement +#: model:ir.model.fields.selection,name:announcement.selection__read_announcement_wizard__read_state__pendant +msgid "Pendant" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_res_users__pendant_announcement_ids +msgid "Pendant Announcement" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_res_users__popup_announcements +msgid "Popup Announcements" +msgstr "" + +#. module: announcement +#: model:ir.model.fields.selection,name:announcement.selection__read_announcement_wizard__read_state__read +msgid "Read" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_res_users__read_announcement_ids +msgid "Read Announcement" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__read_announcement_count +msgid "Read Announcement Count" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__date +msgid "Read Date" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__read_state +msgid "Read State" +msgstr "" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_management_view_tree +msgid "Reads" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__sequence +msgid "Sequence" +msgstr "" + +#. module: announcement +#: model:ir.model,name:announcement.model_read_announcement_wizard +msgid "Show altogether users who read and users who didn't" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__specific_user_ids +msgid "Specific User" +msgstr "" + +#. module: announcement +#: model:ir.model.fields.selection,name:announcement.selection__announcement__announcement_type__specific_users +msgid "Specific users" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__name +msgid "Title" +msgstr "" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_management_view_tree +msgid "Total users" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__user_id +msgid "User" +msgstr "" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__user_group_ids +msgid "User Group" +msgstr "" + +#. module: announcement +#: model:ir.model,name:announcement.model_announcement +msgid "User announcements" +msgstr "" + +#. module: announcement +#: model:ir.model.fields.selection,name:announcement.selection__announcement__announcement_type__user_group +msgid "User groups" +msgstr "" + +#. module: announcement +#: model:ir.model,name:announcement.model_res_users +#: model_terms:ir.ui.view,arch_db:announcement.announcement_management_view_form +msgid "Users" +msgstr "" + +#. module: announcement +#: model:res.groups,comment:announcement.announcemenent_manager +msgid "Users allowed to manage and configure announcements." +msgstr "" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_form +msgid "Valid up to" +msgstr "" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_form +msgid "e.g. Announcement description..." +msgstr "" diff --git a/announcement/i18n/es.po b/announcement/i18n/es.po new file mode 100644 index 0000000000..aa857cc493 --- /dev/null +++ b/announcement/i18n/es.po @@ -0,0 +1,308 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * announcement +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-08-22 11:10+0000\n" +"PO-Revision-Date: 2022-08-22 13:13+0200\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: \n" +"Language: es\n" +"X-Generator: Poedit 2.3\n" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_management_view_form +msgid "Read(s)" +msgstr "Leído(s)" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__active +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_search +msgid "Active" +msgstr "Activo" + +#. module: announcement +#. openerp-web +#: code:addons/announcement/static/src/xml/announcement.xml:0 +#, python-format +msgid "Activities" +msgstr "Actividades" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__allowed_user_ids +msgid "Allowed User" +msgstr "Usuario permitido" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__allowed_users_count +msgid "Allowed Users Count" +msgstr "Número de usuarios permitidos" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_form +msgid "Announce at" +msgstr "Anunciar a" + +#. module: announcement +#. openerp-web +#: code:addons/announcement/static/src/xml/announcement.xml:0 +#: model:ir.model.fields,field_description:announcement.field_announcement_log__announcement_id +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__announcement_id +#: model:ir.module.category,name:announcement.category_announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_form +#, python-format +msgid "Announcement" +msgstr "Anuncio" + +#. module: announcement +#: model:ir.actions.act_window,name:announcement.action_announcement_log +msgid "Announcement Log" +msgstr "Registro del anuncio" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_log_view_tree +#: model_terms:ir.ui.view,arch_db:announcement.read_announcement_wizard_view_tree +msgid "Announcement Logs" +msgstr "Registros de anuncio" + +#. module: announcement +#: model:res.groups,name:announcement.announcemenent_manager +msgid "Announcement Manager" +msgstr "Responsable de anuncios" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__announcement_type +msgid "Announcement Type" +msgstr "Tipo de anuncio" + +#. module: announcement +#. openerp-web +#: code:addons/announcement/static/src/xml/announcement.xml:0 +#: model:ir.actions.act_window,name:announcement.announcement_action +#: model:ir.ui.menu,name:announcement.menu_announcement_management +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_tree +#, python-format +msgid "Announcements" +msgstr "Anuncios" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_search +msgid "Archived" +msgstr "Archivado" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__content +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_form +msgid "Content" +msgstr "Contenido" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__create_uid +#: model:ir.model.fields,field_description:announcement.field_announcement_log__create_uid +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__create_date +#: model:ir.model.fields,field_description:announcement.field_announcement_log__create_date +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__create_date +msgid "Created on" +msgstr "Creado en" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__display_name +#: model:ir.model.fields,field_description:announcement.field_announcement_log__display_name +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_management_view_form +msgid "Groups" +msgstr "Grupos" + +#. module: announcement +#: model:ir.model,name:announcement.model_ir_http +msgid "HTTP Routing" +msgstr "Ruta HTTP" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__id +#: model:ir.model.fields,field_description:announcement.field_announcement_log__id +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement____last_update +#: model:ir.model.fields,field_description:announcement.field_announcement_log____last_update +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard____last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__write_uid +#: model:ir.model.fields,field_description:announcement.field_announcement_log__write_uid +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__write_uid +msgid "Last Updated by" +msgstr "Última actualización de" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__write_date +#: model:ir.model.fields,field_description:announcement.field_announcement_log__write_date +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: announcement +#: model:ir.model,name:announcement.model_announcement_log +msgid "Log user reads" +msgstr "Registrar lecturas de usuario" + +#. module: announcement +#. openerp-web +#: code:addons/announcement/static/src/js/announcement.js:0 +#, python-format +msgid "Mark as read" +msgstr "Marcar como léido" + +#. module: announcement +#. openerp-web +#: code:addons/announcement/static/src/xml/announcement.xml:0 +#, python-format +msgid "No announcements." +msgstr "No hay anuncios." + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__notification_date +msgid "Notification Date" +msgstr "Fecha de notificación" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__notification_expiry_date +msgid "Notification Expiry Date" +msgstr "Fecha de caducidad de la notificación" + +#. module: announcement +#: model:ir.model.fields.selection,name:announcement.selection__read_announcement_wizard__read_state__pendant +msgid "Pendant" +msgstr "Pendientes" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_res_users__pendant_announcement_ids +msgid "Pendant Announcement" +msgstr "Anuncio pendiente" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_res_users__popup_announcements +msgid "Popup Announcements" +msgstr "Mostrar anuncios" + +#. module: announcement +#: model:ir.model.fields.selection,name:announcement.selection__read_announcement_wizard__read_state__read +msgid "Read" +msgstr "Leídos" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_res_users__read_announcement_ids +msgid "Read Announcement" +msgstr "Anuncio leído" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__read_announcement_count +msgid "Read Announcement Count" +msgstr "Número de anuncios leídos" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__date +msgid "Read Date" +msgstr "Fecha de lectura" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__read_state +msgid "Read State" +msgstr "Estado de lectura" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_management_view_tree +msgid "Reads" +msgstr "Lecturas" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: announcement +#: model:ir.model,name:announcement.model_read_announcement_wizard +msgid "Show altogether users who read and users who didn't" +msgstr "Muestra juntos los usuarios que han leído un anuncio y los que no" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__specific_user_ids +msgid "Specific User" +msgstr "Usuarios específico" + +#. module: announcement +#: model:ir.model.fields.selection,name:announcement.selection__announcement__announcement_type__specific_users +msgid "Specific users" +msgstr "Usuarios específicos" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__name +msgid "Title" +msgstr "Título" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_management_view_tree +msgid "Total users" +msgstr "Total usuarios" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_read_announcement_wizard__user_id +msgid "User" +msgstr "Usuario\t" + +#. module: announcement +#: model:ir.model.fields,field_description:announcement.field_announcement__user_group_ids +msgid "User Group" +msgstr "Grupo de usuarios" + +#. module: announcement +#: model:ir.model,name:announcement.model_announcement +msgid "User announcements" +msgstr "Anuncios del usuario" + +#. module: announcement +#: model:ir.model.fields.selection,name:announcement.selection__announcement__announcement_type__user_group +msgid "User groups" +msgstr "Grupos de usuarios" + +#. module: announcement +#: model:ir.model,name:announcement.model_res_users +#: model_terms:ir.ui.view,arch_db:announcement.announcement_management_view_form +msgid "Users" +msgstr "Usuarios" + +#. module: announcement +#: model:res.groups,comment:announcement.announcemenent_manager +msgid "Users allowed to manage and configure announcements." +msgstr "Usuarios autorizados a administrar y configurar anuncios." + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_form +msgid "Valid up to" +msgstr "Válido hasta" + +#. module: announcement +#: model_terms:ir.ui.view,arch_db:announcement.announcement_view_form +msgid "e.g. Announcement description..." +msgstr "p.e. Descripción del anuncio..." diff --git a/announcement/models/__init__.py b/announcement/models/__init__.py new file mode 100644 index 0000000000..df3e823538 --- /dev/null +++ b/announcement/models/__init__.py @@ -0,0 +1,3 @@ +from . import announcement +from . import res_users +from . import ir_http diff --git a/announcement/models/announcement.py b/announcement/models/announcement.py new file mode 100644 index 0000000000..f1c501b1bc --- /dev/null +++ b/announcement/models/announcement.py @@ -0,0 +1,128 @@ +# Copyright 2022 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class AnnouncementLog(models.Model): + _name = "announcement.log" + _description = "Log user reads" + _order = "create_date desc" + + announcement_id = fields.Many2one(comodel_name="announcement") + + +class Announcement(models.Model): + _name = "announcement" + _description = "User announcements" + _order = "notification_date, sequence asc, id" + + active = fields.Boolean(copy=False) + sequence = fields.Integer() + name = fields.Char(string="Title", required=True) + content = fields.Html() + announcement_type = fields.Selection( + selection=[ + ("specific_users", "Specific users"), + ("user_group", "User groups"), + ], + default="specific_users", + required=True, + ) + specific_user_ids = fields.Many2many( + comodel_name="res.users", + domain=[("share", "=", False)], + inverse="_inverse_specific_user_ids", + ) + user_group_ids = fields.Many2many(comodel_name="res.groups") + allowed_user_ids = fields.Many2many( + comodel_name="res.users", + relation="announcement_res_users_allowed_rel", + compute="_compute_allowed_user_ids", + compute_sudo=True, + store=True, + ) + allowed_users_count = fields.Integer( + compute="_compute_allowed_user_ids", compute_sudo=True, store=True, + ) + read_announcement_count = fields.Integer( + compute="_compute_read_announcement_count", store=True, + ) + notification_date = fields.Datetime() + notification_expiry_date = fields.Datetime() + + def _inverse_specific_user_ids(self): + """Used to set users pendant announcements when they're set in the announcement + itself""" + for announcement in self: + for user in announcement.specific_user_ids.filtered( + lambda x: announcement + not in (x.read_announcement_ids + x.pendant_announcement_ids) + ): + user.pendant_announcement_ids |= announcement + + @api.depends("specific_user_ids", "user_group_ids") + def _compute_allowed_user_ids(self): + self.allowed_user_ids = False + self.allowed_users_count = False + for announcement in self.filtered( + lambda x: x.announcement_type == "specific_users" + ): + announcement.allowed_user_ids = announcement.specific_user_ids + announcement.allowed_users_count = len(announcement.specific_user_ids) + for announcement in self.filtered( + lambda x: x.announcement_type == "user_group" + ): + announcement.allowed_user_ids = announcement.user_group_ids.users + announcement.allowed_users_count = len(announcement.user_group_ids.users) + + def _compute_read_announcement_count(self): + logs = self.env["announcement.log"].read_group( + [("announcement_id", "in", self.ids)], + ["announcement_id"], + ["announcement_id"], + ) + result = { + data["announcement_id"][0]: (data["announcement_id_count"]) for data in logs + } + for announcement in self: + announcement.read_announcement_count = result.get(announcement.id, 0) + + @api.onchange("announcement_type") + def _onchange_announcement_type(self): + if self.announcement_type == "specific_users": + self.user_group_ids = False + elif self.announcement_type == "user_group": + self.specific_user_ids = False + + def action_announcement_log(self): + """Return a set of fungible transient records to see altogether read logs + and pendant users""" + self.ensure_one() + read_logs = self.env["announcement.log"].search( + [("announcement_id", "in", self.ids)] + ) + pendant_users = self.allowed_user_ids.filtered( + lambda x: x not in read_logs.create_uid + ) + read_pendant_log = self.env["read.announcement.wizard"].create( + [ + { + "user_id": log.create_uid.id, + "date": log.create_date, + "announcement_id": self.id, + "read_state": "read", + } + for log in read_logs + ] + ) + read_pendant_log += self.env["read.announcement.wizard"].create( + [{"user_id": user.id, "read_state": "pendant"} for user in pendant_users] + ) + return { + "type": "ir.actions.act_window", + "res_model": "read.announcement.wizard", + "views": [[False, "tree"]], + "domain": [("id", "in", read_pendant_log.ids)], + "context": dict(self._context, create=False, group_by=["read_state"]), + "name": "Read Logs", + } diff --git a/announcement/models/ir_http.py b/announcement/models/ir_http.py new file mode 100644 index 0000000000..74fd723ae5 --- /dev/null +++ b/announcement/models/ir_http.py @@ -0,0 +1,11 @@ +from odoo import models + + +class IrHttp(models.AbstractModel): + _inherit = "ir.http" + + def session_info(self): + session_info = super().session_info() + if self.env.user.has_group("base.group_user"): + session_info["popup_announcements"] = self.env.user.popup_announcements + return session_info diff --git a/announcement/models/res_users.py b/announcement/models/res_users.py new file mode 100644 index 0000000000..5ed6e40c39 --- /dev/null +++ b/announcement/models/res_users.py @@ -0,0 +1,62 @@ +# Copyright 2022 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + pendant_announcement_ids = fields.Many2many( + comodel_name="announcement", relation="pendant_announcement_user_rel", + ) + read_announcement_ids = fields.Many2many( + comodel_name="announcement", relation="read_announcement_user_rel", + ) + popup_announcements = fields.Boolean() + + @api.model + def announcement_user_count(self): + """The js widget gathers the announcements from this method""" + now = fields.Datetime.now() + group_announcements = self.env["announcement"].search_read( + [ + ("announcement_type", "=", "user_group"), + "|", + ("notification_date", "<=", now), + ("notification_date", "=", False), + "|", + ("notification_expiry_date", ">=", now), + ("notification_expiry_date", "=", False), + ("id", "not in", self.env.user.read_announcement_ids.ids), + ], + ["user_group_ids"], + ) + announcements = self.env["announcement"].browse( + { + x["id"] + for x in group_announcements + if any( + [g for g in x["user_group_ids"] if g in self.env.user.groups_id.ids] + ) + } + ) + announcements |= self.env.user.pendant_announcement_ids.filtered( + lambda x: (not x.notification_date or x.notification_date <= now) + and (not x.notification_expiry_date or x.notification_expiry_date >= now) + ).sorted(lambda k: k.sequence) + return announcements.read() + + @api.model + def mark_announcement_as_read(self, announcement_id): + """Used as a controller for the widget""" + self.env.user.popup_announcements = False + announcement = self.env["announcement"].browse(int(announcement_id)) + self.env.user.pendant_announcement_ids -= announcement + self.env.user.read_announcement_ids += announcement + self.env["announcement.log"].create({"announcement_id": announcement.id}) + + @api.model + def _update_last_login(self): + """When the user logs in and has pendant announcements they'll be popped up""" + self.env.user.popup_announcements = bool(self.env.user.announcement_user_count) + return super()._update_last_login() diff --git a/announcement/readme/CONFIGURE.rst b/announcement/readme/CONFIGURE.rst new file mode 100644 index 0000000000..24171e9d34 --- /dev/null +++ b/announcement/readme/CONFIGURE.rst @@ -0,0 +1,21 @@ +To create new announcements a user should be in the *Announcements Managers* group. +When your user has such permissions, this is the way to create an announcement: + +#. Go to *Discuss > Announcements* +#. Create a new one and define a title. This title will be shown in the announcement + header. +#. Define the announcement scope: + + - Specific users: manually select which users will see the announcement. + - User groups: users from the selected groups will be the ones to see the + announcement. +#. Define the announcement body. You can use rich formatting and event paste your + own html (editor in debug mode). +#. By default, the announcement will be archived. This is to prevent the announcement + to show up before time. +#. Once the announcement is ready, unarchive it going to the *Actions* menu an choosing + the *Unarchive* option. +#. Optionally you can set an announcement date to schedule the announcement. The + announcement won't show up until that date. +#. If the announcement doesn't make sense once a date is passed, you can set a due date. + From that date, the announcement won't be shown to anyone. diff --git a/announcement/readme/CONTRIBUTORS.rst b/announcement/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..1b92619083 --- /dev/null +++ b/announcement/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `Tecnativa `__: + + * Pedro M. Baeza + * David Vidal diff --git a/announcement/readme/DESCRIPTION.rst b/announcement/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..83f1a08e15 --- /dev/null +++ b/announcement/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module adds popup announcements in the backend for targeted internal users. Those +announcements can contain rich format and a user read log is kept for everyone. diff --git a/announcement/readme/ROADMAP.rst b/announcement/readme/ROADMAP.rst new file mode 100644 index 0000000000..d246442dd6 --- /dev/null +++ b/announcement/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +* It could be integrated in Discuss app to review past announcements. +* Log other information like geolocation, IP, browser agent, etc when marking + announcement as read. diff --git a/announcement/readme/USAGE.rst b/announcement/readme/USAGE.rst new file mode 100644 index 0000000000..b857fab22f --- /dev/null +++ b/announcement/readme/USAGE.rst @@ -0,0 +1,8 @@ +When a user in the scope of active announcements logs in, those will popup. The user +has to mark them as read to continue working. If the announcement is set during the +user session, the announcement will be eventually prompted in the top bar on the right +part. The user click on the unread announcements icon (a speaker) and the announcements +will popup for the user to check them. + +Users can go *Discuss > Announcements* to check current and past announcements. +Announcement managers can also track which users have read the announcement. diff --git a/announcement/security/announcement_security.xml b/announcement/security/announcement_security.xml new file mode 100644 index 0000000000..ee4522342b --- /dev/null +++ b/announcement/security/announcement_security.xml @@ -0,0 +1,50 @@ + + + + Announcement + + + Announcement Manager + + + Users allowed to manage and configure announcements. + + + + + Announcement log per user + + [('create_uid','=', user.id)] + + + + Announcement log manager + + [(1, '=', 1)] + + + + User announcements + + [('allowed_user_ids', 'in', user.id)] + + + + Announcement managers + + [(1, '=', 1)] + + + + diff --git a/announcement/security/ir.model.access.csv b/announcement/security/ir.model.access.csv new file mode 100644 index 0000000000..0420bd42bf --- /dev/null +++ b/announcement/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_announcement_management,announcement.manager,model_announcement,announcement.announcemenent_manager,1,1,1,1 +access_announcement_user,announcement.user,model_announcement,base.group_user,1,0,0,0 +access_announcement_log_all,announcement_log_all,model_announcement_log,base.group_user,1,0,1,0 diff --git a/announcement/static/description/icon.png b/announcement/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9592dd60d5666dbb20ccd3d6c1f3453d47f3de19 GIT binary patch literal 4887 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 As{jB1 literal 0 HcmV?d00001 diff --git a/announcement/static/description/index.html b/announcement/static/description/index.html new file mode 100644 index 0000000000..605d9b4db1 --- /dev/null +++ b/announcement/static/description/index.html @@ -0,0 +1,471 @@ + + + + + + +Announcement + + + +
+

Announcement

+ + +

Beta License: AGPL-3 OCA/server-ux Translate me on Weblate Try me on Runbot

+

This module adds popup announcements in the backend for targeted internal users. Those +announcements can contain rich format and a user read log is kept for everyone.

+

Table of contents

+ +
+

Configuration

+

To create new announcements a user should be in the Announcements Managers group. +When your user has such permissions, this is the way to create an announcement:

+
    +
  1. Go to Discuss > Announcements
  2. +
  3. Create a new one and define a title. This title will be shown in the announcement +header.
  4. +
  5. Define the announcement scope:
      +
    • Specific users: manually select which users will see the announcement.
    • +
    • User groups: users from the selected groups will be the ones to see the +announcement.
    • +
    +
  6. +
  7. Define the announcement body. You can use rich formatting and event paste your +own html (editor in debug mode).
  8. +
  9. By default, the announcement will be archived. This is to prevent the announcement +to show up before time.
  10. +
  11. Once the announcement is ready, unarchive it going to the Actions menu an choosing +the Unarchive option.
  12. +
  13. Optionally you can set an announcement date to schedule the announcement. The +announcement won’t show up until that date.
  14. +
  15. If the announcement doesn’t make sense once a date is passed, you can set a due date. +From that date, the announcement won’t be shown to anyone.
  16. +
+
+
+

Usage

+

When a user in the scope of active announcements logs in, those will popup. The user +has to mark them as read to continue working. If the announcement is set during the +user session, the announcement will be eventually prompted in the top bar on the right +part. The user click on the unread announcements icon (a speaker) and the announcements +will popup for the user to check them.

+

Users can go Discuss > Announcements to check current and past announcements. +Announcement managers can also track which users have read the announcement.

+
+
+

Known issues / Roadmap

+
    +
  • It could be integrated in Discuss app to review past announcements.
  • +
  • Log other information like geolocation, IP, browser agent, etc when marking +announcement as read.
  • +
+
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+
    +
  • Tecnativa:
      +
    • Pedro M. Baeza
    • +
    • David Vidal
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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

+

This module is part of the OCA/server-ux project on GitHub.

+

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

+
+
+
+ + diff --git a/announcement/static/src/js/announcement.js b/announcement/static/src/js/announcement.js new file mode 100644 index 0000000000..401141ecf8 --- /dev/null +++ b/announcement/static/src/js/announcement.js @@ -0,0 +1,222 @@ +odoo.define("announcement.systray", function(require) { + "use strict"; + + require("web.dom_ready"); + const core = require("web.core"); + const session = require("web.session"); + const SystrayMenu = require("web.SystrayMenu"); + const Widget = require("web.Widget"); + const AnnouncementDialog = require("announment.AnnouncementDialog"); + + const QWeb = core.qweb; + const _t = core._t; + + const AnnouncementMenu = Widget.extend({ + template: "announcement.AnnouncementMenu", + events: { + "show.bs.dropdown": "_onAnnouncementMenuShow", + "click .o_mail_preview": "_onAnnouncementClick", + }, + start: function() { + this.$announcement_preview = this.$(".o_mail_systray_dropdown_items"); + this._updateAnnouncementPreview(); + this.call("bus_service", "addChannel", "announcement"); + this.call("bus_service", "startPolling"); + this.call( + "bus_service", + "onNotification", + this, + this._updateAnnouncementPreview + ); + // When the user logs in we show him his pendant announcements + const _this = this; + function waitAndCheck() { + if (odoo.isReady) { + _this._getAnnouncementData().then(() => { + if ( + session.popup_announcements && + !_.isEmpty(_this.announcements) + ) { + _this.announcements[0].dialog.open(); + } + }); + } else { + setTimeout(waitAndCheck, 500); + } + } + setTimeout(waitAndCheck, 500); + return this._super(); + }, + + // Private + + /** + * Make RPC and get current user's activity details + * @private + * @returns {integer} + */ + _getAnnouncementData: function() { + return this._rpc({ + model: "res.users", + method: "announcement_user_count", + kwargs: { + context: session.user_context, + }, + }).then(data => { + this._prepareAnnouncementData(data || []); + this.announcements = data; + this.announcementCounter = this.announcements.length; + this.$(".o_notification_counter").text(this.announcementCounter); + this.$el.toggleClass("o_no_notification", !this.announcementCounter); + }); + }, + /** + * Update(render) announcement system tray view on announcement refresh + * @private + */ + _updateAnnouncementPreview: function() { + this._getAnnouncementData().then(() => { + const render_context = { + announcements: this.announcements, + pending_count: this.announcementCounter, + }; + this.$announcement_preview.html( + QWeb.render("announcement.AnnouncementMenuPreview", render_context) + ); + }); + }, + /** + * Update counter based on announcement status + * @private + * @param {Object} [data] key, value to decide announcement read or unread + * @param {String} [data.type] notification type + * @param {Boolean} [data.announcement_unread] when announcement unread + * @param {Boolean} [data.activity_created] when announcement gets read + */ + _updateCounter: function(data) { + if (data) { + if (data.announcement_unread) { + this.announcementCounter++; + } + if (data.announcement_read && this.announcementCounter > 0) { + this.announcementCounter--; + } + this.$(".o_notification_counter").text(this.announcementCounter); + this.$el.toggleClass("o_no_notification", !this.announcementCounter); + } + }, + /** + * Prepare the announcements data so we can work with them properly (attach + * popup classes, get the next and previous ids for proper navigation, etc.) + * @private + * @param {Object} data + */ + _prepareAnnouncementData: function(data) { + _.each(data, a => { + const index = _.indexOf(data, a); + // We make it as an infinite loop so the last announcement next slide + // is the first announcement. + const previous_announcement_id = + data[index - 1] || data[data.length - 1]; + const next_announcement_id = data[index + 1] || data[0]; + a.next_announcement_id = + next_announcement_id && next_announcement_id.id; + // This one is not being used but it could be handy in future features. + a.previous_announcement_id = + previous_announcement_id && previous_announcement_id.id; + a.dialog = this._builAnnouncementDialog(a); + }); + }, + /** + * @private + * @param {id} announcement + * @returns + */ + _getAnnouncementById: function(id) { + return this.announcements.filter(a => { + return a.id === id && a; + })[0]; + }, + /** + * @private + * @param {id} announcement + * @returns + */ + _openAnnouncemenId: function(id) { + const announcement = this._getAnnouncementById(id); + // The announcement could be already destroyed + if (_.isEmpty(announcement)) { + return; + } + this._getAnnouncementById(id).dialog.open(); + }, + /** + * Build announcement popup + * @private + * @param {Object} announcement + * @returns {Object} dialog - standard odoo dialog + */ + _builAnnouncementDialog: function(announcement) { + const next_announcement = announcement.next_announcement_id; + const dialog = new AnnouncementDialog(this, { + title: announcement.name, + buttons: [ + { + text: _t("Mark as read"), + classes: "btn-primary", + close: true, + click: () => { + this._rpc({ + model: "res.users", + method: "mark_announcement_as_read", + args: [announcement.id], + kwargs: { + context: session.user_context, + }, + }) + .then(() => { + this._updateAnnouncementPreview(); + }) + .then(() => { + // As the announment list is chained in a loop we want + // to avoid opening the same announcement we just closed + if (announcement.id !== next_announcement) { + this._openAnnouncemenId(next_announcement); + } + }); + }, + }, + ], + $content: $("
").append($(announcement.content)), + }); + return dialog; + }, + + // ------------------------------------------------------------ + // Handlers + // ------------------------------------------------------------ + + /** + * Show announcement popup + * @private + * @param {MouseEvent} event + */ + _onAnnouncementClick: function(event) { + const data = $(event.currentTarget).data(); + const announcement = this._getAnnouncementById(data.id); + announcement.dialog.open(); + }, + /** + * When menu clicked update activity preview if counter updated + * @private + * @param {MouseEvent} event + */ + _onAnnouncementMenuShow: function() { + this._updateAnnouncementPreview(); + }, + }); + + SystrayMenu.Items.push(AnnouncementMenu); + + return AnnouncementMenu; +}); diff --git a/announcement/static/src/js/announcement_dialog.js b/announcement/static/src/js/announcement_dialog.js new file mode 100644 index 0000000000..675e10f3eb --- /dev/null +++ b/announcement/static/src/js/announcement_dialog.js @@ -0,0 +1,52 @@ +odoo.define("announment.AnnouncementDialog", function(require) { + "use strict"; + + const core = require("web.core"); + const Dialog = require("web.Dialog"); + + const QWeb = core.qweb; + + /** + * @class AnnouncementDialog + * + * We'd like to use regular dialog, but those can be closed with ESC Key. Anyway, this + * leaves us more freedom to shape the dialog as we want to. + */ + var AnnouncementDialog = Dialog.extend({ + template: "announcement.AnnouncementDialog", + /** + * Wait for XML dependencies and instantiate the modal structure (except + * modal-body). + * + * @override + */ + willStart: function() { + const resize = Boolean(this._trigger_resize); + return this._super.apply(this, arguments).then(() => { + // Render modal once xml dependencies are loaded + this.$modal = $( + QWeb.render("announcement.AnnouncementDialog", { + title: this.title, + subtitle: this.subtitle, + resize: resize, + }) + ); + // Soft compatibility with OCAs `web_dialog_size` + if (resize) { + this.$modal + .find(".dialog_button_extend") + .on("click", this.proxy("_extending")); + this.$modal + .find(".dialog_button_restore") + .on("click", this.proxy("_restore")); + this._restore(); + } + this.$footer = this.$modal.find(".modal-footer"); + this.set_buttons(this.buttons); + this.$modal.on("hidden.bs.modal", _.bind(this.destroy, this)); + }); + }, + }); + + return AnnouncementDialog; +}); diff --git a/announcement/static/src/xml/announcement.xml b/announcement/static/src/xml/announcement.xml new file mode 100644 index 0000000000..f6e321efb3 --- /dev/null +++ b/announcement/static/src/xml/announcement.xml @@ -0,0 +1,53 @@ + + + + + + + +
+
+ Announcement +
+
+
+ + + +
+
+
+
+
+ +
  • + +
  • +
    +
    diff --git a/announcement/static/src/xml/announcement_dialog.xml b/announcement/static/src/xml/announcement_dialog.xml new file mode 100644 index 0000000000..ca56a8157d --- /dev/null +++ b/announcement/static/src/xml/announcement_dialog.xml @@ -0,0 +1,35 @@ + + + + + + diff --git a/announcement/templates/assets_backend.xml b/announcement/templates/assets_backend.xml new file mode 100644 index 0000000000..61f947ad2f --- /dev/null +++ b/announcement/templates/assets_backend.xml @@ -0,0 +1,15 @@ + + +