From 5577cd2c4008cfaa5f0838117fcc7f84e66f7ef8 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 22 Mar 2017 16:16:44 +0100 Subject: [PATCH] [add] mail_digest --- mail_digest/README.rst | 90 ++++++++++ mail_digest/__init__.py | 1 + mail_digest/__openerp__.py | 22 +++ mail_digest/data/ir_cron.xml | 27 +++ mail_digest/demo/ir_ui_view.xml | 39 ++++ mail_digest/demo/mail_template.xml | 10 ++ mail_digest/i18n/de.po | 184 +++++++++++++++++++ mail_digest/i18n/mail_digest.pot | 175 ++++++++++++++++++ mail_digest/images/preview.png | Bin 0 -> 21023 bytes mail_digest/models/__init__.py | 2 + mail_digest/models/mail_digest.py | 183 +++++++++++++++++++ mail_digest/models/res_partner.py | 208 ++++++++++++++++++++++ mail_digest/security/ir.model.access.csv | 5 + mail_digest/static/description/icon.png | Bin 0 -> 9455 bytes mail_digest/templates/digest_default.xml | 47 +++++ mail_digest/tests/__init__.py | 3 + mail_digest/tests/test_digest.py | 147 +++++++++++++++ mail_digest/tests/test_partner_domains.py | 169 ++++++++++++++++++ mail_digest/tests/test_subtypes_conf.py | 136 ++++++++++++++ mail_digest/views/mail_digest_views.xml | 50 ++++++ mail_digest/views/partner_views.xml | 41 +++++ mail_digest/views/user_views.xml | 18 ++ 22 files changed, 1557 insertions(+) create mode 100644 mail_digest/README.rst create mode 100644 mail_digest/__init__.py create mode 100644 mail_digest/__openerp__.py create mode 100644 mail_digest/data/ir_cron.xml create mode 100644 mail_digest/demo/ir_ui_view.xml create mode 100644 mail_digest/demo/mail_template.xml create mode 100644 mail_digest/i18n/de.po create mode 100644 mail_digest/i18n/mail_digest.pot create mode 100644 mail_digest/images/preview.png create mode 100644 mail_digest/models/__init__.py create mode 100644 mail_digest/models/mail_digest.py create mode 100644 mail_digest/models/res_partner.py create mode 100644 mail_digest/security/ir.model.access.csv create mode 100644 mail_digest/static/description/icon.png create mode 100644 mail_digest/templates/digest_default.xml create mode 100644 mail_digest/tests/__init__.py create mode 100644 mail_digest/tests/test_digest.py create mode 100644 mail_digest/tests/test_partner_domains.py create mode 100644 mail_digest/tests/test_subtypes_conf.py create mode 100644 mail_digest/views/mail_digest_views.xml create mode 100644 mail_digest/views/partner_views.xml create mode 100644 mail_digest/views/user_views.xml diff --git a/mail_digest/README.rst b/mail_digest/README.rst new file mode 100644 index 00000000..41e83312 --- /dev/null +++ b/mail_digest/README.rst @@ -0,0 +1,90 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +========================= +Mail digest notifications +========================= + +Features +-------- + +This module allows users/partners to: + +* select "digest" mode in their notification settings +* with digest mode on select a frequency: "daily" or "weekly" +* configure specific rules per message subtype (enabled/disabled) + +to receive or to not receive any email notification for a given subtype. + +The preference tab on user's form will look like: + +.. image:: ./images/preview.png + + +Behavior +-------- + +When a partner with digest mode on is notified with a message of type email or an email +all the messages are collected inside a `mail.digest` container. + +A daily cron and a weekly cron will take care of creating a single email per each digest, +which will be sent as a standard email. + +If the message has a specific subtype, all of this will work only +if personal settings allow to receive notification for that specific subtype. +Specifically: + +* no record for type: message passes +* record disabled for type: message don't pass +* record enabled for type: message pass + +NOTE: under the hood the digest notification logic excludes followers to be notified, +since you really want to notify only mail.digest's partner. + +Known issues / Roadmap +====================== + +* take full control of message and email template. + +Right now the notification message and the digest mail itself is wrapped inside Odoo mail template. +We should be able to customize this easily. + + +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. + +Credits +======= + +Contributors +------------ + +* Simone Orsi + + +Funders +------- + +The development of this module has been financially supported by: `Fluxdock.io `_ + + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/mail_digest/__init__.py b/mail_digest/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/mail_digest/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mail_digest/__openerp__.py b/mail_digest/__openerp__.py new file mode 100644 index 00000000..169b9c5d --- /dev/null +++ b/mail_digest/__openerp__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + 'name': 'Mail digest', + 'summary': """Basic digest mail handling.""", + 'version': '9.0.1.0.0', + 'license': 'AGPL-3', + 'author': 'Camptocamp,Odoo Community Association (OCA)', + 'depends': [ + 'mail', + ], + 'data': [ + 'data/ir_cron.xml', + 'security/ir.model.access.csv', + 'views/mail_digest_views.xml', + 'views/partner_views.xml', + 'views/user_views.xml', + 'templates/digest_default.xml', + ], +} diff --git a/mail_digest/data/ir_cron.xml b/mail_digest/data/ir_cron.xml new file mode 100644 index 00000000..39c1087a --- /dev/null +++ b/mail_digest/data/ir_cron.xml @@ -0,0 +1,27 @@ + + + + + Digest mail process - daily + + 1 + days + -1 + + + + + + + Digest mail process - weekly + + 1 + weeks + -1 + + + + + + + diff --git a/mail_digest/demo/ir_ui_view.xml b/mail_digest/demo/ir_ui_view.xml new file mode 100644 index 00000000..bf7bff5d --- /dev/null +++ b/mail_digest/demo/ir_ui_view.xml @@ -0,0 +1,39 @@ + + + + + diff --git a/mail_digest/demo/mail_template.xml b/mail_digest/demo/mail_template.xml new file mode 100644 index 00000000..d53b6aac --- /dev/null +++ b/mail_digest/demo/mail_template.xml @@ -0,0 +1,10 @@ + + + + QWeb demo + qweb + + + QWeb demo email + + diff --git a/mail_digest/i18n/de.po b/mail_digest/i18n/de.po new file mode 100644 index 00000000..48d21581 --- /dev/null +++ b/mail_digest/i18n/de.po @@ -0,0 +1,184 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_digest +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 9.0c\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-05-23 07:08+0000\n" +"PO-Revision-Date: 2017-04-27 16:55+0200\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: \n" +"X-Generator: Poedit 1.8.9\n" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_create_uid +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_create_uid +msgid "Created by" +msgstr "Angelegt von" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_create_date +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_create_date +msgid "Created on" +msgstr "Angelegt am" + +#. module: mail_digest +#: selection:res.partner,notify_frequency:0 +msgid "Daily" +msgstr "Täglich" + +#. module: mail_digest +#: code:addons/mail_digest/models/mail_digest.py:106 +msgid "Daily update" +msgstr "täglich Übersicht" + +#. module: mail_digest +#: code:addons/mail_digest/models/res_partner.py:11 +#: model:ir.actions.act_window,name:mail_digest.action_digest_all +#: model:ir.ui.menu,name:mail_digest.menu_email_digest +#, python-format +msgid "Digest" +msgstr "Übersicht" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_display_name +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_display_name +msgid "Display Name" +msgstr "Angezeigter Name" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_enabled +msgid "Enabled" +msgstr "Aktivieren" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_frequency +#: model:ir.model.fields,field_description:mail_digest.field_res_partner_notify_frequency +msgid "Frequency" +msgstr "Häufigkeit" + +#. module: mail_digest +#: model:ir.ui.view,arch_db:mail_digest.default_digest_tmpl +msgid "Hello," +msgstr "Guten Tag" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_id +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_id +msgid "ID" +msgstr "ID" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest___last_update +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf___last_update +msgid "Last Modified on" +msgstr "Zuletzt geändert am" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_write_uid +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_write_uid +msgid "Last Updated by" +msgstr "Zuletzt aktualisiert durch" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_write_date +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_write_date +msgid "Last Updated on" +msgstr "Zuletzt aktualisiert am" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_mail_id +msgid "Mail" +msgstr "E-Mail" + +#. module: mail_digest +#: model:ir.model,name:mail_digest.model_mail_digest +#: model:ir.ui.view,arch_db:mail_digest.mail_digest_form +#: model:ir.ui.view,arch_db:mail_digest.mail_digest_tree +msgid "Mail digest" +msgstr "Übersicht" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_message_ids +#: model:ir.ui.view,arch_db:mail_digest.mail_digest_form +msgid "Messages" +msgstr "Mitteilungen" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_name +msgid "Name" +msgstr "Bezeichnung" + +#. module: mail_digest +#: model:ir.ui.view,arch_db:mail_digest.notification_form +msgid "Notification" +msgstr "Benachrichtigung" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_subtype_id +msgid "Notification type" +msgstr "Benachrichtigung" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_res_partner_notify_conf_ids +#: model:ir.ui.view,arch_db:mail_digest.notification_tree +msgid "Notifications" +msgstr "Benachrichtigungen" + +#. module: mail_digest +#: model:ir.model,name:mail_digest.model_res_partner +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_partner_id +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_partner_id +msgid "Partner" +msgstr "Partner" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_res_partner_disabled_notify_subtype_ids +msgid "Partner disabled subtypes" +msgstr "Partner disabled subtypes" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_res_partner_enabled_notify_subtype_ids +msgid "Partner enabled subtypes" +msgstr "Partner enabled subtypes" + +#. module: mail_digest +#: model:ir.model,name:mail_digest.model_partner_notification_conf +msgid "Partner notification configuration" +msgstr "Partner notification configuration" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_state +msgid "Status" +msgstr "Status" + +#. module: mail_digest +#: selection:res.partner,notify_frequency:0 +msgid "Weekly" +msgstr "Wöchentlich" + +#. module: mail_digest +#: code:addons/mail_digest/models/mail_digest.py:108 +msgid "Weekly update" +msgstr "wöchentlich Übersicht" + +#. module: mail_digest +#: sql_constraint:partner.notification.conf:0 +msgid "You can have only one configuration per subtype!" +msgstr "You can have only one configuration per subtype!" + +#~ msgid "Mail notification" +#~ msgstr "Mail notification" + +#~ msgid "Mail notifications control panel" +#~ msgstr "Mail notifications control panel" + +#~ msgid "Message subtypes" +#~ msgstr "Nachrichten-Subtyp" diff --git a/mail_digest/i18n/mail_digest.pot b/mail_digest/i18n/mail_digest.pot new file mode 100644 index 00000000..cdd22638 --- /dev/null +++ b/mail_digest/i18n/mail_digest.pot @@ -0,0 +1,175 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_digest +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 9.0c\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-05-23 07:08+0000\n" +"PO-Revision-Date: 2017-05-23 07:08+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_create_uid +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_create_uid +msgid "Created by" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_create_date +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_create_date +msgid "Created on" +msgstr "" + +#. module: mail_digest +#: selection:res.partner,notify_frequency:0 +msgid "Daily" +msgstr "" + +#. module: mail_digest +#: code:addons/mail_digest/models/mail_digest.py:106 +#, python-format +msgid "Daily update" +msgstr "" + +#. module: mail_digest +#: code:addons/mail_digest/models/res_partner.py:11 +#: model:ir.actions.act_window,name:mail_digest.action_digest_all +#: model:ir.ui.menu,name:mail_digest.menu_email_digest +#, python-format +msgid "Digest" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_display_name +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_display_name +msgid "Display Name" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_enabled +msgid "Enabled" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_frequency +#: model:ir.model.fields,field_description:mail_digest.field_res_partner_notify_frequency +msgid "Frequency" +msgstr "" + +#. module: mail_digest +#: model:ir.ui.view,arch_db:mail_digest.default_digest_tmpl +msgid "Hello," +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_id +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_id +msgid "ID" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest___last_update +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf___last_update +msgid "Last Modified on" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_write_uid +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_write_uid +msgid "Last Updated by" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_write_date +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_write_date +msgid "Last Updated on" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_mail_id +msgid "Mail" +msgstr "" + +#. module: mail_digest +#: model:ir.model,name:mail_digest.model_mail_digest +#: model:ir.ui.view,arch_db:mail_digest.mail_digest_form +#: model:ir.ui.view,arch_db:mail_digest.mail_digest_tree +msgid "Mail digest" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_message_ids +#: model:ir.ui.view,arch_db:mail_digest.mail_digest_form +msgid "Messages" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_name +msgid "Name" +msgstr "" + +#. module: mail_digest +#: model:ir.ui.view,arch_db:mail_digest.notification_form +msgid "Notification" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_subtype_id +msgid "Notification type" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_res_partner_notify_conf_ids +#: model:ir.ui.view,arch_db:mail_digest.notification_tree +msgid "Notifications" +msgstr "" + +#. module: mail_digest +#: model:ir.model,name:mail_digest.model_res_partner +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_partner_id +#: model:ir.model.fields,field_description:mail_digest.field_partner_notification_conf_partner_id +msgid "Partner" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_res_partner_disabled_notify_subtype_ids +msgid "Partner disabled subtypes" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_res_partner_enabled_notify_subtype_ids +msgid "Partner enabled subtypes" +msgstr "" + +#. module: mail_digest +#: model:ir.model,name:mail_digest.model_partner_notification_conf +msgid "Partner notification configuration" +msgstr "" + +#. module: mail_digest +#: model:ir.model.fields,field_description:mail_digest.field_mail_digest_state +msgid "Status" +msgstr "" + +#. module: mail_digest +#: selection:res.partner,notify_frequency:0 +msgid "Weekly" +msgstr "" + +#. module: mail_digest +#: code:addons/mail_digest/models/mail_digest.py:108 +#, python-format +msgid "Weekly update" +msgstr "" + +#. module: mail_digest +#: sql_constraint:partner.notification.conf:0 +msgid "You can have only one configuration per subtype!" +msgstr "" diff --git a/mail_digest/images/preview.png b/mail_digest/images/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..af147c9ad0d54be3de3f09491934a62b9d956552 GIT binary patch literal 21023 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 +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from openerp import fields, models, api, _ + +import logging + +logger = logging.getLogger('[mail_digest]') + + +class MailDigest(models.Model): + _name = 'mail.digest' + _description = 'Mail digest' + _order = 'create_date desc' + + name = fields.Char( + string="Name", + compute="_compute_name", + readonly=True, + ) + # maybe we can retrieve the from messages? + partner_id = fields.Many2one( + string='Partner', + comodel_name='res.partner', + readonly=True, + required=True, + ondelete='cascade', + ) + frequency = fields.Selection( + related='partner_id.notify_frequency', + readonly=True, + ) + message_ids = fields.Many2many( + comodel_name='mail.message', + string='Messages' + ) + # TODO: take care of `auto_delete` feature + mail_id = fields.Many2one( + 'mail.mail', + 'Mail', + ondelete='set null', + ) + state = fields.Selection(related='mail_id.state') + + @api.multi + @api.depends("partner_id", "partner_id.notify_frequency") + def _compute_name(self): + for rec in self: + rec.name = u'{} - {}'.format( + rec.partner_id.name, rec._get_subject()) + + @api.model + def create_or_update(self, partners, message, subtype_id=None): + subtype_id = subtype_id or message.subtype_id + for partner in partners: + digest = self._get_or_create_by_partner(partner, message) + digest.message_ids |= message + return True + + @api.model + def _get_by_partner(self, partner, mail_id=False): + domain = [ + ('partner_id', '=', partner.id), + ('mail_id', '=', mail_id), + ] + return self.search(domain, limit=1) + + @api.model + def _get_or_create_by_partner(self, partner, message=None, mail_id=False): + existing = self._get_by_partner(partner, mail_id=mail_id) + if existing: + return existing + values = {'partner_id': partner.id, } + return self.create(values) + + @api.model + def _message_group_by_key(self, msg): + return msg.subtype_id.id + + @api.multi + def _message_group_by(self): + self.ensure_one() + grouped = {} + for msg in self.message_ids: + grouped.setdefault(self._message_group_by_key(msg), []).append(msg) + return grouped + + def _get_template(self): + # TODO: move this to a configurable field + return self.env.ref('mail_digest.default_digest_tmpl') + + def _get_site_name(self): + # default to company + name = self.env.user.company_id.name + if 'website' in self.env: + try: + ws = self.env['website'].get_current_website() + except RuntimeError: + # RuntimeError: object unbound -> no website request + ws = None + if ws: + name = ws.name + return name + + @api.multi + def _get_subject(self): + # TODO: shall we move this to computed field? + self.ensure_one() + subject = self._get_site_name() + ' ' + if self.partner_id.notify_frequency == 'daily': + subject += _('Daily update') + elif self.partner_id.notify_frequency == 'weekly': + subject += _('Weekly update') + return subject + + @api.multi + def _get_template_values(self): + self.ensure_one() + subject = self._get_subject() + template_values = { + 'digest': self, + 'subject': subject, + 'grouped_messages': self._message_group_by(), + 'base_url': + self.env['ir.config_parameter'].get_param('web.base.url'), + } + return template_values + + @api.multi + def _get_email_values(self, template=None): + self.ensure_one() + template = template or self._get_template() + subject = self._get_subject() + template_values = self._get_template_values() + values = { + 'email_from': self.env.user.company_id.email, + 'recipient_ids': [(4, self.partner_id.id)], + 'subject': subject, + 'body_html': template.with_context( + **self._template_context() + ).render(template_values), + } + return values + + def _create_mail_context(self): + return { + 'notify_only_recipients': True, + } + + @api.multi + def _template_context(self): + self.ensure_one() + return { + 'lang': self.partner_id.lang, + } + + @api.multi + def create_email(self, template=None): + mail_model = self.env['mail.mail'].with_context( + **self._create_mail_context()) + created = [] + for item in self: + if not item.message_ids: + # useless to create a mail for a digest w/ messages + # messages could be deleted by admin for instance. + continue + values = item.with_context( + **item._template_context() + )._get_email_values(template=template) + item.mail_id = mail_model.create(values) + created.append(item.id) + if created: + logger.info('Create email for digest IDS=%s', str(created)) + + @api.model + def process(self, frequency='daily', domain=None): + if not domain: + domain = [ + ('mail_id', '=', False), + ('partner_id.notify_frequency', '=', frequency), + ] + self.search(domain).create_email() diff --git a/mail_digest/models/res_partner.py b/mail_digest/models/res_partner.py new file mode 100644 index 00000000..b66809f0 --- /dev/null +++ b/mail_digest/models/res_partner.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from openerp import models, fields, api, _ + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + notify_email = fields.Selection(selection_add=[('digest', _('Digest'))]) + notify_frequency = fields.Selection( + string='Frequency', + selection=[ + ('daily', 'Daily'), + ('weekly', 'Weekly') + ], + default='weekly', + required=True, + ) + notify_conf_ids = fields.One2many( + string='Notifications', + inverse_name='partner_id', + comodel_name='partner.notification.conf', + ) + enabled_notify_subtype_ids = fields.Many2many( + string='Partner enabled subtypes', + comodel_name='mail.message.subtype', + compute='_compute_enabled_notify_subtype_ids', + search='_search_enabled_notify_subtype_ids', + ) + disabled_notify_subtype_ids = fields.Many2many( + string='Partner disabled subtypes', + comodel_name='mail.message.subtype', + compute='_compute_disabled_notify_subtype_ids', + search='_search_disabled_notify_subtype_ids', + ) + + @api.multi + def _compute_notify_subtypes(self, enabled): + self.ensure_one() + query = ( + 'SELECT subtype_id FROM partner_notification_conf ' + 'WHERE partner_id=%s AND enabled = %s' + ) + self.env.cr.execute( + query, (self.id, enabled)) + return [x[0] for x in self.env.cr.fetchall()] + + @api.multi + @api.depends('notify_conf_ids.subtype_id') + def _compute_enabled_notify_subtype_ids(self): + for partner in self: + partner.enabled_notify_subtype_ids = \ + partner._compute_notify_subtypes(True) + + @api.multi + @api.depends('notify_conf_ids.subtype_id') + def _compute_disabled_notify_subtype_ids(self): + for partner in self: + partner.disabled_notify_subtype_ids = \ + partner._compute_notify_subtypes(False) + + def _search_notify_subtype_ids_domain(self, operator, value, enabled): + if operator in ('in', 'not in') and \ + not isinstance(value, (tuple, list)): + value = [value, ] + conf_value = value + if isinstance(conf_value, int): + # we search conf records always w/ 'in' + conf_value = [conf_value] + _value = self.env['partner.notification.conf'].search([ + ('subtype_id', 'in', conf_value), + ('enabled', '=', enabled), + ]).mapped('partner_id').ids + return [('id', operator, _value)] + + def _search_enabled_notify_subtype_ids(self, operator, value): + return self._search_notify_subtype_ids_domain( + operator, value, True) + + def _search_disabled_notify_subtype_ids(self, operator, value): + return self._search_notify_subtype_ids_domain( + operator, value, False) + + @api.multi + def _notify(self, message, force_send=False, user_signature=True): + """Override to delegate domain generation.""" + # notify_by_email + email_domain = self._get_notify_by_email_domain(message) + # `sudo` from original odoo method + # the reason should be that anybody can write messages to a partner + # and you really want to find all ppl to be notified + partners = self.sudo().search(email_domain) + partners._notify_by_email( + message, force_send=force_send, user_signature=user_signature) + # notify_by_digest + digest_domain = self._get_notify_by_email_domain( + message, digest=True) + partners = self.sudo().search(digest_domain) + partners._notify_by_digest(message) + + # notify_by_chat + self._notify_by_chat(message) + return True + + @api.multi + def _notify_by_digest(self, message): + message_sudo = message.sudo() + if not message_sudo.message_type == 'email': + return + self.env['mail.digest'].sudo().create_or_update(self, message) + + @api.model + def _get_notify_by_email_domain(self, message, digest=False): + """Return domain to collect partners to be notified by email. + + :param message: instance of mail.message + :param digest: include/exclude digest enabled partners + + NOTE: since mail.mail inherits from mail.message + this method is called even when + we create the final email for mail.digest object. + Here we introduce a new context flag `notify_only_recipients` + to explicitely retrieve only partners among message's recipients. + """ + + message_sudo = message.sudo() + channels = message.channel_ids.filtered( + lambda channel: channel.email_send) + email = message_sudo.author_id \ + and message_sudo.author_id.email or message.email_from + + ids = self.ids + if self.env.context.get('notify_only_recipients'): + ids = [x for x in ids if x in message.partner_ids.ids] + domain = [ + '|', + ('id', 'in', ids), + ('channel_ids', 'in', channels.ids), + ('email', '!=', email) + ] + if not digest: + domain.append(('notify_email', 'not in', ('none', 'digest'))) + else: + domain.append(('notify_email', '=', 'digest')) + if message.subtype_id: + domain.extend(self._get_domain_subtype_leaf(message.subtype_id)) + return domain + + @api.model + def _get_domain_subtype_leaf(self, subtype): + return [ + '|', + ('disabled_notify_subtype_ids', 'not in', (subtype.id, )), + ('enabled_notify_subtype_ids', 'in', (subtype.id, )), + ] + + @api.multi + def _notify_update_subtype(self, subtype, enable): + self.ensure_one() + exists = self.env['partner.notification.conf'].search([ + ('subtype_id', '=', subtype.id), + ('partner_id', '=', self.id) + ], limit=1) + if exists: + exists.enabled = enable + else: + self.write({ + 'notify_conf_ids': [ + (0, 0, {'enabled': enable, 'subtype_id': subtype.id})] + }) + + @api.multi + def _notify_enable_subtype(self, subtype): + self._notify_update_subtype(subtype, True) + + @api.multi + def _notify_disable_subtype(self, subtype): + self._notify_update_subtype(subtype, False) + + +class PartnerNotificationConf(models.Model): + """Hold partner's single notification configuration.""" + _name = 'partner.notification.conf' + _description = 'Partner notification configuration' + # TODO: add friendly onchange to not yield errors when editin via UI + _sql_constraints = [ + ('unique_partner_subtype_conf', + 'unique (partner_id,subtype_id)', + 'You can have only one configuration per subtype!') + ] + + partner_id = fields.Many2one( + string='Partner', + comodel_name='res.partner', + readonly=True, + required=True, + ondelete='cascade', + index=True, + ) + subtype_id = fields.Many2one( + 'mail.message.subtype', + 'Notification type', + ondelete='cascade', + required=True, + ) + enabled = fields.Boolean(default=True, index=True) diff --git a/mail_digest/security/ir.model.access.csv b/mail_digest/security/ir.model.access.csv new file mode 100644 index 00000000..43892a41 --- /dev/null +++ b/mail_digest/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mail_digest_all,mail.digest.all,model_mail_digest,,1,0,0,0 +access_partner_notification_conf_all,partner.notification.all,model_partner_notification_conf,,1,0,0,0 +access_mail_digest_system,mail.digest.all,model_mail_digest,base.group_system,1,1,1,1 +access_partner_notification_conf_system,partner.notification.all,model_partner_notification_conf,base.group_system,1,1,1,1 diff --git a/mail_digest/static/description/icon.png b/mail_digest/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/mail_digest/templates/digest_default.xml b/mail_digest/templates/digest_default.xml new file mode 100644 index 00000000..38e9eda9 --- /dev/null +++ b/mail_digest/templates/digest_default.xml @@ -0,0 +1,47 @@ + + + + + + + diff --git a/mail_digest/tests/__init__.py b/mail_digest/tests/__init__.py new file mode 100644 index 00000000..1e4552ba --- /dev/null +++ b/mail_digest/tests/__init__.py @@ -0,0 +1,3 @@ +from . import test_digest +from . import test_partner_domains +from . import test_subtypes_conf diff --git a/mail_digest/tests/test_digest.py b/mail_digest/tests/test_digest.py new file mode 100644 index 00000000..a7460c97 --- /dev/null +++ b/mail_digest/tests/test_digest.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from openerp.tests.common import TransactionCase + + +class DigestCase(TransactionCase): + + def setUp(self): + super(DigestCase, self).setUp() + self.partner_model = self.env['res.partner'] + self.message_model = self.env['mail.message'] + self.subtype_model = self.env['mail.message.subtype'] + self.digest_model = self.env['mail.digest'] + self.conf_model = self.env['partner.notification.conf'] + + self.partner1 = self.partner_model.with_context( + tracking_disable=1).create({ + 'name': 'Partner 1', + 'email': 'partner1@test.foo.com', + }) + self.partner2 = self.partner_model.with_context( + tracking_disable=1).create({ + 'name': 'Partner 2', + 'email': 'partner2@test.foo.com', + }) + self.partner3 = self.partner_model.with_context( + tracking_disable=1).create({ + 'name': 'Partner 3', + 'email': 'partner3@test.foo.com', + }) + self.subtype1 = self.subtype_model.create({'name': 'Type 1'}) + self.subtype2 = self.subtype_model.create({'name': 'Type 2'}) + + def test_get_or_create_digest(self): + message1 = self.message_model.create({ + 'body': 'My Body 1', + 'subtype_id': self.subtype1.id, + }) + message2 = self.message_model.create({ + 'body': 'My Body 2', + 'subtype_id': self.subtype2.id, + }) + # 2 messages, 1 digest container + dig1 = self.digest_model._get_or_create_by_partner( + self.partner1, message1) + dig2 = self.digest_model._get_or_create_by_partner( + self.partner1, message2) + self.assertEqual(dig1, dig2) + + def test_create_or_update_digest(self): + partners = self.partner_model + partners |= self.partner1 + partners |= self.partner2 + message1 = self.message_model.create({ + 'body': 'My Body 1', + 'subtype_id': self.subtype1.id, + }) + message2 = self.message_model.create({ + 'body': 'My Body 2', + 'subtype_id': self.subtype2.id, + }) + # partner 1 + self.digest_model.create_or_update(self.partner1, message1) + self.digest_model.create_or_update(self.partner1, message2) + p1dig = self.digest_model._get_or_create_by_partner(self.partner1) + self.assertIn(message1, p1dig.message_ids) + self.assertIn(message2, p1dig.message_ids) + # partner 2 + self.digest_model.create_or_update(self.partner2, message1) + self.digest_model.create_or_update(self.partner2, message2) + p2dig = self.digest_model._get_or_create_by_partner(self.partner2) + self.assertIn(message1, p2dig.message_ids) + self.assertIn(message2, p2dig.message_ids) + + def test_notify_partner_digest(self): + message = self.message_model.create({ + 'body': 'My Body 1', + 'subtype_id': self.subtype1.id, + }) + self.partner1.notify_email = 'digest' + # notify partner + self.partner1._notify(message) + # we should find the message in its digest + dig1 = self.digest_model._get_or_create_by_partner( + self.partner1, message) + self.assertIn(message, dig1.message_ids) + + def test_notify_partner_digest_followers(self): + self.partner3.message_subscribe(self.partner2.ids) + self.partner1.notify_email = 'digest' + self.partner2.notify_email = 'digest' + partners = self.partner1 + self.partner2 + message = self.message_model.create({ + 'body': 'My Body 1', + 'subtype_id': self.subtype1.id, + 'res_id': self.partner3.id, + 'model': 'res.partner', + 'partner_ids': [(4, self.partner1.id)] + }) + # notify partners + partners._notify(message) + # we should find the a digest for each partner + dig1 = self.digest_model._get_by_partner(self.partner1) + dig2 = self.digest_model._get_by_partner(self.partner2) + # and the message in them + self.assertIn(message, dig1.message_ids) + self.assertIn(message, dig2.message_ids) + # now we exclude followers + dig1.unlink() + dig2.unlink() + partners.with_context(notify_only_recipients=1)._notify(message) + # we should find the a digest for each partner + self.assertTrue(self.digest_model._get_by_partner(self.partner1)) + self.assertFalse(self.digest_model._get_by_partner(self.partner2)) + + def _create_for_partner(self, partner): + messages = {} + for type_id in (self.subtype1.id, self.subtype2.id): + for k in xrange(1, 3): + key = '{}_{}'.format(type_id, k) + messages[key] = self.message_model.create({ + 'subject': 'My Subject {}'.format(key), + 'body': 'My Body {}'.format(key), + 'subtype_id': type_id, + }) + self.digest_model.create_or_update( + partner, messages[key]) + return self.digest_model._get_or_create_by_partner(partner) + + def test_digest_group_messages(self): + dig = self._create_for_partner(self.partner1) + grouped = dig._message_group_by() + for type_id in (self.subtype1.id, self.subtype2.id): + self.assertIn(type_id, grouped) + self.assertEqual(len(grouped[type_id]), 2) + + def test_digest_mail_values(self): + dig = self._create_for_partner(self.partner1) + values = dig._get_email_values() + expected = ('recipient_ids', 'subject', 'body_html') + for k in expected: + self.assertIn(k, values) + + self.assertEqual(self.env.user.company_id.email, values['email_from']) + self.assertEqual([(4, self.partner1.id)], values['recipient_ids']) diff --git a/mail_digest/tests/test_partner_domains.py b/mail_digest/tests/test_partner_domains.py new file mode 100644 index 00000000..09e5951c --- /dev/null +++ b/mail_digest/tests/test_partner_domains.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from openerp.tests.common import TransactionCase + + +class PartnerDomainCase(TransactionCase): + + def setUp(self): + super(PartnerDomainCase, self).setUp() + self.partner_model = self.env['res.partner'] + self.message_model = self.env['mail.message'] + self.subtype_model = self.env['mail.message.subtype'] + + self.partner1 = self.partner_model.with_context( + tracking_disable=1).create({ + 'name': 'Partner 1', + 'email': 'partner1@test.foo.com', + }) + self.partner2 = self.partner_model.with_context( + tracking_disable=1).create({ + 'name': 'Partner 2', + 'email': 'partner2@test.foo.com', + }) + self.partner3 = self.partner_model.with_context( + tracking_disable=1).create({ + 'name': 'Partner 3', + 'email': 'partner3@test.foo.com', + }) + self.subtype1 = self.subtype_model.create({'name': 'Type 1'}) + self.subtype2 = self.subtype_model.create({'name': 'Type 2'}) + + def _assert_found(self, domain, not_found=False, partner=None): + partner = partner or self.partner1 + if not_found: + self.assertNotIn(partner, self.partner_model.search(domain)) + else: + self.assertIn(partner, self.partner_model.search(domain)) + + def test_notify_domains_always(self): + # we don't set recipients + # because we call `_get_notify_by_email_domain` directly + message = self.message_model.create({'body': 'My Body', }) + partner = self.partner1 + partner.notify_email = 'always' + domain = partner._get_notify_by_email_domain(message) + self._assert_found(domain) + domain = partner._get_notify_by_email_domain(message, digest=1) + self._assert_found(domain, not_found=1) + + def test_notify_domains_only_recipients(self): + # we don't set recipients + # because we call `_get_notify_by_email_domain` directly + self.partner1.notify_email = 'always' + self.partner2.notify_email = 'always' + partners = self.partner1 + self.partner2 + # followers + self.partner3.message_subscribe(self.partner2.ids) + # partner1 is the only recipient + message = self.message_model.create({ + 'body': 'My Body', + 'res_id': self.partner3.id, + 'model': 'res.partner', + 'partner_ids': [(4, self.partner1.id)] + }) + domain = partners._get_notify_by_email_domain(message) + # we find both of them since partner2 is a follower + self._assert_found(domain) + self._assert_found(domain, partner=self.partner2) + # no one here in digest mode + domain = partners._get_notify_by_email_domain(message, digest=1) + self._assert_found(domain, not_found=1) + self._assert_found(domain, not_found=1, partner=self.partner2) + + # include only recipients + domain = partners.with_context( + notify_only_recipients=1)._get_notify_by_email_domain(message) + self._assert_found(domain) + self._assert_found(domain, partner=self.partner2, not_found=1) + + def test_notify_domains_digest(self): + # we don't set recipients + # because we call `_get_notify_by_email_domain` directly + message = self.message_model.create({'body': 'My Body', }) + partner = self.partner1 + partner.notify_email = 'digest' + domain = partner._get_notify_by_email_domain(message) + self._assert_found(domain, not_found=1) + domain = partner._get_notify_by_email_domain(message, digest=1) + self._assert_found(domain) + + def test_notify_domains_none(self): + message = self.message_model.create({'body': 'My Body', }) + partner = self.partner1 + partner.notify_email = 'none' + domain = partner._get_notify_by_email_domain(message) + self._assert_found(domain, not_found=1) + domain = partner._get_notify_by_email_domain(message, digest=1) + self._assert_found(domain, not_found=1) + + def test_notify_domains_match_type_digest(self): + # Test message subtype matches partner settings. + # The partner can have several `partner.notification.conf` records. + # Each records establish notification rules by type. + # If you don't have any record in it, you allow all subtypes. + # Record `typeX` with `enable=True` enables notification for `typeX`. + # Record `typeX` with `enable=False` disables notification for `typeX`. + + partner = self.partner1 + # enable digest + partner.notify_email = 'digest' + message_t1 = self.message_model.create({ + 'body': 'My Body', + 'subtype_id': self.subtype1.id, + }) + message_t2 = self.message_model.create({ + 'body': 'My Body', + 'subtype_id': self.subtype2.id, + }) + # enable subtype on partner + partner._notify_enable_subtype(self.subtype1) + domain = partner._get_notify_by_email_domain( + message_t1, digest=True) + # notification enabled: we find the partner. + self._assert_found(domain) + # for subtype2 we don't have any explicit rule: we find the partner + domain = partner._get_notify_by_email_domain( + message_t2, digest=True) + self._assert_found(domain) + # enable subtype2: find the partner anyway + partner._notify_enable_subtype(self.subtype2) + domain = partner._get_notify_by_email_domain( + message_t2, digest=True) + self._assert_found(domain) + # disable subtype2: we don't find the partner anymore + partner._notify_disable_subtype(self.subtype2) + domain = partner._get_notify_by_email_domain( + message_t2, digest=True) + self._assert_found(domain, not_found=1) + + def test_notify_domains_match_type_always(self): + message_t1 = self.message_model.create({ + 'body': 'My Body', + 'subtype_id': self.subtype1.id, + }) + message_t2 = self.message_model.create({ + 'body': 'My Body', + 'subtype_id': self.subtype2.id, + }) + # enable always + partner = self.partner1 + partner.notify_email = 'always' + # enable subtype on partner + partner._notify_enable_subtype(self.subtype1) + domain = partner._get_notify_by_email_domain(message_t1) + # notification enabled: we find the partner. + self._assert_found(domain) + # for subtype2 we don't have any explicit rule: we find the partner + domain = partner._get_notify_by_email_domain(message_t2) + self._assert_found(domain) + # enable subtype2: find the partner anyway + partner._notify_enable_subtype(self.subtype2) + domain = partner._get_notify_by_email_domain(message_t2) + self._assert_found(domain) + # disable subtype2: we don't find the partner anymore + partner._notify_disable_subtype(self.subtype2) + domain = partner._get_notify_by_email_domain(message_t2) + self._assert_found(domain, not_found=1) diff --git a/mail_digest/tests/test_subtypes_conf.py b/mail_digest/tests/test_subtypes_conf.py new file mode 100644 index 00000000..075a48c4 --- /dev/null +++ b/mail_digest/tests/test_subtypes_conf.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from openerp.tests.common import TransactionCase + + +class SubtypesCase(TransactionCase): + + def setUp(self): + super(SubtypesCase, self).setUp() + self.partner_model = self.env['res.partner'] + self.message_model = self.env['mail.message'] + self.subtype_model = self.env['mail.message.subtype'] + + self.partner1 = self.partner_model.with_context( + tracking_disable=1).create({ + 'name': 'Partner 1!', + 'email': 'partner1@test.foo.com', + }) + self.partner2 = self.partner_model.with_context( + tracking_disable=1).create({ + 'name': 'Partner 2!', + 'email': 'partner2@test.foo.com', + }) + self.subtype1 = self.subtype_model.create({'name': 'Type 1'}) + self.subtype2 = self.subtype_model.create({'name': 'Type 2'}) + + def _test_subtypes_rel(self): + # setup: + # t1, t2 enabled + # t3 disabled + # t4 no conf + self.subtype3 = self.subtype_model.create({'name': 'Type 3'}) + self.subtype4 = self.subtype_model.create({'name': 'Type 4'}) + # enable t1 t2 + self.partner1._notify_enable_subtype(self.subtype1) + self.partner1._notify_enable_subtype(self.subtype2) + # disable t3 + self.partner1._notify_disable_subtype(self.subtype3) + + def test_partner_computed_subtype(self): + self._test_subtypes_rel() + # check computed fields + self.assertIn( + self.subtype1, self.partner1.enabled_notify_subtype_ids) + self.assertNotIn( + self.subtype1, self.partner1.disabled_notify_subtype_ids) + self.assertIn( + self.subtype2, self.partner1.enabled_notify_subtype_ids) + self.assertNotIn( + self.subtype2, self.partner1.disabled_notify_subtype_ids) + self.assertIn( + self.subtype3, self.partner1.disabled_notify_subtype_ids) + self.assertNotIn( + self.subtype3, self.partner1.enabled_notify_subtype_ids) + self.assertNotIn( + self.subtype4, + self.partner1.enabled_notify_subtype_ids) + self.assertNotIn( + self.subtype4, + self.partner1.disabled_notify_subtype_ids) + + def test_partner_find_by_subtype_incl(self): + self._test_subtypes_rel() + domain = [( + 'enabled_notify_subtype_ids', + 'in', (self.subtype1.id, self.subtype2.id), + )] + self.assertIn( + self.partner1, + self.partner_model.search(domain) + ) + domain = [( + 'disabled_notify_subtype_ids', 'in', self.subtype3.id, + )] + self.assertIn( + self.partner1, + self.partner_model.search(domain) + ) + domain = [( + 'enabled_notify_subtype_ids', 'in', (self.subtype3.id, ), + )] + self.assertNotIn( + self.partner1, + self.partner_model.search(domain) + ) + domain = [( + 'enabled_notify_subtype_ids', 'in', (self.subtype4.id, ), + )] + self.assertNotIn( + self.partner1, + self.partner_model.search(domain) + ) + domain = [( + 'disabled_notify_subtype_ids', 'in', (self.subtype4.id, ), + )] + self.assertNotIn( + self.partner1, + self.partner_model.search(domain) + ) + + def test_partner_find_by_subtype_escl(self): + self._test_subtypes_rel() + domain = [( + 'enabled_notify_subtype_ids', + 'not in', (self.subtype4.id, ), + )] + self.assertIn( + self.partner1, + self.partner_model.search(domain) + ) + domain = [( + 'disabled_notify_subtype_ids', + 'not in', (self.subtype4.id, ), + )] + self.assertIn( + self.partner1, + self.partner_model.search(domain) + ) + domain = [( + 'enabled_notify_subtype_ids', + 'not in', (self.subtype3.id, ), + )] + self.assertIn( + self.partner1, + self.partner_model.search(domain) + ) + domain = [( + 'disabled_notify_subtype_ids', + 'not in', (self.subtype1.id, self.subtype2.id), + )] + self.assertIn( + self.partner1, + self.partner_model.search(domain) + ) diff --git a/mail_digest/views/mail_digest_views.xml b/mail_digest/views/mail_digest_views.xml new file mode 100644 index 00000000..a85f2705 --- /dev/null +++ b/mail_digest/views/mail_digest_views.xml @@ -0,0 +1,50 @@ + + + + + + mail_digest mail.digest.tree + mail.digest + + + + + + + + + + + + mail_digest mail.digest.form + mail.digest + +
+ + + + + + + + + + +
+
+
+ + + Digest + mail.digest + form + form,tree + + + + + +
+
diff --git a/mail_digest/views/partner_views.xml b/mail_digest/views/partner_views.xml new file mode 100644 index 00000000..0629a819 --- /dev/null +++ b/mail_digest/views/partner_views.xml @@ -0,0 +1,41 @@ + + + + + + mail.notifications res.partner.form + res.partner + + + + + + + + + + partner.notification.conf form + partner.notification.conf + +
+ + + + +
+
+
+ + + partner.notification.conf tree + partner.notification.conf + + + + + + + + +
+
diff --git a/mail_digest/views/user_views.xml b/mail_digest/views/user_views.xml new file mode 100644 index 00000000..88b44fd8 --- /dev/null +++ b/mail_digest/views/user_views.xml @@ -0,0 +1,18 @@ + + + + + + mail.notifications res.users.form + res.users + + + + + + + + + + +