[WIP] started to reduce code
[odoo/odoo.git] / addons / mass_mailing / models / mass_mailing.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2013-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
22 from datetime import datetime
23 from dateutil import relativedelta
24 import random
25 import json
26 import urllib
27 import urlparse
28
29 from openerp import tools
30 from openerp.exceptions import Warning
31 from openerp.tools.safe_eval import safe_eval as eval
32 from openerp.tools.translate import _
33 from openerp.osv import osv, fields
34
35
36 class MassMailingCategory(osv.Model):
37     """Model of categories of mass mailing, i.e. marketing, newsletter, ... """
38     _name = 'mail.mass_mailing.category'
39     _description = 'Mass Mailing Category'
40     _order = 'name'
41     _columns = {
42         'name': fields.char('Name', required=True),
43     }
44
45 class MassMailingContact(osv.Model):
46     """Model of a contact. This model is different from the partner model
47     because it holds only some basic information: name, email. The purpose is to
48     be able to deal with large contact list to email without bloating the partner
49     base."""
50     _name = 'mail.mass_mailing.contact'
51     _description = 'Mass Mailing Contact'
52     _order = 'email'
53     _rec_name = 'email'
54     _columns = {
55         'name': fields.char('Name'),
56         'email': fields.char('Email', required=True),
57         'list_id': fields.many2one(
58             'mail.mass_mailing.list', string='Mailing List',
59             ondelete='cascade',
60         ),
61         'opt_out': fields.boolean('Opt Out', help='The contact has chosen not to receive mails anymore from this list'),
62     }
63
64     def _get_latest_list(self, cr, uid, context={}):
65         lid = self.pool.get('mail.mass_mailing.list').search(cr, uid, [], limit=1, order='id desc', context=context)
66         return lid and lid[0] or False
67     _defaults = {
68         'list_id': _get_latest_list
69     }
70
71
72 class MassMailingList(osv.Model):
73     """Model of a contact list. """
74     _name = 'mail.mass_mailing.list'
75     _order = 'name'
76     _description = 'Mailing List'
77     _columns = {
78         'name': fields.char('Mailing List', required=True),
79     }
80
81
82 class MassMailingStage(osv.Model):
83     """Stage for mass mailing campaigns. """
84     _name = 'mail.mass_mailing.stage'
85     _description = 'Mass Mailing Campaign Stage'
86     _order = 'sequence'
87     _columns = {
88         'name': fields.char('Name', required=True, translate=True),
89         'sequence': fields.integer('Sequence'),
90     }
91     _defaults = {
92         'sequence': 0,
93     }
94
95
96 class MassMailingCampaign(osv.Model):
97     """Model of mass mailing campaigns. """
98     _name = "mail.mass_mailing.campaign"
99     _description = 'Mass Mailing Campaign'
100     _columns = {
101         'name': fields.char('Name', required=True),
102         'stage_id': fields.many2one('mail.mass_mailing.stage', 'Stage', required=True),
103         'user_id': fields.many2one(
104             'res.users', 'Responsible',
105             required=True,
106         ),
107         'category_ids': fields.many2many(
108             'mail.mass_mailing.category', 'Categories'),
109         'mass_mailing_ids': fields.one2many(
110             'mail.mass_mailing', 'mass_mailing_campaign_id',
111             'Mass Mailings',
112         ),
113         'color': fields.integer('Color Index'),
114     }
115
116     def _get_default_stage_id(self, cr, uid, context=None):
117         stage_ids = self.pool['mail.mass_mailing.stage'].search(cr, uid, [], limit=1, context=context)
118         return stage_ids and stage_ids[0] or False
119
120     _defaults = {
121         'user_id': lambda self, cr, uid, ctx=None: uid,
122         'stage_id': lambda self, *args: self._get_default_stage_id(*args),
123     }
124
125
126 class MassMailing(osv.Model):
127     """ MassMailing models a wave of emails for a mass mailign campaign.
128     A mass mailing is an occurence of sending emails. """
129     _name = 'mail.mass_mailing'
130     _description = 'Mass Mailing'
131     _order = 'id DESC'
132
133     def _get_private_models(self, context=None):
134         return ['res.partner', 'mail.mass_mailing.contact']
135
136     def _get_auto_reply_to_available(self, cr, uid, ids, name, arg, context=None):
137         res = dict.fromkeys(ids, False)
138         for mailing in self.browse(cr, uid, ids, context=context):
139             res[mailing.id] = mailing.mailing_model not in self._get_private_models(context=context)
140         return res
141
142     def _get_mailing_model(self, cr, uid, context=None):
143         return [
144             ('res.partner', 'Customers'),
145             ('mail.mass_mailing.contact', 'Contacts')
146         ]
147
148     _columns = {
149         'name': fields.char('Subject', required=True),
150         'email_from': fields.char('From'),
151         'date': fields.datetime('Date'),
152         'body_html': fields.html('Body'),
153
154         'mass_mailing_campaign_id': fields.many2one(
155             'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
156             ondelete='set null',
157         ),
158         'state': fields.selection(
159             [('draft', 'Schedule'), ('test', 'Tested'), ('done', 'Sent')], string='Status', required=True,
160         ),
161         'color': fields.related(
162             'mass_mailing_campaign_id', 'color',
163             type='integer', string='Color Index',
164         ),
165
166         # mailing options
167         # TODO: simplify these 4 fields
168         'reply_in_thread': fields.boolean('Reply in thread'),
169         'reply_specified': fields.boolean('Specific Reply-To'),
170         'auto_reply_to_available': fields.function(
171             _get_auto_reply_to_available,
172             type='boolean', string='Reply in thread available'
173         ),
174         'reply_to': fields.char('Reply To'),
175
176         # Target Emails
177         'mailing_model': fields.selection(_get_mailing_model, string='Model', required=True),
178         'mailing_domain': fields.char('Domain', required=True),
179         'contact_list_ids': fields.many2many(
180             'mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
181             string='Mailing Lists',
182         ),
183         'contact_ab_pc': fields.integer(
184             'AB Testing percentage',
185             help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.'
186         ),
187     }
188
189     _defaults = {
190         'state': 'draft',
191         'date': fields.datetime.now,
192         'email_from': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
193         'mailing_model': 'res.partner',
194         'contact_ab_pc': 100,
195     }
196
197     #------------------------------------------------------
198     # Technical stuff
199     #------------------------------------------------------
200
201     def copy_data(self, cr, uid, id, default=None, context=None):
202         default = default or {}
203         mailing = self.browse(cr, uid, id, context=context)
204         default.update({
205             'state': 'draft',
206             'statistics_ids': [],
207             'name': _('%s (duplicate)') % mailing.name,
208         })
209         return super(MassMailing, self).copy_data(cr, uid, id, default, context=context)
210
211     #------------------------------------------------------
212     # Views & Actions
213     #------------------------------------------------------
214
215     def on_change_mailing_model(self, cr, uid, ids, mailing_model, context=None):
216         values = {
217             'contact_list_ids': [],
218             'template_id': False,
219             'contact_nbr': 0,
220             'auto_reply_to_available': not mailing_model in self._get_private_models(context),
221             'reply_in_thread': not mailing_model in self._get_private_models(context),
222             'reply_specified': mailing_model in self._get_private_models(context)
223         }
224         return {'value': values}
225
226     def on_change_reply_specified(self, cr, uid, ids, reply_specified, reply_in_thread, context=None):
227         if reply_specified == reply_in_thread:
228             return {'value': {'reply_in_thread': not reply_specified}}
229         return {}
230
231     def on_change_reply_in_thread(self, cr, uid, ids, reply_specified, reply_in_thread, context=None):
232         if reply_in_thread == reply_specified:
233             return {'value': {'reply_specified': not reply_in_thread}}
234         return {}
235
236     def on_change_contact_list_ids(self, cr, uid, ids, mailing_model, contact_list_ids, context=None):
237         values = {}
238         list_ids = []
239         for command in contact_list_ids:
240             if command[0] == 6:
241                 list_ids += command[2]
242         if list_ids:
243             values['contact_nbr'] = self.pool[mailing_model].search(
244                 cr, uid,
245                 self.pool['mail.mass_mailing.list'].get_global_domain(cr, uid, list_ids, context=context)[mailing_model],
246                 count=True, context=context
247             )
248         return {'value': values}
249
250     def on_change_template_id(self, cr, uid, ids, template_id, context=None):
251         values = {}
252         if template_id:
253             template = self.pool['email.template'].browse(cr, uid, template_id, context=context)
254             if template.email_from:
255                 values['email_from'] = template.email_from
256             if template.reply_to:
257                 values['reply_to'] = template.reply_to
258             values['body_html'] = template.body_html
259         else:
260             values['email_from'] = self.pool['mail.message']._get_default_from(cr, uid, context=context)
261             values['reply_to'] = False
262             values['body_html'] = False
263         return {'value': values}
264
265     def on_change_contact_ab_pc(self, cr, uid, ids, contact_ab_pc, contact_nbr, context=None):
266         return {'value': {'contact_ab_nbr': contact_nbr * contact_ab_pc / 100.0}}
267
268     def action_duplicate(self, cr, uid, ids, context=None):
269         copy_id = None
270         for mid in ids:
271             copy_id = self.copy(cr, uid, mid, context=context)
272         if copy_id:
273             return {
274                 'type': 'ir.actions.act_window',
275                 'view_type': 'form',
276                 'view_mode': 'form',
277                 'res_model': 'mail.mass_mailing',
278                 'res_id': copy_id,
279                 'context': context,
280             }
281         return False
282
283     def _get_model_to_list_action_id(self, cr, uid, model, context=None):
284         if model == 'res.partner':
285             return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'mass_mailing.action_partner_to_mailing_list')
286         else:
287             return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'mass_mailing.action_contact_to_mailing_list')
288
289     def action_new_list(self, cr, uid, ids, context=None):
290         mailing = self.browse(cr, uid, ids[0], context=context)
291         action_id = self._get_model_to_list_action_id(cr, uid, mailing.mailing_model, context=context)
292         ctx = dict(context,
293                    search_default_not_opt_out=True,
294                    view_manager_highlight=[action_id],
295                    default_name=mailing.name,
296                    default_mass_mailing_id=ids[0],
297                    default_model=mailing.mailing_model)
298         return {
299             'name': _('Choose Recipients'),
300             'type': 'ir.actions.act_window',
301             'view_type': 'form',
302             'view_mode': 'tree,form',
303             'res_model': mailing.mailing_model,
304             'context': ctx,
305         }
306
307     def action_see_recipients(self, cr, uid, ids, context=None):
308         mailing = self.browse(cr, uid, ids[0], context=context)
309         domain = self.pool['mail.mass_mailing.list'].get_global_domain(cr, uid, [c.id for c in mailing.contact_list_ids], context=context)[mailing.mailing_model]
310         return {
311             'name': _('See Recipients'),
312             'type': 'ir.actions.act_window',
313             'view_type': 'form',
314             'view_mode': 'tree,form',
315             'res_model': mailing.mailing_model,
316             'target': 'new',
317             'domain': domain,
318             'context': context,
319         }
320
321     def action_test_mailing(self, cr, uid, ids, context=None):
322         ctx = dict(context, default_mass_mailing_id=ids[0])
323         return {
324             'name': _('Test Mailing'),
325             'type': 'ir.actions.act_window',
326             'view_mode': 'form',
327             'res_model': 'mail.mass_mailing.test',
328             'target': 'new',
329             'context': ctx,
330         }
331
332     def action_edit_html(self, cr, uid, ids, context=None):
333         url = '/website_mail/email_designer?model=mail.mass_mailing&res_id=%d' % ids[0]
334         return {
335             'name': _('Open with Visual Editor'),
336             'type': 'ir.actions.act_url',
337             'url': url,
338             'target': 'self',
339         }
340
341     #------------------------------------------------------
342     # Email Sending
343     #------------------------------------------------------
344
345     def get_recipients_data(self, cr, uid, mailing, res_ids, context=None):
346         # tde todo: notification link ?
347         if mailing.mailing_model == 'mail.mass_mailing.contact':
348             contacts = self.pool['mail.mass_mailing.contact'].browse(cr, uid, res_ids, context=context)
349             return dict((contact.id, {'partner_id': False, 'name': contact.name, 'email': contact.email}) for contact in contacts)
350         else:
351             partners = self.pool['res.partner'].browse(cr, uid, res_ids, context=context)
352             return dict((partner.id, {'partner_id': partner.id, 'name': partner.name, 'email': partner.email}) for partner in partners)
353
354     def get_recipients(self, cr, uid, mailing, context=None):
355         domain = self.pool['mail.mass_mailing.list'].get_global_domain(
356             cr, uid, [l.id for l in mailing.contact_list_ids], context=context
357         )[mailing.mailing_model]
358         res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context)
359
360         # randomly choose a fragment
361         if mailing.contact_ab_pc != 100:
362             topick = mailing.contact_ab_nbr
363             if mailing.mass_mailing_campaign_id and mailing.ab_testing:
364                 already_mailed = self.pool['mail.mass_mailing.campaign'].get_recipients(cr, uid, [mailing.mass_mailing_campaign_id.id], context=context)[mailing.mass_mailing_campaign_id.id]
365             else:
366                 already_mailed = set([])
367             remaining = set(res_ids).difference(already_mailed)
368             if topick > len(remaining):
369                 topick = len(remaining)
370             res_ids = random.sample(remaining, topick)
371         return res_ids
372
373     def get_unsubscribe_url(self, cr, uid, mailing_id, res_id, email, msg=None, context=None):
374         base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
375         url = urlparse.urljoin(
376             base_url, 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % {
377                 'mailing_id': mailing_id,
378                 'params': urllib.urlencode({'db': cr.dbname, 'res_id': res_id, 'email': email})
379             }
380         )
381         return '<small><a href="%s">%s</a></small>' % (url, msg or 'Click to unsubscribe')
382
383     def send_mail(self, cr, uid, ids, context=None):
384         author_id = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id.id
385         for mailing in self.browse(cr, uid, ids, context=context):
386             if not mailing.contact_nbr:
387                 raise Warning('Please select recipients.')
388             # instantiate an email composer + send emails
389             res_ids = self.get_recipients(cr, uid, mailing, context=context)
390             comp_ctx = dict(context, active_ids=res_ids)
391             composer_values = {
392                 'author_id': author_id,
393                 'body': mailing.body_html,
394                 'subject': mailing.name,
395                 'model': mailing.mailing_model,
396                 'email_from': mailing.email_from,
397                 'record_name': False,
398                 'composition_mode': 'mass_mail',
399                 'mass_mailing_id': mailing.id,
400                 'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
401             }
402             if mailing.reply_specified:
403                 composer_values['reply_to'] = mailing.reply_to
404             composer_id = self.pool['mail.compose.message'].create(cr, uid, composer_values, context=comp_ctx)
405             self.pool['mail.compose.message'].send_mail(cr, uid, [composer_id], context=comp_ctx)
406             self.write(cr, uid, [mailing.id], {'date': fields.datetime.now(), 'state': 'done'}, context=context)
407         return True
408
409