[MERGE] forward port of branch saas-4 up to 7ecaab9
[odoo/odoo.git] / addons / mail / mail_followers.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2009-today OpenERP SA (<http://www.openerp.com>)
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>
19 #
20 ##############################################################################
21 from openerp.osv import osv, fields
22 from openerp import tools, SUPERUSER_ID
23 from openerp.tools.translate import _
24 from openerp.tools.mail import plaintext2html
25
26 class mail_followers(osv.Model):
27     """ mail_followers holds the data related to the follow mechanism inside
28         OpenERP. Partners can choose to follow documents (records) of any kind
29         that inherits from mail.thread. Following documents allow to receive
30         notifications for new messages.
31         A subscription is characterized by:
32             :param: res_model: model of the followed objects
33             :param: res_id: ID of resource (may be 0 for every objects)
34     """
35     _name = 'mail.followers'
36     _rec_name = 'partner_id'
37     _log_access = False
38     _description = 'Document Followers'
39     _columns = {
40         'res_model': fields.char('Related Document Model', size=128,
41                         required=True, select=1,
42                         help='Model of the followed resource'),
43         'res_id': fields.integer('Related Document ID', select=1,
44                         help='Id of the followed resource'),
45         'partner_id': fields.many2one('res.partner', string='Related Partner',
46                         ondelete='cascade', required=True, select=1),
47         'subtype_ids': fields.many2many('mail.message.subtype', string='Subtype',
48             help="Message subtypes followed, meaning subtypes that will be pushed onto the user's Wall."),
49     }
50
51     _sql_constraints = [('mail_followers_res_partner_res_model_id_uniq','unique(res_model,res_id,partner_id)','Error, a partner cannot follow twice the same object.')]
52
53 class mail_notification(osv.Model):
54     """ Class holding notifications pushed to partners. Followers and partners
55         added in 'contacts to notify' receive notifications. """
56     _name = 'mail.notification'
57     _rec_name = 'partner_id'
58     _log_access = False
59     _description = 'Notifications'
60
61     _columns = {
62         'partner_id': fields.many2one('res.partner', string='Contact',
63                         ondelete='cascade', required=True, select=1),
64         'read': fields.boolean('Read', select=1),
65         'starred': fields.boolean('Starred', select=1,
66             help='Starred message that goes into the todo mailbox'),
67         'message_id': fields.many2one('mail.message', string='Message',
68                         ondelete='cascade', required=True, select=1),
69     }
70
71     _defaults = {
72         'read': False,
73         'starred': False,
74     }
75
76     def init(self, cr):
77         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_partner_id_read_starred_message_id',))
78         if not cr.fetchone():
79             cr.execute('CREATE INDEX mail_notification_partner_id_read_starred_message_id ON mail_notification (partner_id, read, starred, message_id)')
80
81     def get_partners_to_email(self, cr, uid, ids, message, context=None):
82         """ Return the list of partners to notify, based on their preferences.
83
84             :param browse_record message: mail.message to notify
85             :param list partners_to_notify: optional list of partner ids restricting
86                 the notifications to process
87         """
88         notify_pids = []
89         for notification in self.browse(cr, uid, ids, context=context):
90             if notification.read:
91                 continue
92             partner = notification.partner_id
93             # Do not send to partners without email address defined
94             if not partner.email:
95                 continue
96             # Do not send to partners having same email address than the author (can cause loops or bounce effect due to messy database)
97             if message.author_id and message.author_id.email == partner.email:
98                 continue
99             # Partner does not want to receive any emails or is opt-out
100             if partner.notify_email == 'none':
101                 continue
102             notify_pids.append(partner.id)
103         return notify_pids
104
105     def get_signature_footer(self, cr, uid, user_id, res_model=None, res_id=None, context=None):
106         """ Format a standard footer for notification emails (such as pushed messages
107             notification or invite emails).
108             Format:
109                 <p>--<br />
110                     Administrator
111                 </p>
112                 <div>
113                     <small>Sent from <a ...>Your Company</a> using <a ...>OpenERP</a>.</small>
114                 </div>
115         """
116         footer = ""
117         if not user_id:
118             return footer
119
120         # add user signature
121         user = self.pool.get("res.users").browse(cr, SUPERUSER_ID, [user_id], context=context)[0]
122         if user.signature:
123             signature = plaintext2html(user.signature)
124         else:
125             signature = "--<br />%s" % user.name
126         footer = tools.append_content_to_html(footer, signature, plaintext=False, container_tag='p')
127
128         # add company signature
129         if user.company_id.website:
130             website_url = ('http://%s' % user.company_id.website) if not user.company_id.website.lower().startswith(('http:', 'https:')) \
131                 else user.company_id.website
132             company = "<a style='color:inherit' href='%s'>%s</a>" % (website_url, user.company_id.name)
133         else:
134             company = user.company_id.name
135         sent_by = _('Sent by %(company)s using %(odoo)s.')
136
137         signature_company = '<small>%s</small>' % (sent_by % {
138             'company': company,
139             'odoo': "<a style='color:inherit' href='https://www.odoo.com/'>Odoo</a>"
140         })
141         footer = tools.append_content_to_html(footer, signature_company, plaintext=False, container_tag='div')
142
143         return footer
144
145     def update_message_notification(self, cr, uid, ids, message_id, partner_ids, context=None):
146         existing_pids = set()
147         new_pids = set()
148         new_notif_ids = []
149
150         for notification in self.browse(cr, uid, ids, context=context):
151             existing_pids.add(notification.partner_id.id)
152
153         # update existing notifications
154         self.write(cr, uid, ids, {'read': False}, context=context)
155
156         # create new notifications
157         new_pids = set(partner_ids) - existing_pids
158         for new_pid in new_pids:
159             new_notif_ids.append(self.create(cr, uid, {'message_id': message_id, 'partner_id': new_pid, 'read': False}, context=context))
160         return new_notif_ids
161
162     def _notify_email(self, cr, uid, ids, message_id, force_send=False, user_signature=True, context=None):
163         message = self.pool['mail.message'].browse(cr, SUPERUSER_ID, message_id, context=context)
164
165         # compute partners
166         email_pids = self.get_partners_to_email(cr, uid, ids, message, context=None)
167         if not email_pids:
168             return True
169
170         # compute email body (signature, company data)
171         body_html = message.body
172         # add user signature except for mail groups, where users are usually adding their own signatures already
173         if user_signature and message.model != 'mail.group':
174             user_id = message.author_id and message.author_id.user_ids and message.author_id.user_ids[0] and message.author_id.user_ids[0].id or None
175             signature_company = self.get_signature_footer(cr, uid, user_id, res_model=message.model, res_id=message.res_id, context=context)
176             body_html = tools.append_content_to_html(body_html, signature_company, plaintext=False, container_tag='div')
177
178         # compute email references
179         references = message.parent_id.message_id if message.parent_id else False
180
181         # create email values
182         max_recipients = 50
183         chunks = [email_pids[x:x + max_recipients] for x in xrange(0, len(email_pids), max_recipients)]
184         email_ids = []
185         for chunk in chunks:
186             mail_values = {
187                 'mail_message_id': message.id,
188                 'auto_delete': True,
189                 'body_html': body_html,
190                 'recipient_ids': [(4, id) for id in chunk],
191                 'references': references,
192             }
193             email_ids.append(self.pool.get('mail.mail').create(cr, uid, mail_values, context=context))
194         if force_send and len(chunks) < 2:  # for more than 50 followers, use the queue system
195             self.pool.get('mail.mail').send(cr, uid, email_ids, context=context)
196         return True
197
198     def _notify(self, cr, uid, message_id, partners_to_notify=None, context=None,
199                 force_send=False, user_signature=True):
200         """ Send by email the notification depending on the user preferences
201
202             :param list partners_to_notify: optional list of partner ids restricting
203                 the notifications to process
204             :param bool force_send: if True, the generated mail.mail is
205                 immediately sent after being created, as if the scheduler
206                 was executed for this message only.
207             :param bool user_signature: if True, the generated mail.mail body is
208                 the body of the related mail.message with the author's signature
209         """
210         notif_ids = self.search(cr, SUPERUSER_ID, [('message_id', '=', message_id), ('partner_id', 'in', partners_to_notify)], context=context)
211
212         # update or create notifications
213         new_notif_ids = self.update_message_notification(cr, SUPERUSER_ID, notif_ids, message_id, partners_to_notify, context=context)
214
215         # mail_notify_noemail (do not send email) or no partner_ids: do not send, return
216         if context and context.get('mail_notify_noemail'):
217             return True
218
219         # browse as SUPERUSER_ID because of access to res_partner not necessarily allowed
220         self._notify_email(cr, SUPERUSER_ID, new_notif_ids, message_id, force_send, user_signature, context=context)