diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 90d24275051..3f1b79cff76 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -51,7 +51,6 @@ class Lead(models.Model): _description = "Lead/Opportunity" _order = "priority desc,activity_date_deadline,id desc" _inherit = ['mail.thread', 'mail.activity.mixin', 'utm.mixin', 'format.address.mixin'] - _mail_mass_mailing = _('Leads / Opportunities') def _default_probability(self): stage_id = self._default_stage_id() diff --git a/addons/event/models/event.py b/addons/event/models/event.py index c5f7f43915a..85406ba3ba9 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -319,12 +319,12 @@ class EventEvent(models.Model): def _is_event_registrable(self): return True + class EventRegistration(models.Model): _name = 'event.registration' _description = 'Attendee' _inherit = ['mail.thread'] _order = 'name, create_date desc' - _mail_mass_mailing = _('Event Attendees') origin = fields.Char( string='Source Document', readonly=True, diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py index 3a237e9f283..5f432488587 100644 --- a/addons/hr_recruitment/models/hr_recruitment.py +++ b/addons/hr_recruitment/models/hr_recruitment.py @@ -89,7 +89,6 @@ class Applicant(models.Model): _description = "Applicant" _order = "priority desc, id desc" _inherit = ['mail.thread', 'mail.activity.mixin', 'utm.mixin'] - _mail_mass_mailing = _('Applicants') def _default_stage_id(self): if self._context.get('default_job_id'): diff --git a/addons/link_tracker/models/link_tracker.py b/addons/link_tracker/models/link_tracker.py index d256429a0c4..36f43d485a8 100644 --- a/addons/link_tracker/models/link_tracker.py +++ b/addons/link_tracker/models/link_tracker.py @@ -244,23 +244,21 @@ class link_tracker_click(models.Model): again = self.search_count([('link_id', '=', code_rec.link_id.id), ('ip', '=', ip)]) if not again: - country_record = self.env['res.country'].search([('code', '=', country_code)], limit=1) + self.create( + self._get_click_values(dict( + code=code, + ip=ip, + country_code=country_code, + stat_id=stat_id, + ))) - vals = { - 'link_id': code_rec.link_id.id, - 'create_date': datetime.date.today(), - 'ip': ip, - 'country_id': country_record.id, - 'mail_stat_id': stat_id - } + def _get_click_values_from_route(self, route_values): + code = self.env['link.tracker.code'].search([('code', '=', route_values['code'])], limit=1) + country = self.env['res.country'].search([('code', '=', route_values['country_code'])], limit=1) - if stat_id: - mail_stat = self.env['mail.mail.statistics'].search([('id', '=', stat_id)]) - - if mail_stat.mass_mailing_campaign_id: - vals['mass_mailing_campaign_id'] = mail_stat.mass_mailing_campaign_id.id - - if mail_stat.mass_mailing_id: - vals['mass_mailing_id'] = mail_stat.mass_mailing_id.id - - self.create(vals) + return { + 'link_id': code.link_id.id, + 'create_date': datetime.date.today(), + 'ip': route_values['ip'], + 'country_id': country.id, + } diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 6e097196a1b..2587601e2b1 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -79,7 +79,6 @@ class MailThread(models.AbstractModel): _description = 'Email Thread' _mail_flat_thread = True # flatten the discussino history _mail_post_access = 'write' # access required on the document to post on it - _mail_mass_mailing = False # enable mass mailing on this model _Attachment = namedtuple('Attachment', ('fname', 'content', 'info')) message_is_follower = fields.Boolean( @@ -2201,13 +2200,3 @@ class MailThread(models.AbstractModel): msg_comment.write({"res_id": new_thread.id, "model": new_thread._name}) msg_not_comment.write({"res_id": new_thread.id, "model": new_thread._name, "subtype_id": None}) return True - - # ------------------------------------------------------ - # Mass mailing - # ------------------------------------------------------ - - def message_mass_mailing_enabled(self): - if self._mail_mass_mailing: - # TODO master properly translate - # the _mail_mass_mailing is evaluted at code start so not translated - return _(self._mail_mass_mailing) diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 9140959f89e..0b7533c853d 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -18,7 +18,6 @@ class Partner(models.Model): _name = "res.partner" _inherit = ['res.partner', 'mail.thread', 'mail.activity.mixin'] _mail_flat_thread = False - _mail_mass_mailing = _('Customers') message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact", default=0) opt_out = fields.Boolean( diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv index 0bd1697ca87..99f811a3634 100644 --- a/addons/mail/security/ir.model.access.csv +++ b/addons/mail/security/ir.model.access.csv @@ -35,6 +35,7 @@ access_mail_shortcode,mail.shortcode,model_mail_shortcode,base.group_user,1,1,1, access_mail_shortcode_portal,mail.shortcode.portal,model_mail_shortcode,base.group_portal,1,0,0,0 access_mail_test_user,mail.test.all,model_mail_test,base.group_user,1,1,1,1 access_mail_test_portal,mail.test.all,model_mail_test,base.group_portal,1,1,0,0 +access_mail_test_simple,mail.test.simple.all,model_mail_test_simple,base.group_user,1,1,1,1 access_mail_activity_all,mail.activity.all,model_mail_activity,,1,0,0,0 access_mail_activity_user,mail.activity.user,model_mail_activity,base.group_user,1,1,1,1 access_mail_activity_type_all,mail.activity.type.all,model_mail_activity_type,,1,0,0,0 diff --git a/addons/mail/tests/test_mail_model.py b/addons/mail/tests/test_mail_model.py index 8fc3a6f3453..578c779aad5 100644 --- a/addons/mail/tests/test_mail_model.py +++ b/addons/mail/tests/test_mail_model.py @@ -1,16 +1,23 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import fields, models + + +class MailTestSimple(models.Model): + _description = 'Test Simple Chatter Record' + _name = 'mail.test.simple' + _inherit = ['mail.thread'] + + name = fields.Char() + email_from = fields.Char() + description = fields.Text() class MailTest(models.Model): - """ A mail.channel is a discussion group that may behave like a listener - on documents. """ _description = 'Test Mail Model' _name = 'mail.test' _mail_post_access = 'read' - # _mail_flat_thread = False _inherit = ['mail.thread', 'mail.alias.mixin'] name = fields.Char() diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py index b2e77c2875e..bcee631aad6 100644 --- a/addons/mass_mailing/controllers/main.py +++ b/addons/mass_mailing/controllers/main.py @@ -17,7 +17,7 @@ class MassMailController(http.Controller): if mailing.exists(): res_id = res_id and int(res_id) res_ids = [] - if mailing.mailing_model == 'mail.mass_mailing.contact': + if mailing.mailing_model_name == 'mail.mass_mailing.contact': contacts = request.env['mail.mass_mailing.contact'].sudo().search([ ('email', '=', email), ('list_id', 'in', [mailing_list.id for mailing_list in mailing.contact_list_ids]) diff --git a/addons/mass_mailing/data/mass_mailing_demo.xml b/addons/mass_mailing/data/mass_mailing_demo.xml index 0afa9973dd0..347a04deaf1 100644 --- a/addons/mass_mailing/data/mass_mailing_demo.xml +++ b/addons/mass_mailing/data/mass_mailing_demo.xml @@ -64,7 +64,7 @@ done - res.partner + [('customer', '=', True)] email ]]> @@ -77,7 +77,7 @@ Second Newsletter draft - res.partner + [('customer', '=', True)] email ]]> diff --git a/addons/mass_mailing/models/link_tracker.py b/addons/mass_mailing/models/link_tracker.py index 2ced9d90992..c7077191dab 100644 --- a/addons/mass_mailing/models/link_tracker.py +++ b/addons/mass_mailing/models/link_tracker.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import api, fields, models class LinkTracker(models.Model): @@ -17,3 +17,23 @@ class LinkTrackerClick(models.Model): mail_stat_id = fields.Many2one('mail.mail.statistics', string='Mail Statistics') mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing') mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign') + + @api.model + def add_click(self, code, ip, country_code, stat_id=False): + res = super(LinkTrackerClick, self).add_click(code, ip, country_code, stat_id=stat_id) + if stat_id: + stat_sudo = self.env['mail.mail.statistics'].sudo().browse(stat_id) + stat_sudo.set_opened() + stat_sudo.set_clicked() + return res + + def _get_click_values_from_route(self, route_values): + click_values = super(LinkTrackerClick, self)._get_click_values_from_route(route_values) + if route_values['stat_id']: + mail_stat = self.env['mail.mail.statistics'].browse(route_values['stat_id']) + click_values['mail_stat_id'] = mail_stat.id + if mail_stat.mass_mailing_campaign_id: + click_values['mass_mailing_campaign_id'] = mail_stat.mass_mailing_campaign_id.id + if mail_stat.mass_mailing_id: + click_values['mass_mailing_id'] = mail_stat.mass_mailing_id.id + return click_values diff --git a/addons/mass_mailing/models/mail_thread.py b/addons/mass_mailing/models/mail_thread.py index ee5fb45f6eb..8e4ee298c67 100644 --- a/addons/mass_mailing/models/mail_thread.py +++ b/addons/mass_mailing/models/mail_thread.py @@ -37,6 +37,7 @@ class MailThread(models.AbstractModel): matching message_id in mail.mail.statistics. """ if message.get('References'): message_ids = [x.strip() for x in decode_smtp_header(message['References']).split()] + self.env['mail.mail.statistics'].set_opened(mail_message_ids=message_ids) self.env['mail.mail.statistics'].set_replied(mail_message_ids=message_ids) return super(MailThread, self).message_route_process(message, message_dict, routes) diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index a10e6143c89..c78cd4a5d9e 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -6,6 +6,7 @@ import hmac from datetime import datetime import logging import random +import threading from odoo import api, fields, models, tools, _ from odoo.exceptions import UserError @@ -290,22 +291,6 @@ class MassMailing(models.Model): res['reply_to_mode'] = 'thread' return res - def _get_mailing_model(self): - res = [] - for model_name in self.env: - model = self.env[model_name] - if hasattr(model, '_mail_mass_mailing') and getattr(model, '_mail_mass_mailing'): - if getattr(model, 'message_mass_mailing_enabled'): - res.append((model._name, model.message_mass_mailing_enabled())) - else: - res.append((model._name, model._mail_mass_mailing)) - res.append(('mail.mass_mailing.contact', _('Mail Contacts'))) - res.append(('mail.mass_mailing.list', _('Mailing List'))) - return res - - # indirections for inheritance - _mailing_model = lambda self: self._get_mailing_model() - active = fields.Boolean(default=True) email_from = fields.Char(string='From', required=True, default=lambda self: self.env['mail.message']._get_default_from()) @@ -335,7 +320,9 @@ class MassMailing(models.Model): default=lambda self: self.env['mail.message']._get_default_from()) # recipients mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mail.mass_mailing.contact', required=True) - mailing_model = fields.Selection(selection=_mailing_model, string='Recipients Model', default='mail.mass_mailing.list') + mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain="[('is_mail_thread', '=', True)]", + default=lambda self: self.env.ref('mass_mailing.model_mail_mass_mailing_list').id) + mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name') mailing_domain = fields.Char(string='Domain', oldname='domain', default=[]) contact_list_ids = fields.Many2many('mail.mass_mailing.list', 'mail_mass_mailing_list_rel', string='Mailing Lists') @@ -376,10 +363,10 @@ class MassMailing(models.Model): for mass_mailing in self: mass_mailing.clicks_ratio = mapped_data.get(mass_mailing.id, 0) - @api.depends('mailing_model') + @api.depends('mailing_model_id') def _compute_model(self): for record in self: - record.mailing_model_real = (record.mailing_model != 'mail.mass_mailing.list') and record.mailing_model or 'mail.mass_mailing.contact' + record.mailing_model_real = (record.mailing_model_name != 'mail.mass_mailing.list') and record.mailing_model_name or 'mail.mass_mailing.contact' def _compute_statistics(self): """ Compute statistics of the mass mailing """ @@ -451,14 +438,14 @@ class MassMailing(models.Model): 'medium_id': self.mass_mailing_campaign_id.medium_id} self.update(dic) - @api.onchange('mailing_model', 'contact_list_ids') + @api.onchange('mailing_model_id', 'contact_list_ids') def _onchange_model_and_list(self): - if self.mailing_model == 'mail.mass_mailing.list': + if self.mailing_model_name == 'mail.mass_mailing.list': if self.contact_list_ids: self.mailing_domain = "[('list_ids', 'in', [%s]), ('opt_out', '=', False)]" % (','.join(str(id) for id in self.contact_list_ids.ids),) else: self.mailing_domain = "[(0, '=', 1)]" - elif 'opt_out' in self.env[self.mailing_model]._fields and not self.mailing_domain: + elif 'opt_out' in self.env[self.mailing_model_name]._fields and not self.mailing_domain: self.mailing_domain = "[('opt_out', '=', False)]" self.body_html = "on_change_model_and_list" @@ -659,11 +646,12 @@ class MassMailing(models.Model): already_mailed_res_ids = [record['res_id'] for record in already_mailed] return list(set(res_ids) - set(already_mailed_res_ids)) - def send_mail(self): + def send_mail(self, res_ids=None): author_id = self.env.user.partner_id.id + for mailing in self: - # instantiate an email composer + send emails - res_ids = mailing.get_remaining_recipients() + if not res_ids: + res_ids = mailing.get_remaining_recipients() if not res_ids: raise UserError(_('Please select recipients.')) @@ -689,7 +677,9 @@ class MassMailing(models.Model): composer = self.env['mail.compose.message'].with_context(active_ids=res_ids).create(composer_values) extra_context = self._get_mass_mailing_context() composer = composer.with_context(active_ids=res_ids, **extra_context) - composer.send_mail(auto_commit=True) + # auto-commit except in testing mode + auto_commit = not getattr(threading.currentThread(), 'testing', False) + composer.send_mail(auto_commit=auto_commit) mailing.state = 'done' return True diff --git a/addons/mass_mailing/models/mass_mailing_stats.py b/addons/mass_mailing/models/mass_mailing_stats.py index e330975fe5f..2b22ae1d508 100644 --- a/addons/mass_mailing/models/mass_mailing_stats.py +++ b/addons/mass_mailing/models/mass_mailing_stats.py @@ -38,7 +38,10 @@ class MailMailStats(models.Model): opened = fields.Datetime(help='Date when the email has been opened the first time') replied = fields.Datetime(help='Date when this email has been replied for the first time.') bounced = fields.Datetime(help='Date when this email has bounced.') + # Link tracking links_click_ids = fields.One2many('link.tracker.click', 'mail_stat_id', string='Links click') + clicked = fields.Datetime(help='Date when customer clicked on at least one tracked link') + # Status state = fields.Selection(compute="_compute_state", selection=[('outgoing', 'Outgoing'), ('exception', 'Exception'), @@ -51,7 +54,7 @@ class MailMailStats(models.Model): store=True) recipient = fields.Char(compute="_compute_recipient") - @api.depends('sent', 'opened', 'replied', 'bounced', 'exception') + @api.depends('sent', 'opened', 'clicked', 'replied', 'bounced', 'exception') def _compute_state(self): self.update({'state_update': fields.Datetime.now()}) for stat in self: @@ -59,7 +62,7 @@ class MailMailStats(models.Model): stat.state = 'exception' elif stat.sent: stat.state = 'sent' - elif stat.opened: + elif stat.opened or stat.clicked: stat.state = 'opened' elif stat.replied: stat.state = 'replied' @@ -105,6 +108,11 @@ class MailMailStats(models.Model): statistics.write({'opened': fields.Datetime.now()}) return statistics + def set_clicked(self, mail_mail_ids=None, mail_message_ids=None): + statistics = self._get_records(mail_mail_ids, mail_message_ids, [('clicked', '=', False)]) + statistics.write({'clicked': fields.Datetime.now()}) + return statistics + def set_replied(self, mail_mail_ids=None, mail_message_ids=None): statistics = self._get_records(mail_mail_ids, mail_message_ids, [('replied', '=', False)]) statistics.write({'replied': fields.Datetime.now()}) diff --git a/addons/mass_mailing/views/mass_mailing_stats_views.xml b/addons/mass_mailing/views/mass_mailing_stats_views.xml index 142b83c1233..497e629e532 100644 --- a/addons/mass_mailing/views/mass_mailing_stats_views.xml +++ b/addons/mass_mailing/views/mass_mailing_stats_views.xml @@ -11,6 +11,7 @@ + @@ -34,6 +35,7 @@ + @@ -54,6 +56,7 @@ + diff --git a/addons/mass_mailing/views/mass_mailing_views.xml b/addons/mass_mailing/views/mass_mailing_views.xml index 6eba6d8573b..7708a620335 100644 --- a/addons/mass_mailing/views/mass_mailing_views.xml +++ b/addons/mass_mailing/views/mass_mailing_views.xml @@ -413,15 +413,16 @@
-