diff --git a/mail_post_defer/README.rst b/mail_post_defer/README.rst new file mode 100644 index 0000000..29fabb6 --- /dev/null +++ b/mail_post_defer/README.rst @@ -0,0 +1,164 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======================== +Deferred Message Posting +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:5d35c257858b754315a0d2e0b11be43a2d0337857cbdc635da5dfa83d48e2477 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmail-lightgray.png?logo=github + :target: https://github.com/OCA/mail/tree/19.0/mail_post_defer + :alt: OCA/mail +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/mail-19-0/mail-19-0-mail_post_defer + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/mail&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module enhances mail threads by using the mail queue by default. + +Without this module, Odoo attempts to notify recipients of your message +immediately. If your mail server is slow or you have many followers, +this can mean a lot of time. Install this module and make Odoo more +snappy! + +All emails will be kept in the outgoing queue by at least 30 seconds, +giving you some time to re-think what you wrote. During that time, you +can still delete the message and start again. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +You usually don't need to do anything. The module is configured +appropriately out of the box. Just make sure the following scheduled +actions are active: + +- Mail: Email Queue Manager (mail.ir_cron_mail_scheduler_action) +- Notification: Notify scheduled messages + (mail.ir_cron_send_scheduled_message) + +The mail queue processing and scheduled messages notifications are +handled by cron jobs. This is normal Odoo behavior, not specific to this +module. However, since you will start using that queue for every message +posted by any user in any thread, both jobs are configured to execute +every minute by default. + +You can still change that cadence after installing the module (although +it is not recommended). To do so: + +1. Log in with an administrator user. +2. Activate developer mode. +3. Go to *Settings > Technical > Automation > Scheduled Actions*. +4. Find the cron(s) you want to adjust: + + - Mail: Email Queue Manager - handles outgoing emails. + - Notification: Notify scheduled messages - handles notifications. + +5. Lower down the frequency in the field *Execute Every*. Recommended: 1 + minute. + +Usage +===== + +To use this module, you need to: + +1. Go to the form view of any record that has a mail thread. It can be a + partner, for example. +2. Post a message. + +The mail is now in the outgoing mail queue. It will be there for at +least 30 seconds. It will be really sent the next time the "Mail: Email +Queue Manager" cron job is executed. + +While the message has not been yet sent: + +1. Click the little envelope. You will see a paper airplane icon, + indicating it is still outgoing. +2. Hover over the message and click on *⠇ > 🗑️ Delete*. Mails will not + be sent. + +Known issues / Roadmap +====================== + +- Add minimal deferring time configuration if it ever becomes necessary. + See https://github.com/OCA/social/pull/1001#issuecomment-1461581573 + for the rationale behind current hardcoded value of 30 seconds. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Moduon +* OERP Canada + +Contributors +------------ + +- Jairo Llopis (https://www.moduon.team/) +- `OERP Canada `__ + + - Yash Pathak + +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. + +.. |maintainer-Yajo| image:: https://github.com/Yajo.png?size=40px + :target: https://github.com/Yajo + :alt: Yajo + +Current `maintainer `__: + +|maintainer-Yajo| + +This module is part of the `OCA/mail `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mail_post_defer/__init__.py b/mail_post_defer/__init__.py new file mode 100644 index 0000000..cc6b635 --- /dev/null +++ b/mail_post_defer/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import post_init_hook diff --git a/mail_post_defer/__manifest__.py b/mail_post_defer/__manifest__.py new file mode 100644 index 0000000..0c67bf1 --- /dev/null +++ b/mail_post_defer/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2022-2023 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). +{ + "name": "Deferred Message Posting", + "summary": "Faster and cancellable outgoing messages", + "version": "19.0.1.0.0", + "development_status": "Alpha", + "category": "Productivity/Discuss", + "website": "https://github.com/OCA/mail", + "author": "Moduon, OERP Canada, Odoo Community Association (OCA)", + "maintainers": ["Yajo"], + "license": "LGPL-3", + "depends": [ + "mail", + ], + "post_init_hook": "post_init_hook", +} diff --git a/mail_post_defer/hooks.py b/mail_post_defer/hooks.py new file mode 100644 index 0000000..bb9f035 --- /dev/null +++ b/mail_post_defer/hooks.py @@ -0,0 +1,28 @@ +# Copyright 2022-2023 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). +import logging + +_logger = logging.getLogger(__name__) + + +def post_init_hook(env): + """Increase cadence of mail queue and notification crons.""" + crons = { + "mail.ir_cron_mail_scheduler_action": "Mail: Email Queue Manager", + "mail.ir_cron_send_scheduled_message": "Notification: Notify scheduled messages", # noqa: E501 + } + + for cron_ref, description in crons.items(): + try: + cron = env.ref(cron_ref) + except ValueError: + _logger.warning( + "Couldn't find the %s cron (%s). " + "Maybe no mails/notification will be ever sent!.", + description, + cron_ref, + ) + else: + _logger.info("Setting %s cron cadence to 1 minute", description) + cron.interval_number = 1 + cron.interval_type = "minutes" diff --git a/mail_post_defer/i18n/es.po b/mail_post_defer/i18n/es.po new file mode 100644 index 0000000..7b7e5b9 --- /dev/null +++ b/mail_post_defer/i18n/es.po @@ -0,0 +1,39 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_post_defer +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-01-09 20:34+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: mail_post_defer +#. odoo-python +#: code:addons/mail_post_defer/models/mail_thread.py:0 +#, python-format +msgid "Cannot modify message; notifications were already sent." +msgstr "No se puede modificar el mensaje; ya se han enviado notificaciones." + +#. module: mail_post_defer +#: model:ir.model,name:mail_post_defer.model_mail_thread +msgid "Email Thread" +msgstr "Hilo de Correo Electrónico" + +#. module: mail_post_defer +#: model:ir.model,name:mail_post_defer.model_mail_message +msgid "Message" +msgstr "Mensaje" + +#. module: mail_post_defer +#: model:ir.model,name:mail_post_defer.model_mail_message_schedule +msgid "Scheduled Messages" +msgstr "" diff --git a/mail_post_defer/i18n/it.po b/mail_post_defer/i18n/it.po new file mode 100644 index 0000000..fbc405f --- /dev/null +++ b/mail_post_defer/i18n/it.po @@ -0,0 +1,40 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_post_defer +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-05-27 12:55+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: mail_post_defer +#. odoo-python +#: code:addons/mail_post_defer/models/mail_thread.py:0 +#, python-format +msgid "Cannot modify message; notifications were already sent." +msgstr "" +"Non si può modificare il messaggio; le notifiche sono già state spedite." + +#. module: mail_post_defer +#: model:ir.model,name:mail_post_defer.model_mail_thread +msgid "Email Thread" +msgstr "Discussione e-mail" + +#. module: mail_post_defer +#: model:ir.model,name:mail_post_defer.model_mail_message +msgid "Message" +msgstr "Messaggio" + +#. module: mail_post_defer +#: model:ir.model,name:mail_post_defer.model_mail_message_schedule +msgid "Scheduled Messages" +msgstr "Messaggi schedulati" diff --git a/mail_post_defer/i18n/mail_post_defer.pot b/mail_post_defer/i18n/mail_post_defer.pot new file mode 100644 index 0000000..9933b7c --- /dev/null +++ b/mail_post_defer/i18n/mail_post_defer.pot @@ -0,0 +1,36 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_post_defer +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.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: mail_post_defer +#: model:ir.model.fields,field_description:mail_post_defer.field_mail_message_schedule__display_name +#: model:ir.model.fields,field_description:mail_post_defer.field_mail_thread__display_name +msgid "Display Name" +msgstr "" + +#. module: mail_post_defer +#: model:ir.model,name:mail_post_defer.model_mail_thread +msgid "Email Thread" +msgstr "" + +#. module: mail_post_defer +#: model:ir.model.fields,field_description:mail_post_defer.field_mail_message_schedule__id +#: model:ir.model.fields,field_description:mail_post_defer.field_mail_thread__id +msgid "ID" +msgstr "" + +#. module: mail_post_defer +#: model:ir.model,name:mail_post_defer.model_mail_message_schedule +msgid "Scheduled Messages" +msgstr "" diff --git a/mail_post_defer/models/__init__.py b/mail_post_defer/models/__init__.py new file mode 100644 index 0000000..140386f --- /dev/null +++ b/mail_post_defer/models/__init__.py @@ -0,0 +1,2 @@ +from . import mail_message_schedule +from . import mail_thread diff --git a/mail_post_defer/models/mail_message_schedule.py b/mail_post_defer/models/mail_message_schedule.py new file mode 100644 index 0000000..0cfb98a --- /dev/null +++ b/mail_post_defer/models/mail_message_schedule.py @@ -0,0 +1,26 @@ +# Copyright 2024 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import models + + +class MailMessageSchedule(models.Model): + _inherit = "mail.message.schedule" + + def _send_notifications(self, default_notify_kwargs=None): + """Avoid deferring notifications when they should be sent.""" + _self = self.with_context(mail_defer_seconds=0) + return super(MailMessageSchedule, _self)._send_notifications( + default_notify_kwargs=default_notify_kwargs + ) + + def _group_by_model(self): + """Make sure only mail.thread children are grouped with model.""" + result = super()._group_by_model() + for model, records in result.copy().items(): + # Move records without mail.thread mixin to a False key + if model and not hasattr(self.env.get(model), "_notify_thread"): + result.pop(model) + result.setdefault(False, self.browse()) + result[False] += records + return result diff --git a/mail_post_defer/models/mail_thread.py b/mail_post_defer/models/mail_thread.py new file mode 100644 index 0000000..40ba0da --- /dev/null +++ b/mail_post_defer/models/mail_thread.py @@ -0,0 +1,70 @@ +# Copyright 2022-2024 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from datetime import timedelta + +from odoo import fields, models + + +class MailThread(models.AbstractModel): + _inherit = "mail.thread" + + def _notify_thread(self, message, msg_vals=False, **kwargs): + """Defer emails by default.""" + # Don't defer automatically if forcing send + _self = self + if "mail_defer_seconds" not in _self.env.context: + force_send = _self.env.context.get("mail_notify_force_send") or kwargs.get( + "force_send", False + ) + kwargs.setdefault("force_send", force_send) + if not force_send: + # If deferring message, give the user some minimal time to revert it + _self = _self.with_context(mail_defer_seconds=30) + # Apply deferring + defer_seconds = _self.env.context.get("mail_defer_seconds") + if defer_seconds: + kwargs.setdefault( + "scheduled_date", + fields.Datetime.now() + timedelta(seconds=defer_seconds), + ) + return super(MailThread, _self)._notify_thread( + message, msg_vals=msg_vals, **kwargs + ) + + def _message_update_content(self, message, /, *, body, **kwargs): + # If anything already went out, fall back to the standard behavior + if any(ntf.notification_status == "sent" for ntf in message.notification_ids): + return super()._message_update_content(message, body=body, **kwargs) + scheduled_date = fields.Datetime.now() + timedelta(seconds=30) + Schedule = self.env["mail.message.schedule"].sudo() + sched = Schedule.search([("mail_message_id", "=", message.id)], limit=1) + if sched: + sched.scheduled_datetime = scheduled_date + # In case emails are already created but not sent yet, cancel queued mails and + # delete the notification (so nothing goes out with old content) + else: + message.mail_ids.filtered( + lambda x: x.state in {"outgoing", "exception", "draft"} + ).write({"state": "cancel"}) + message.notification_ids.filtered( + lambda x: x.notification_status + in {"ready", "exception", "canceled", "bounce"} + ).unlink() + Schedule.create( + {"mail_message_id": message.id, "scheduled_datetime": scheduled_date} + ) + kw = dict(kwargs) + # Drop empty [] for partner_ids, or else Odoo will try to send to nobody + if not kw.get("partner_ids"): + kw.pop("partner_ids", None) + res = super()._message_update_content(message, body=body, **kw) + # Delete empty pending outgoing mails + if empty_messages := message.sudo()._filter_empty(): + empty_messages.mail_ids.filtered( + lambda mail: mail.state == "outgoing" + ).unlink() + empty_messages.env["mail.message.schedule"].search( + [("mail_message_id", "in", empty_messages.ids)] + ).unlink() + return res diff --git a/mail_post_defer/pyproject.toml b/mail_post_defer/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/mail_post_defer/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/mail_post_defer/readme/CONFIGURE.md b/mail_post_defer/readme/CONFIGURE.md new file mode 100644 index 0000000..11c8ff0 --- /dev/null +++ b/mail_post_defer/readme/CONFIGURE.md @@ -0,0 +1,21 @@ +You usually don't need to do anything. The module is configured appropriately out of the box. +Just make sure the following scheduled actions are active: + +- Mail: Email Queue Manager (mail.ir_cron_mail_scheduler_action) +- Notification: Notify scheduled messages (mail.ir_cron_send_scheduled_message) + +The mail queue processing and scheduled messages notifications are handled by cron jobs. +This is normal Odoo behavior, not specific to this module. However, since you will start +using that queue for every message posted by any user in any thread, +both jobs are configured to execute every minute by default. + +You can still change that cadence after installing the module (although +it is not recommended). To do so: + +1. Log in with an administrator user. +2. Activate developer mode. +3. Go to *Settings \> Technical \> Automation \> Scheduled Actions*. +4. Find the cron(s) you want to adjust: + - Mail: Email Queue Manager - handles outgoing emails. + - Notification: Notify scheduled messages - handles notifications. +5. Lower down the frequency in the field *Execute Every*. Recommended: 1 minute. diff --git a/mail_post_defer/readme/CONTRIBUTORS.md b/mail_post_defer/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..d36af61 --- /dev/null +++ b/mail_post_defer/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Jairo Llopis () +- [OERP Canada](https://www.oerp.ca) + - Yash Pathak diff --git a/mail_post_defer/readme/DESCRIPTION.md b/mail_post_defer/readme/DESCRIPTION.md new file mode 100644 index 0000000..51bd185 --- /dev/null +++ b/mail_post_defer/readme/DESCRIPTION.md @@ -0,0 +1,10 @@ +This module enhances mail threads by using the mail queue by default. + +Without this module, Odoo attempts to notify recipients of your message +immediately. If your mail server is slow or you have many followers, +this can mean a lot of time. Install this module and make Odoo more +snappy! + +All emails will be kept in the outgoing queue by at least 30 seconds, +giving you some time to re-think what you wrote. During that time, you +can still delete the message and start again. diff --git a/mail_post_defer/readme/ROADMAP.md b/mail_post_defer/readme/ROADMAP.md new file mode 100644 index 0000000..ad91258 --- /dev/null +++ b/mail_post_defer/readme/ROADMAP.md @@ -0,0 +1,3 @@ +- Add minimal deferring time configuration if it ever becomes necessary. + See + for the rationale behind current hardcoded value of 30 seconds. diff --git a/mail_post_defer/readme/USAGE.md b/mail_post_defer/readme/USAGE.md new file mode 100644 index 0000000..30610e0 --- /dev/null +++ b/mail_post_defer/readme/USAGE.md @@ -0,0 +1,15 @@ +To use this module, you need to: + +1. Go to the form view of any record that has a mail thread. It can be + a partner, for example. +2. Post a message. + +The mail is now in the outgoing mail queue. It will be there for at +least 30 seconds. It will be really sent the next time the "Mail: Email +Queue Manager" cron job is executed. + +While the message has not been yet sent: + +1. Click the little envelope. You will see a paper airplane icon, + indicating it is still outgoing. +2. Hover over the message and click on *⠇ \> 🗑️ Delete*. Mails will not be sent. diff --git a/mail_post_defer/static/description/icon.png b/mail_post_defer/static/description/icon.png new file mode 100644 index 0000000..94a2bcd Binary files /dev/null and b/mail_post_defer/static/description/icon.png differ diff --git a/mail_post_defer/static/description/index.html b/mail_post_defer/static/description/index.html new file mode 100644 index 0000000..93a9d65 --- /dev/null +++ b/mail_post_defer/static/description/index.html @@ -0,0 +1,509 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Deferred Message Posting

+ +

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

+

This module enhances mail threads by using the mail queue by default.

+

Without this module, Odoo attempts to notify recipients of your message +immediately. If your mail server is slow or you have many followers, +this can mean a lot of time. Install this module and make Odoo more +snappy!

+

All emails will be kept in the outgoing queue by at least 30 seconds, +giving you some time to re-think what you wrote. During that time, you +can still delete the message and start again.

+
+

Important

+

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

+
+

Table of contents

+ +
+

Configuration

+

You usually don’t need to do anything. The module is configured +appropriately out of the box. Just make sure the following scheduled +actions are active:

+
    +
  • Mail: Email Queue Manager (mail.ir_cron_mail_scheduler_action)
  • +
  • Notification: Notify scheduled messages +(mail.ir_cron_send_scheduled_message)
  • +
+

The mail queue processing and scheduled messages notifications are +handled by cron jobs. This is normal Odoo behavior, not specific to this +module. However, since you will start using that queue for every message +posted by any user in any thread, both jobs are configured to execute +every minute by default.

+

You can still change that cadence after installing the module (although +it is not recommended). To do so:

+
    +
  1. Log in with an administrator user.
  2. +
  3. Activate developer mode.
  4. +
  5. Go to Settings > Technical > Automation > Scheduled Actions.
  6. +
  7. Find the cron(s) you want to adjust:
      +
    • Mail: Email Queue Manager - handles outgoing emails.
    • +
    • Notification: Notify scheduled messages - handles notifications.
    • +
    +
  8. +
  9. Lower down the frequency in the field Execute Every. Recommended: 1 +minute.
  10. +
+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to the form view of any record that has a mail thread. It can be a +partner, for example.
  2. +
  3. Post a message.
  4. +
+

The mail is now in the outgoing mail queue. It will be there for at +least 30 seconds. It will be really sent the next time the “Mail: Email +Queue Manager” cron job is executed.

+

While the message has not been yet sent:

+
    +
  1. Click the little envelope. You will see a paper airplane icon, +indicating it is still outgoing.
  2. +
  3. Hover over the message and click on ⠇ > 🗑️ Delete. Mails will not +be sent.
  4. +
+
+
+

Known issues / Roadmap

+ +
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Moduon
  • +
  • OERP Canada
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

Yajo

+

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

+

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

+
+
+
+
+ + diff --git a/mail_post_defer/tests/__init__.py b/mail_post_defer/tests/__init__.py new file mode 100644 index 0000000..f8340d8 --- /dev/null +++ b/mail_post_defer/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_install +from . import test_mail diff --git a/mail_post_defer/tests/test_install.py b/mail_post_defer/tests/test_install.py new file mode 100644 index 0000000..c83fd2d --- /dev/null +++ b/mail_post_defer/tests/test_install.py @@ -0,0 +1,12 @@ +# Copyright 2022-2023 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo.tests.common import TransactionCase + + +class InstallationCase(TransactionCase): + def test_cron_cadence(self): + """Test that the post_init_hook was properly executed.""" + cron = self.env.ref("mail.ir_cron_mail_scheduler_action") + cadence = cron.interval_number, cron.interval_type + self.assertEqual(cadence, (1, "minutes")) diff --git a/mail_post_defer/tests/test_mail.py b/mail_post_defer/tests/test_mail.py new file mode 100644 index 0000000..7122b3a --- /dev/null +++ b/mail_post_defer/tests/test_mail.py @@ -0,0 +1,309 @@ +# Copyright 2022-2023 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from datetime import datetime + +import freezegun +from lxml import html + +from odoo.exceptions import UserError +from odoo.tests import tagged + +from odoo.addons.mail.tests.common import MailCommon + + +class MailPostDeferCommon(MailCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.classPatch(cls.cr, "now", datetime.now) + cls._create_portal_user() + # Notify employee by email + cls.user_employee.notification_type = "email" + + +@freezegun.freeze_time("2023-01-02 10:00:00") +class MessagePostCase(MailPostDeferCommon): + def test_standard(self): + """A normal call just uses the queue by default.""" + with self.mock_mail_gateway(): + msg = self.partner_portal.message_post( + body="test body", + subject="test subject", + message_type="comment", + partner_ids=self.partner_employee.ids, + ) + schedules = self.env["mail.message.schedule"].search( + [ + ("mail_message_id", "=", msg.id), + ("scheduled_datetime", "=", "2023-01-02 10:00:30"), + ] + ) + self.assertEqual(len(schedules), 1) + self.assertNoMail(self.partner_employee) + + def test_forced_arg(self): + """A forced send via method argument is sent directly.""" + with self.mock_mail_gateway(): + self.partner_portal.message_post( + body="test body", + subject="test subject", + message_type="comment", + partner_ids=self.partner_employee.ids, + force_send=True, + ) + self.assertMailMail( + self.partner_employee, + "sent", + author=self.env.user.partner_id, + content="test body", + fields_values={"scheduled_date": False}, + ) + + def test_forced_context(self): + """A forced send via context is sent directly.""" + with self.mock_mail_gateway(): + self.partner_portal.with_context(mail_notify_force_send=True).message_post( + body="test body", + subject="test subject", + message_type="comment", + partner_ids=self.partner_employee.ids, + ) + self.assertMailMail( + self.partner_employee, + "sent", + author=self.env.user.partner_id, + content="test body", + fields_values={"scheduled_date": False}, + ) + + def test_msg_edit(self): + """Can update messages. + + Upstream Odoo allows only updating notes, regardless of their sent + status. We allow updating any message that is not sent yet. + """ + with self.mock_mail_gateway(): + msg = self.partner_portal.message_post( + body="test body", + subject="test subject", + message_type="comment", + partner_ids=self.partner_employee.ids, + subtype_xmlid="mail.mt_comment", + ) + schedules = self.env["mail.message.schedule"].search( + [ + ("mail_message_id", "=", msg.id), + ("scheduled_datetime", "=", "2023-01-02 10:00:30"), + ] + ) + self.assertEqual(len(schedules), 1) + self.assertNoMail(self.partner_employee) + # After 15 seconds, the user updates the message + with freezegun.freeze_time("2023-01-02 10:00:15"): + self.partner_portal._message_update_content(msg, body="new body") + schedules = self.env["mail.message.schedule"].search( + [ + ("mail_message_id", "=", msg.id), + ("scheduled_datetime", "=", "2023-01-02 10:00:45"), + ] + ) + self.assertEqual(len(schedules), 1) + self.assertNoMail(self.partner_employee) + # After a minute, the mail is created + with freezegun.freeze_time("2023-01-02 10:01:00"): + self.env["mail.message.schedule"]._send_notifications_cron() + self.assertMailMail( + self.partner_employee, + "outgoing", + author=self.env.user.partner_id, + content="new body", + ) + + def test_queued_msg_delete(self): + """A user can delete a message before it's sent.""" + with self.mock_mail_gateway(): + msg = self.partner_portal.message_post( + body="test body", + subject="test subject", + message_type="comment", + partner_ids=self.partner_employee.ids, + subtype_xmlid="mail.mt_comment", + ) + schedules = self.env["mail.message.schedule"].search( + [ + ("mail_message_id", "=", msg.id), + ("scheduled_datetime", "=", "2023-01-02 10:00:30"), + ] + ) + self.assertEqual(len(schedules), 1) + # Emulate user clicking on delete button and going through the + # `/mail/message/update_content` controller + self.partner_portal._message_update_content(msg, body="", partner_ids=[]) + self.env.flush_all() + self.assertFalse(schedules.exists()) + self.assertNoMail( + self.partner_employee, + author=self.env.user.partner_id, + ) + # One minute later, the cron has no mails to send + with freezegun.freeze_time("2023-01-02 10:01:00"): + self.env["mail.message.schedule"]._send_notifications_cron() + self.env["mail.mail"].process_email_queue() + self.assertNoMail( + self.partner_employee, + author=self.env.user.partner_id, + ) + + def test_no_sent_msg_delete(self): + """A user cannot delete a message after it's sent. + + Usually, the trash button will be hidden in UI if the message is sent. + However, the server-side protection is still important, because there + can be a race condition when the mail is sent in the background but + the user didn't refresh the view. + """ + with self.mock_mail_gateway(): + msg = self.partner_portal.message_post( + body="test body", + subject="test subject", + message_type="comment", + partner_ids=self.partner_employee.ids, + subtype_xmlid="mail.mt_comment", + ) + # We add tracking value to raise the userError in _message_update_content + field = self.env["ir.model.fields"]._get("mail.message", "body") + msg.tracking_value_ids = [ + ( + 0, + 0, + { + "field_id": field.id, + "old_value_char": "test body", + "new_value_char": "test body edited", + }, + ) + ] + # One minute later, the cron sends the mail + with freezegun.freeze_time("2023-01-02 10:01:00"): + self.env["mail.message.schedule"]._send_notifications_cron() + self.env["mail.mail"].process_email_queue() + self.assertMailMail( + self.partner_employee, + "sent", + author=self.env.user.partner_id, + content="test body", + ) + # Emulate user clicking on delete button and going through the + # `/mail/message/update_content` controller + with self.assertRaises(UserError): + self.partner_portal._message_update_content( + msg, body="", partner_ids=[] + ) + + def test_model_without_threading(self): + """When models don't inherit from mail.thread, they still work.""" + self.partner_portal.email = "portal@example.com" + with self.mock_mail_gateway(): + self.env["mail.thread"].message_notify( + author_id=self.partner_employee.id, + body="test body", + model="res.country", + partner_ids=(self.partner_employee | self.partner_portal).ids, + res_id=self.ref("base.es"), + notify_author_mention=False, + ) + self.assertNoMail(self.partner_employee | self.partner_portal) + # One minute later, the cron sends the mail + with freezegun.freeze_time("2023-01-02 10:01:00"): + self.env["mail.message.schedule"]._send_notifications_cron() + self.env["mail.mail"].process_email_queue() + self.assertMailMail( + self.partner_portal, + "sent", + author=self.partner_employee, + content="test body", + ) + # res.partner does not send mail because res.country does not inherit + # from mail.thread + self.assertNoMail(self.partner_employee) + # Safety belt to avoid false positives in this test + self.assertFalse(hasattr(self.env["res.country"], "_notify_thread")) + self.assertTrue(hasattr(self.env["res.partner"], "_notify_thread")) + + def test_button_access(self): + """A button is added to the email to access the record.""" + customer = self.env["res.partner"].create( + {"name": "Customer", "email": "customer@example.com"} + ) + with self.mock_mail_gateway(): + customer.message_post( + body="test body", + subject="test subject", + message_type="comment", + partner_ids=(self.partner_employee | customer).ids, + ) + self.assertNoMail(self.partner_employee | customer) + # After a minute, mails are sent + with freezegun.freeze_time("2023-01-02 10:01:00"): + self.env["mail.message.schedule"]._send_notifications_cron() + self.env["mail.mail"].process_email_queue() + # Employee has a button that grants them access + customer_link = customer._notify_get_action_link("view") + employee_mail = self.assertSentEmail( + self.env.user.partner_id, + self.partner_employee, + body_content="test body", + ) + self.assertEqual( + html.fromstring(employee_mail["body"]) + .xpath(f"//a[contains(@href, '{customer_link}')]")[0] + .text_content() + .strip(), + "View Contact", + ) + # Customer got the mail, but doesn't have access + customer_mail = self.assertSentEmail( + self.env.user.partner_id, + customer, + body_content="test body", + ) + self.assertFalse( + html.fromstring(customer_mail["body"]).xpath( + f"//a[contains(@href, '{customer_link}')]" + ) + ) + + +@tagged("-at_install", "post_install") +@freezegun.freeze_time("2023-01-02 10:00:00") +class AutomaticNotificationCase(MailPostDeferCommon): + """Check that automatic notifications are queued too. + + This is a separate case because some notifications require a + completely-loaded registry, so this case needs to run in post-install mode. + """ + + def test_assignation_mail(self): + """When assigning a record to a user, a notification is scheduled.""" + with self.mock_mail_gateway(): + self.partner_portal.user_id = self.user_employee.id + self.partner_portal.flush_recordset() + self.assertNoMail(self.partner_employee) + schedules = self.env["mail.message.schedule"].search( + [ + ("mail_message_id.res_id", "=", self.partner_portal.id), + ("mail_message_id.model", "=", "res.partner"), + ("scheduled_datetime", "=", "2023-01-02 10:00:30"), + ] + ) + self.assertEqual(len(schedules), 1) + # After a minute, the mail is sent + with freezegun.freeze_time("2023-01-02 10:01:00"): + self.env["mail.message.schedule"]._send_notifications_cron() + self.assertMailMail( + self.partner_employee, + "outgoing", + author=self.env.user.partner_id, + content="You have been assigned to the Contact Chell Gladys.", + )