[FIX] mail, BaseModel, portal_sale: fixes and improvements in the URL
[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',
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     #
52     # Modifying followers change access rights to individual documents. As the
53     # cache may contain accessible/inaccessible data, one has to refresh it.
54     #
55     def create(self, cr, uid, vals, context=None):
56         res = super(mail_followers, self).create(cr, uid, vals, context=context)
57         self.invalidate_cache(cr, uid, context=context)
58         return res
59
60     def write(self, cr, uid, ids, vals, context=None):
61         res = super(mail_followers, self).write(cr, uid, ids, vals, context=context)
62         self.invalidate_cache(cr, uid, context=context)
63         return res
64
65     def unlink(self, cr, uid, ids, context=None):
66         res = super(mail_followers, self).unlink(cr, uid, ids, context=context)
67         self.invalidate_cache(cr, uid, context=context)
68         return res
69
70
71 class mail_notification(osv.Model):
72     """ Class holding notifications pushed to partners. Followers and partners
73         added in 'contacts to notify' receive notifications. """
74     _name = 'mail.notification'
75     _rec_name = 'partner_id'
76     _log_access = False
77     _description = 'Notifications'
78
79     _columns = {
80         'partner_id': fields.many2one('res.partner', string='Contact',
81                         ondelete='cascade', required=True, select=1),
82         'is_read': fields.boolean('Read', select=1),
83         'starred': fields.boolean('Starred', select=1,
84             help='Starred message that goes into the todo mailbox'),
85         'message_id': fields.many2one('mail.message', string='Message',
86                         ondelete='cascade', required=True, select=1),
87     }
88
89     _defaults = {
90         'is_read': False,
91         'starred': False,
92     }
93
94     def init(self, cr):
95         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_partner_id_read_starred_message_id',))
96         if not cr.fetchone():
97             cr.execute('CREATE INDEX mail_notification_partner_id_read_starred_message_id ON mail_notification (partner_id, is_read, starred, message_id)')
98
99     def get_partners_to_email(self, cr, uid, ids, message, context=None):
100         """ Return the list of partners to notify, based on their preferences.
101
102             :param browse_record message: mail.message to notify
103             :param list partners_to_notify: optional list of partner ids restricting
104                 the notifications to process
105         """
106         notify_pids = []
107         for notification in self.browse(cr, uid, ids, context=context):
108             if notification.is_read:
109                 continue
110             partner = notification.partner_id
111             # Do not send to partners without email address defined
112             if not partner.email:
113                 continue
114             # Do not send to partners having same email address than the author (can cause loops or bounce effect due to messy database)
115             if message.author_id and message.author_id.email == partner.email:
116                 continue
117             # Partner does not want to receive any emails or is opt-out
118             if partner.notify_email == 'none':
119                 continue
120             notify_pids.append(partner.id)
121         return notify_pids
122
123     def get_signature_footer(self, cr, uid, user_id, res_model=None, res_id=None, context=None, user_signature=True):
124         """ Format a standard footer for notification emails (such as pushed messages
125             notification or invite emails).
126             Format:
127                 <p>--<br />
128                     Administrator
129                 </p>
130                 <div>
131                     <small>Sent from <a ...>Your Company</a> using <a ...>OpenERP</a>.</small>
132                 </div>
133         """
134         footer = ""
135         if not user_id:
136             return footer
137
138         # add user signature
139         user = self.pool.get("res.users").browse(cr, SUPERUSER_ID, [user_id], context=context)[0]
140         if user_signature:
141             if user.signature:
142                 signature = user.signature
143             else:
144                 signature = "--<br />%s" % user.name
145             footer = tools.append_content_to_html(footer, signature, plaintext=False)
146
147         # add company signature
148         if user.company_id.website:
149             website_url = ('http://%s' % user.company_id.website) if not user.company_id.website.lower().startswith(('http:', 'https:')) \
150                 else user.company_id.website
151             company = "<a style='color:inherit' href='%s'>%s</a>" % (website_url, user.company_id.name)
152         else:
153             company = user.company_id.name
154         sent_by = _('Sent by %(company)s using %(odoo)s.')
155
156         signature_company = '<small>%s</small>' % (sent_by % {
157             'company': company,
158             'odoo': "<a style='color:inherit' href='https://www.odoo.com/'>Odoo</a>"
159         })
160         footer = tools.append_content_to_html(footer, signature_company, plaintext=False, container_tag='div')
161
162         return footer
163
164     def update_message_notification(self, cr, uid, ids, message_id, partner_ids, context=None):
165         existing_pids = set()
166         new_pids = set()
167         new_notif_ids = []
168
169         for notification in self.browse(cr, uid, ids, context=context):
170             existing_pids.add(notification.partner_id.id)
171
172         # update existing notifications
173         self.write(cr, uid, ids, {'is_read': False}, context=context)
174
175         # create new notifications
176         new_pids = set(partner_ids) - existing_pids
177         for new_pid in new_pids:
178             new_notif_ids.append(self.create(cr, uid, {'message_id': message_id, 'partner_id': new_pid, 'is_read': False}, context=context))
179         return new_notif_ids
180
181     def _notify_email(self, cr, uid, ids, message_id, force_send=False, user_signature=True, context=None):
182         message = self.pool['mail.message'].browse(cr, SUPERUSER_ID, message_id, context=context)
183
184         # compute partners
185         email_pids = self.get_partners_to_email(cr, uid, ids, message, context=None)
186         if not email_pids:
187             return True
188
189         # compute email body (signature, company data)
190         body_html = message.body
191         # add user signature except for mail groups, where users are usually adding their own signatures already
192         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
193         signature_company = self.get_signature_footer(cr, uid, user_id, res_model=message.model, res_id=message.res_id, context=context, user_signature=(user_signature and message.model != 'mail.group'))
194         if signature_company:
195             body_html = tools.append_content_to_html(body_html, signature_company, plaintext=False, container_tag='div')
196
197         # compute email references
198         references = message.parent_id.message_id if message.parent_id else False
199
200         # custom values
201         custom_values = dict()
202         if message.model and message.res_id and self.pool.get(message.model) and hasattr(self.pool[message.model], 'message_get_email_values'):
203             custom_values = self.pool[message.model].message_get_email_values(cr, uid, message.res_id, message, context=context)
204
205         # create email values
206         max_recipients = 50
207         chunks = [email_pids[x:x + max_recipients] for x in xrange(0, len(email_pids), max_recipients)]
208         email_ids = []
209         for chunk in chunks:
210             mail_values = {
211                 'mail_message_id': message.id,
212                 'auto_delete': True,
213                 'body_html': body_html,
214                 'recipient_ids': [(4, id) for id in chunk],
215                 'references': references,
216             }
217             mail_values.update(custom_values)
218             email_ids.append(self.pool.get('mail.mail').create(cr, uid, mail_values, context=context))
219         if force_send and len(chunks) < 2:  # for more than 50 followers, use the queue system
220             self.pool.get('mail.mail').send(cr, uid, email_ids, context=context)
221         return True
222
223     def _notify(self, cr, uid, message_id, partners_to_notify=None, context=None,
224                 force_send=False, user_signature=True):
225         """ Send by email the notification depending on the user preferences
226
227             :param list partners_to_notify: optional list of partner ids restricting
228                 the notifications to process
229             :param bool force_send: if True, the generated mail.mail is
230                 immediately sent after being created, as if the scheduler
231                 was executed for this message only.
232             :param bool user_signature: if True, the generated mail.mail body is
233                 the body of the related mail.message with the author's signature
234         """
235         notif_ids = self.search(cr, SUPERUSER_ID, [('message_id', '=', message_id), ('partner_id', 'in', partners_to_notify)], context=context)
236
237         # update or create notifications
238         new_notif_ids = self.update_message_notification(cr, SUPERUSER_ID, notif_ids, message_id, partners_to_notify, context=context)
239
240         # mail_notify_noemail (do not send email) or no partner_ids: do not send, return
241         if context and context.get('mail_notify_noemail'):
242             return True
243
244         # browse as SUPERUSER_ID because of access to res_partner not necessarily allowed
245         self._notify_email(cr, SUPERUSER_ID, new_notif_ids, message_id, force_send, user_signature, context=context)