Browse Source
Merge pull request #158 from camptocamp/add-mail_digest
Merge pull request #158 from camptocamp/add-mail_digest
[add] mail_digestpull/184/head
Pedro M. Baeza
8 years ago
committed by
GitHub
22 changed files with 1557 additions and 0 deletions
-
90mail_digest/README.rst
-
1mail_digest/__init__.py
-
22mail_digest/__openerp__.py
-
27mail_digest/data/ir_cron.xml
-
39mail_digest/demo/ir_ui_view.xml
-
10mail_digest/demo/mail_template.xml
-
184mail_digest/i18n/de.po
-
175mail_digest/i18n/mail_digest.pot
-
BINmail_digest/images/preview.png
-
2mail_digest/models/__init__.py
-
183mail_digest/models/mail_digest.py
-
208mail_digest/models/res_partner.py
-
5mail_digest/security/ir.model.access.csv
-
BINmail_digest/static/description/icon.png
-
47mail_digest/templates/digest_default.xml
-
3mail_digest/tests/__init__.py
-
147mail_digest/tests/test_digest.py
-
169mail_digest/tests/test_partner_domains.py
-
136mail_digest/tests/test_subtypes_conf.py
-
50mail_digest/views/mail_digest_views.xml
-
41mail_digest/views/partner_views.xml
-
18mail_digest/views/user_views.xml
@ -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 |
||||
|
<https://github.com/OCA/social/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 <simone.orsi@camptocamp.com> |
||||
|
|
||||
|
|
||||
|
Funders |
||||
|
------- |
||||
|
|
||||
|
The development of this module has been financially supported by: `Fluxdock.io <https://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. |
@ -0,0 +1 @@ |
|||||
|
from . import models |
@ -0,0 +1,22 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com> |
||||
|
# 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', |
||||
|
], |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<odoo> |
||||
|
<data noupdate="1"> |
||||
|
<record forcecreate="True" id="ir_cron_mail_digest_daily_action" model="ir.cron"> |
||||
|
<field name="name">Digest mail process - daily</field> |
||||
|
<field name="user_id" ref="base.user_root"/> |
||||
|
<field name="interval_number">1</field> |
||||
|
<field name="interval_type">days</field> |
||||
|
<field name="numbercall">-1</field> |
||||
|
<field eval="False" name="doall"/> |
||||
|
<field eval="'mail.digest'" name="model"/> |
||||
|
<field eval="'process'" name="function"/> |
||||
|
<field eval="'()'" name="args"/> |
||||
|
</record> |
||||
|
<record forcecreate="True" id="ir_cron_mail_digest_weekly_action" model="ir.cron"> |
||||
|
<field name="name">Digest mail process - weekly</field> |
||||
|
<field name="user_id" ref="base.user_root"/> |
||||
|
<field name="interval_number">1</field> |
||||
|
<field name="interval_type">weeks</field> |
||||
|
<field name="numbercall">-1</field> |
||||
|
<field eval="False" name="doall"/> |
||||
|
<field eval="'mail.digest'" name="model"/> |
||||
|
<field eval="'process'" name="function"/> |
||||
|
<field eval="'(\'weekly\',)'" name="args"/> |
||||
|
</record> |
||||
|
</data> |
||||
|
</odoo> |
@ -0,0 +1,39 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<odoo> |
||||
|
<template id="view_email_template_corporate_identity"> |
||||
|
<body> |
||||
|
<html> |
||||
|
<img style="float: right" t-attf-src="data:image;base64,{{env.user.company_id.logo}}" /> |
||||
|
<!-- if some template calling us sets this variable, |
||||
|
we print a h1 tag /--> |
||||
|
<h1 t-if="email_heading"><t t-esc="email_heading" /></h1> |
||||
|
<t t-raw="0" /> |
||||
|
<!-- use some standard footer if the user doesn't have |
||||
|
a signature /--> |
||||
|
<footer t-if="not email_use_user_signature"> |
||||
|
<p> |
||||
|
<a t-att-href="env.user.company_id.website"> |
||||
|
<t t-esc="env.user.company_id.name" /> |
||||
|
</a> |
||||
|
</p> |
||||
|
<p><t t-esc="env.user.company_id.phone" /></p> |
||||
|
</footer> |
||||
|
<footer t-if="email_use_user_signature"> |
||||
|
<t t-raw="env.user.signature" /> |
||||
|
</footer> |
||||
|
</html> |
||||
|
</body> |
||||
|
</template> |
||||
|
<template id="view_email_template_demo1"> |
||||
|
<!-- because we can simply call the ci here, we don't need to |
||||
|
repeat it /--> |
||||
|
<t t-call="email_template_qweb.view_email_template_corporate_identity"> |
||||
|
<!-- the template we call uses this as title if we set it /--> |
||||
|
<t t-set="email_heading" t-value="email_template.subject" /> |
||||
|
<h2>Dear <t t-esc="object.name" />,</h2> |
||||
|
<p> |
||||
|
This is an email template using qweb. |
||||
|
</p> |
||||
|
</t> |
||||
|
</template> |
||||
|
</odoo> |
@ -0,0 +1,10 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<odoo> |
||||
|
<record id="email_template_demo1" model="mail.template"> |
||||
|
<field name="name">QWeb demo</field> |
||||
|
<field name="body_type">qweb</field> |
||||
|
<field name="body_view_id" ref="view_email_template_demo1" /> |
||||
|
<field name="model_id" ref="base.model_res_users" /> |
||||
|
<field name="subject">QWeb demo email</field> |
||||
|
</record> |
||||
|
</odoo> |
@ -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" |
@ -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 "" |
After Width: 837 | Height: 253 | Size: 20 KiB |
@ -0,0 +1,2 @@ |
|||||
|
from . import mail_digest |
||||
|
from . import res_partner |
@ -0,0 +1,183 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com> |
||||
|
# 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() |
@ -0,0 +1,208 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com> |
||||
|
# 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) |
@ -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 |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
@ -0,0 +1,47 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<odoo> |
||||
|
<template id="digest_layout" name="Mail digest layout"> |
||||
|
<html> |
||||
|
<body> |
||||
|
<img style="float: right" t-attf-src="data:image;base64,{{env.user.company_id.logo}}" /> |
||||
|
<!-- if some template calling us sets this variable, |
||||
|
we print a h1 tag /--> |
||||
|
<h1 t-if="email_heading"><t t-raw="email_heading" /></h1> |
||||
|
<t t-raw="0" /> |
||||
|
<!-- use some standard footer if the user doesn't have |
||||
|
a signature /--> |
||||
|
<footer t-if="not email_use_user_signature"> |
||||
|
<p> |
||||
|
<a t-att-href="env.user.company_id.website"> |
||||
|
<t t-esc="env.user.company_id.name" /> |
||||
|
</a> |
||||
|
</p> |
||||
|
<p><t t-esc="env.user.company_id.phone" /></p> |
||||
|
</footer> |
||||
|
<footer t-if="email_use_user_signature"> |
||||
|
<t t-raw="env.user.signature" /> |
||||
|
</footer> |
||||
|
</body> |
||||
|
</html> |
||||
|
</template> |
||||
|
|
||||
|
<template id="default_digest_tmpl" name="Mail digest default template"> |
||||
|
<t t-call="mail_digest.digest_layout"> |
||||
|
<div id="mail_content"> |
||||
|
<h2>Hello,</h2> |
||||
|
<div id="mail_inner_content"> |
||||
|
<t t-foreach="grouped_messages.iterkeys()" t-as="gkey"> |
||||
|
<t t-set="messages" t-value="grouped_messages[gkey]" /> |
||||
|
<t t-foreach="messages" t-as="msg"> |
||||
|
<div style="margin:20px"> |
||||
|
<h3 t-esc="msg.subject" /> |
||||
|
<t t-raw="msg.body" /> |
||||
|
</div> |
||||
|
</t> |
||||
|
</t> |
||||
|
</div> |
||||
|
</div> |
||||
|
</t> |
||||
|
</template> |
||||
|
|
||||
|
</odoo> |
@ -0,0 +1,3 @@ |
|||||
|
from . import test_digest |
||||
|
from . import test_partner_domains |
||||
|
from . import test_subtypes_conf |
@ -0,0 +1,147 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com> |
||||
|
# 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']) |
@ -0,0 +1,169 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com> |
||||
|
# 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) |
@ -0,0 +1,136 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com> |
||||
|
# 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) |
||||
|
) |
@ -0,0 +1,50 @@ |
|||||
|
<?xml version="1.0"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
|
||||
|
<record model="ir.ui.view" id="mail_digest_tree"> |
||||
|
<field name="name">mail_digest mail.digest.tree</field> |
||||
|
<field name="model">mail.digest</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<tree string="Mail digest"> |
||||
|
<field name="partner_id" /> |
||||
|
<field name="mail_id" /> |
||||
|
<field name="state" /> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
|
||||
|
<record model="ir.ui.view" id="mail_digest_form"> |
||||
|
<field name="name">mail_digest mail.digest.form</field> |
||||
|
<field name="model">mail.digest</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form string="Mail digest"> |
||||
|
<group name="main" col="4"> |
||||
|
<field name="name" /> |
||||
|
<field name="partner_id" /> |
||||
|
<field name="frequency" /> |
||||
|
<field name="mail_id" /> |
||||
|
<field name="state" /> |
||||
|
</group> |
||||
|
<group name="messages" string="Messages" col="4"> |
||||
|
<field name="message_ids" nolabel="1" /> |
||||
|
</group> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record model="ir.actions.act_window" id="action_digest_all"> |
||||
|
<field name="name">Digest</field> |
||||
|
<field name="res_model">mail.digest</field> |
||||
|
<field name="view_type">form</field> |
||||
|
<field name="view_mode">form,tree</field> |
||||
|
<field name="view_id" ref="mail_digest_tree" /> |
||||
|
</record> |
||||
|
|
||||
|
<menuitem id="menu_email_digest" parent="base.menu_email" |
||||
|
action="action_digest_all" |
||||
|
sequence="90"/> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
@ -0,0 +1,41 @@ |
|||||
|
<?xml version="1.0"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
|
||||
|
<record id="notifications_emails_partner_info_form" model="ir.ui.view"> |
||||
|
<field name="name">mail.notifications res.partner.form</field> |
||||
|
<field name="model">res.partner</field> |
||||
|
<field name="inherit_id" ref="mail.view_emails_partner_info_form"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<xpath expr="//field[@name='notify_email']" position="after"> |
||||
|
<field name="notify_conf_ids" attrs="{'invisible': [('notify_email','=', 'none')]}"/> |
||||
|
</xpath> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record model="ir.ui.view" id="notification_form"> |
||||
|
<field name="name">partner.notification.conf form</field> |
||||
|
<field name="model">partner.notification.conf</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form string="Notification"> |
||||
|
<group name="main"> |
||||
|
<field name="enabled" /> |
||||
|
<field name="subtype_id" options="{'no_create': True}" /> |
||||
|
</group> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record model="ir.ui.view" id="notification_tree"> |
||||
|
<field name="name">partner.notification.conf tree</field> |
||||
|
<field name="model">partner.notification.conf</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<tree string="Notifications" editable="top"> |
||||
|
<field name="enabled" /> |
||||
|
<field name="subtype_id" /> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
@ -0,0 +1,18 @@ |
|||||
|
<?xml version="1.0"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
|
||||
|
<record id="notifications_email_user_info_form" model="ir.ui.view"> |
||||
|
<field name="name">mail.notifications res.users.form</field> |
||||
|
<field name="model">res.users</field> |
||||
|
<field name="inherit_id" ref="mail.view_users_form_mail"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<xpath expr="//field[@name='notify_email']" position="after"> |
||||
|
<field name="notify_frequency" attrs="{'invisible': [('notify_email','=', 'none')]}" /> |
||||
|
<field name="notify_conf_ids" attrs="{'invisible': [('notify_email','=', 'none')]}" /> |
||||
|
</xpath> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue