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