1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
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
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
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/>
20 ##############################################################################
22 from datetime import datetime
23 from dateutil import relativedelta
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
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'
42 'name': fields.char('Name', required=True),
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
50 _name = 'mail.mass_mailing.contact'
51 _description = 'Mass Mailing Contact'
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',
61 'opt_out': fields.boolean('Opt Out', help='The contact has chosen not to receive mails anymore from this list'),
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
68 'list_id': _get_latest_list
72 class MassMailingList(osv.Model):
73 """Model of a contact list. """
74 _name = 'mail.mass_mailing.list'
76 _description = 'Mailing List'
78 'name': fields.char('Mailing List', required=True),
82 class MassMailingStage(osv.Model):
83 """Stage for mass mailing campaigns. """
84 _name = 'mail.mass_mailing.stage'
85 _description = 'Mass Mailing Campaign Stage'
88 'name': fields.char('Name', required=True, translate=True),
89 'sequence': fields.integer('Sequence'),
96 class MassMailingCampaign(osv.Model):
97 """Model of mass mailing campaigns. """
98 _name = "mail.mass_mailing.campaign"
99 _description = 'Mass Mailing Campaign'
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',
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',
113 'color': fields.integer('Color Index'),
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
121 'user_id': lambda self, cr, uid, ctx=None: uid,
122 'stage_id': lambda self, *args: self._get_default_stage_id(*args),
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'
133 def _get_private_models(self, context=None):
134 return ['res.partner', 'mail.mass_mailing.contact']
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)
142 def _get_mailing_model(self, cr, uid, context=None):
144 ('res.partner', 'Customers'),
145 ('mail.mass_mailing.contact', 'Contacts')
149 'name': fields.char('Subject', required=True),
150 'email_from': fields.char('From'),
151 'date': fields.datetime('Date'),
152 'body_html': fields.html('Body'),
154 'mass_mailing_campaign_id': fields.many2one(
155 'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
158 'state': fields.selection(
159 [('draft', 'Schedule'), ('test', 'Tested'), ('done', 'Sent')], string='Status', required=True,
161 'color': fields.related(
162 'mass_mailing_campaign_id', 'color',
163 type='integer', string='Color Index',
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'
174 'reply_to': fields.char('Reply To'),
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',
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.'
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,
197 #------------------------------------------------------
199 #------------------------------------------------------
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)
206 'statistics_ids': [],
207 'name': _('%s (duplicate)') % mailing.name,
209 return super(MassMailing, self).copy_data(cr, uid, id, default, context=context)
211 #------------------------------------------------------
213 #------------------------------------------------------
215 def on_change_mailing_model(self, cr, uid, ids, mailing_model, context=None):
217 'contact_list_ids': [],
218 'template_id': False,
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)
224 return {'value': values}
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}}
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}}
236 def on_change_contact_list_ids(self, cr, uid, ids, mailing_model, contact_list_ids, context=None):
239 for command in contact_list_ids:
241 list_ids += command[2]
243 values['contact_nbr'] = self.pool[mailing_model].search(
245 self.pool['mail.mass_mailing.list'].get_global_domain(cr, uid, list_ids, context=context)[mailing_model],
246 count=True, context=context
248 return {'value': values}
250 def on_change_template_id(self, cr, uid, ids, template_id, context=None):
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
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}
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}}
268 def action_duplicate(self, cr, uid, ids, context=None):
271 copy_id = self.copy(cr, uid, mid, context=context)
274 'type': 'ir.actions.act_window',
277 'res_model': 'mail.mass_mailing',
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')
287 return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'mass_mailing.action_contact_to_mailing_list')
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)
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)
299 'name': _('Choose Recipients'),
300 'type': 'ir.actions.act_window',
302 'view_mode': 'tree,form',
303 'res_model': mailing.mailing_model,
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]
311 'name': _('See Recipients'),
312 'type': 'ir.actions.act_window',
314 'view_mode': 'tree,form',
315 'res_model': mailing.mailing_model,
321 def action_test_mailing(self, cr, uid, ids, context=None):
322 ctx = dict(context, default_mass_mailing_id=ids[0])
324 'name': _('Test Mailing'),
325 'type': 'ir.actions.act_window',
327 'res_model': 'mail.mass_mailing.test',
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]
335 'name': _('Open with Visual Editor'),
336 'type': 'ir.actions.act_url',
341 #------------------------------------------------------
343 #------------------------------------------------------
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)
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)
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)
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]
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)
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})
381 return '<small><a href="%s">%s</a></small>' % (url, msg or 'Click to unsubscribe')
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)
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],
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)