diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index d54244a2776..0341be802d1 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -545,7 +545,7 @@ class Meeting(models.Model): """ _name = 'calendar.event' - _description = "Event" + _description = "Calendar Event" _order = "id desc" _inherit = ["mail.thread"] diff --git a/addons/calendar_sms/views/calendar_views.xml b/addons/calendar_sms/views/calendar_views.xml index bb27097ee14..39ad8c52d31 100644 --- a/addons/calendar_sms/views/calendar_views.xml +++ b/addons/calendar_sms/views/calendar_views.xml @@ -2,7 +2,7 @@ - diff --git a/addons/crm_sms/views/crm_lead_views.xml b/addons/crm_sms/views/crm_lead_views.xml index 2b08fa5631f..6a451038434 100644 --- a/addons/crm_sms/views/crm_lead_views.xml +++ b/addons/crm_sms/views/crm_lead_views.xml @@ -22,6 +22,7 @@ target="new" context="{ 'default_composition_mode': 'comment', + 'default_res_id': active_id, }" /> \ No newline at end of file diff --git a/addons/event_sms/tests/test_sms_schedule.py b/addons/event_sms/tests/test_sms_schedule.py index 14ba81b1ffa..e1aa63e6800 100644 --- a/addons/event_sms/tests/test_sms_schedule.py +++ b/addons/event_sms/tests/test_sms_schedule.py @@ -59,7 +59,7 @@ class TestSMSSchedule(TestEventCommon, MockSMS): for registration in self.event_0.registration_ids: reg_sanitized_number = phone_validation.phone_format(registration.phone, 'BE', '32', force_format='E164') sanitized_numbers.append(reg_sanitized_number) - self.assertSMSNotification([{'number': reg_sanitized_number, 'state': 'ready'}], '%s registration confirmation.' % self.event_0.organizer_id.name) + self.assertSMSOutgoing(self.env['res.partner'], reg_sanitized_number, '%s registration confirmation.' % self.event_0.organizer_id.name) # clear notification queue to avoid conflicts when checking next notifications self.env['mail.notification'].search([('sms_number', 'in', sanitized_numbers)]).unlink() @@ -76,4 +76,4 @@ class TestSMSSchedule(TestEventCommon, MockSMS): # verify that subscription scheduler was auto-executed after each registration for registration in self.event_0.registration_ids: reg_sanitized_number = phone_validation.phone_format(registration.phone, 'BE', '32', force_format='E164') - self.assertSMSNotification([{'number': reg_sanitized_number, 'state': 'ready'}], '%s reminder' % self.event_0.organizer_id.name) + self.assertSMSOutgoing(self.env['res.partner'], reg_sanitized_number, '%s reminder' % self.event_0.organizer_id.name) diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py index 43b04896ffd..e38d5b5a14e 100644 --- a/addons/hr/models/hr_employee.py +++ b/addons/hr/models/hr_employee.py @@ -381,8 +381,8 @@ class HrEmployeePrivate(models.Model): def message_post(self, *args, **kwargs): return super(HrEmployeePrivate, self._post_author()).message_post(*args, **kwargs) - def _sms_get_default_partners(self): - return self.mapped('user_partner_id') + def _sms_get_partner_fields(self): + return ['user_partner_id'] def _sms_get_number_fields(self): return ['mobile_phone'] diff --git a/addons/hr_presence/models/hr_employee.py b/addons/hr_presence/models/hr_employee.py index 9860fa0ec1b..a21d3b1f59f 100644 --- a/addons/hr_presence/models/hr_employee.py +++ b/addons/hr_presence/models/hr_employee.py @@ -152,7 +152,7 @@ Do not hesitate to contact your manager or the human resource department.""") "res_model": "sms.composer", "view_mode": 'form', "context": context, - "name": "Send SMS", + "name": "Send SMS Text Message", "target": "new", } diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index c0ff09fec94..efcd8f8d2ae 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -985,78 +985,77 @@ class Message(models.Model): 'message_needaction_counter', ], ids=[res_id]) - @api.model - def create(self, values): - # coming from mail.js that does not have pid in its values - if self.env.context.get('default_starred'): - self = self.with_context({'default_starred_partner_ids': [(4, self.env.user.partner_id.id)]}) + @api.model_create_multi + def create(self, values_list): + tracking_values_list = [] + for values in values_list: + if 'email_from' not in values: # needed to compute reply_to + values['email_from'] = self._get_default_from() + if not values.get('message_id'): + values['message_id'] = self._get_message_id(values) + if 'reply_to' not in values: + values['reply_to'] = self._get_reply_to(values) + if 'record_name' not in values and 'default_record_name' not in self.env.context: + values['record_name'] = self._get_record_name(values) - if 'email_from' not in values: # needed to compute reply_to - values['email_from'] = self._get_default_from() - if not values.get('message_id'): - values['message_id'] = self._get_message_id(values) - if 'reply_to' not in values: - values['reply_to'] = self._get_reply_to(values) - if 'record_name' not in values and 'default_record_name' not in self.env.context: - values['record_name'] = self._get_record_name(values) + if 'attachment_ids' not in values: + values['attachment_ids'] = [] + # extract base64 images + if 'body' in values: + Attachments = self.env['ir.attachment'] + data_to_url = {} + def base64_to_boundary(match): + key = match.group(2) + if not data_to_url.get(key): + name = match.group(4) if match.group(4) else 'image%s' % len(data_to_url) + try: + attachment = Attachments.create({ + 'name': name, + 'datas': match.group(2), + 'res_model': values.get('model'), + 'res_id': values.get('res_id'), + }) + except binascii_error: + _logger.warning("Impossible to create an attachment out of badly formated base64 embedded image. Image has been removed.") + return match.group(3) # group(3) is the url ending single/double quote matched by the regexp + else: + attachment.generate_access_token() + values['attachment_ids'].append((4, attachment.id)) + data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name] + return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1]) + values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body'])) - if 'attachment_ids' not in values: - values['attachment_ids'] = [] - # extract base64 images - if 'body' in values: - Attachments = self.env['ir.attachment'] - data_to_url = {} - def base64_to_boundary(match): - key = match.group(2) - if not data_to_url.get(key): - name = match.group(4) if match.group(4) else 'image%s' % len(data_to_url) - try: - attachment = Attachments.create({ - 'name': name, - 'datas': match.group(2), - 'res_model': values.get('model'), - 'res_id': values.get('res_id'), - }) - except binascii_error: - _logger.warning("Impossible to create an attachment out of badly formated base64 embedded image. Image has been removed.") - return match.group(3) # group(3) is the url ending single/double quote matched by the regexp - else: - attachment.generate_access_token() - values['attachment_ids'].append((4, attachment.id)) - data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name] - return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1]) - values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body'])) + # delegate creation of tracking after the create as sudo to avoid access rights issues + tracking_values_list.append(values.pop('tracking_value_ids', False)) - # delegate creation of tracking after the create as sudo to avoid access rights issues - tracking_values_cmd = values.pop('tracking_value_ids', False) - message = super(Message, self).create(values) + messages = super(Message, self).create(values_list) - # check attachement access - if values.get('attachment_ids'): - attachment_ids = [] - if all(isinstance(command, int) or command[0] in (4, 6) for command in values.get('attachment_ids')): - attachment_ids = [] + check_attachment_access = [] + if all(isinstance(command, int) or command[0] in (4, 6) for values in values_list for command in values.get('attachment_ids')): + for values in values_list: for command in values.get('attachment_ids'): if isinstance(command, int): - attachment_ids += [command] + check_attachment_access += [command] elif command[0] == 6: - attachment_ids += command[2] - else: # command[0] == 4: - attachment_ids += [command[1]] - else: - attachment_ids = message.mapped('attachment_ids').ids # fallback on read if any unknow command - self.env['ir.attachment'].browse(attachment_ids).check(mode='read') + check_attachment_access += command[2] + else: # command[0] == 4: + check_attachment_access += [command[1]] + else: + check_attachment_access = messages.mapped('attachment_ids').ids # fallback on read if any unknow command + if check_attachment_access: + self.env['ir.attachment'].browse(check_attachment_access).check(mode='read') - if tracking_values_cmd: - vals_lst = [dict(cmd[2], mail_message_id=message.id) for cmd in tracking_values_cmd if len(cmd) == 3 and cmd[0] == 0] - other_cmd = [cmd for cmd in tracking_values_cmd if len(cmd) != 3 or cmd[0] != 0] - if vals_lst: - self.env['mail.tracking.value'].sudo().create(vals_lst) - if other_cmd: - message.sudo().write({'tracking_value_ids': tracking_values_cmd}) + for message, values, tracking_values_cmd in zip(messages, values_list, tracking_values_list): + if tracking_values_cmd: + vals_lst = [dict(cmd[2], mail_message_id=message.id) for cmd in tracking_values_cmd if len(cmd) == 3 and cmd[0] == 0] + other_cmd = [cmd for cmd in tracking_values_cmd if len(cmd) != 3 or cmd[0] != 0] + if vals_lst: + self.env['mail.tracking.value'].sudo().create(vals_lst) + if other_cmd: + message.sudo().write({'tracking_value_ids': tracking_values_cmd}) - if message.is_thread_message(values): - message._invalidate_documents(values.get('model'), values.get('res_id')) + if message.is_thread_message(values): + message._invalidate_documents(values.get('model'), values.get('res_id')) return message diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index bdfd821967c..701690039ca 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1699,7 +1699,7 @@ class MailThread(models.AbstractModel): @api.returns('mail.message', lambda value: value.id) def message_post(self, body='', subject=None, message_type='notification', - email_from=False, author_id=None, parent_id=False, + email_from=None, author_id=None, parent_id=False, subtype_id=False, subtype=None, partner_ids=None, channel_ids=None, attachments=None, attachment_ids=None, add_sign=True, record_name=False, @@ -1750,10 +1750,8 @@ class MailThread(models.AbstractModel): raise ValueError('message_post partner_ids and channel_ids must be integer list, not commands') # Find the message's author - if author_id is None: # keep False values - author_id = self.env.user.partner_id.id - if not email_from: - email_from = self.env['res.partner'].browse(author_id).sudo().email_formatted if author_id else self.env['mail.message']._get_default_from() + author_info = self._message_compute_author(author_id, email_from, raise_exception=True) + author_id, email_from = author_info['author_id'], author_info['email_from'] if not subtype_id: subtype = subtype or 'mt_note' @@ -1786,6 +1784,7 @@ class MailThread(models.AbstractModel): values = dict(msg_kwargs) values.update({ 'author_id': author_id, + 'email_from': email_from, 'model': self._name, 'res_id': self.id, 'body': body, @@ -1797,7 +1796,6 @@ class MailThread(models.AbstractModel): 'channel_ids': channel_ids, 'add_sign': add_sign, 'record_name': record_name, - 'email_from': email_from, }) attachments = attachments or [] attachment_ids = attachment_ids or [] @@ -1887,7 +1885,7 @@ class MailThread(models.AbstractModel): return composer.send_mail() def message_notify(self, partner_ids=False, parent_id=False, model=False, res_id=False, - author_id=False, body='', subject=False, **kwargs): + author_id=None, email_from=None, body='', subject=False, **kwargs): """ Shortcut allowing to notify partners of messages that shouldn't be displayed on a document. It pushes notifications on inbox or by email depending on the user configuration, like other notifications. """ @@ -1897,16 +1895,8 @@ class MailThread(models.AbstractModel): msg_kwargs = dict((key, val) for key, val in kwargs.items() if key in self.env['mail.message']._fields) notif_kwargs = dict((key, val) for key, val in kwargs.items() if key not in msg_kwargs) - if author_id: - author = self.env['res.partner'].sudo().browse(author_id) - else: - author = self.env.user.partner_id - - if not author.email: - raise exceptions.UserError(_("Unable to notify message, please configure the sender's email address.")) - email_from = author.email_formatted - - partner_ids = partner_ids or set() + author_info = self._message_compute_author(author_id, email_from, raise_exception=True) + author_id, email_from = author_info['author_id'], author_info['email_from'] if not partner_ids: _logger.warning('Message notify called without recipient_ids, skipping') @@ -1924,7 +1914,7 @@ class MailThread(models.AbstractModel): 'message_type': 'user_notification', 'subject': subject, 'body': body, - 'author_id': author.id, + 'author_id': author_id, 'email_from': email_from, 'partner_ids': partner_ids, 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'), @@ -1937,7 +1927,7 @@ class MailThread(models.AbstractModel): MailThread._notify_thread(new_message, values, **notif_kwargs) return new_message - def _message_log(self, body='', author_id=None, subject=False, message_type='notification', **kwargs): + def _message_log(self, body='', author_id=None, email_from=None, subject=False, message_type='notification', **kwargs): """ Shortcut allowing to post note on a document. It does not perform any notification and pre-computes some values to have a short code as optimized as possible. This method is private as it does not check @@ -1945,24 +1935,13 @@ class MailThread(models.AbstractModel): the log process. This method should be called within methods where access rights are already granted to avoid privilege escalation. """ self.ensure_one() - if author_id: - author = self.env['res.partner'].sudo().browse(author_id) - else: - author = self.env.user.partner_id - author_id = author.id - - if author.email: - email_from = author.email_formatted - elif self.env.su: - # superuser mode without author email -> probably public user; anyway we don't want to crash - email_from = False - else: - raise exceptions.UserError(_("Unable to log message, please configure the sender's email address.")) + author_info = self._message_compute_author(author_id, email_from, raise_exception=False) + author_id, email_from = author_info['author_id'], author_info['email_from'] message_values = { 'subject': subject, 'body': body, - 'author_id': author.id, + 'author_id': author_id, 'email_from': email_from, 'message_type': message_type, 'model': kwargs.get('model', self._name), @@ -1973,14 +1952,73 @@ class MailThread(models.AbstractModel): 'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify } message_values.update(kwargs) - message = self.sudo()._message_create(message_values) - return message + return self.sudo()._message_create(message_values) - def _message_create(self, values): - create_values = dict(values) - create_values['partner_ids'] = [(4, pid) for pid in create_values.get('partner_ids', [])] - create_values['channel_ids'] = [(4, cid) for cid in create_values.get('channel_ids', [])] - return self.env['mail.message'].create(create_values) + def _message_log_batch(self, bodies, author_id=None, email_from=None, subject=False, message_type='notification'): + """ Shortcut allowing to post notes on a batch of documents. It achieve the + same purpose as _message_log, done in batch to speedup quick note log. + + :param bodies: dict {record_id: body} + """ + author_info = self._message_compute_author(author_id, email_from, raise_exception=False) + author_id, email_from = author_info['author_id'], author_info['email_from'] + + base_message_values = { + 'subject': subject, + 'author_id': author_id, + 'email_from': email_from, + 'message_type': message_type, + 'model': self._name, + 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'), + 'record_name': False, + 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from, records=None)[False], + 'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify + } + values_list = [dict(base_message_values, + res_id=record.id, + body=bodies.get(record.id, '')) + for record in self] + return self.sudo()._message_create(values_list) + + def _message_compute_author(self, author_id=None, email_from=None, raise_exception=True): + """ Tool method computing author information for messages. Purpose is + to ensure maximum coherence between author / current user / email_from + when sending emails. """ + if author_id is None: + if email_from: + author = self._mail_find_partner_from_emails([email_from])[0] + else: + author = self.env.user.partner_id + email_from = author.email_formatted + author_id = author.id + + if email_from is None: + if author_id: + author = self.env['res.partner'].browse(author_id) + email_from = author.email_formatted + + # superuser mode without author email -> probably public user; anyway we don't want to crash + if not email_from and not self.env.su and raise_exception: + raise exceptions.UserError(_("Unable to log message, please configure the sender's email address.")) + + return { + 'author_id': author_id, + 'email_from': email_from, + } + + def _message_create(self, values_list): + if not isinstance(values_list, (list)): + values_list = [values_list] + create_values_list = [] + for values in values_list: + create_values = dict(values) + # Avoid warnings about non-existing fields + for x in ('from', 'to', 'cc', 'canned_response_ids'): + create_values.pop(x, None) + create_values['partner_ids'] = [(4, pid) for pid in create_values.get('partner_ids', [])] + create_values['channel_ids'] = [(4, cid) for cid in create_values.get('channel_ids', [])] + create_values_list.append(create_values) + return self.env['mail.message'].create(create_values_list) # ------------------------------------------------------ # Notification API diff --git a/addons/mass_mailing/data/mass_mailing_demo.xml b/addons/mass_mailing/data/mass_mailing_demo.xml index 40d04fcd0c1..e2768d2ae16 100644 --- a/addons/mass_mailing/data/mass_mailing_demo.xml +++ b/addons/mass_mailing/data/mass_mailing_demo.xml @@ -129,27 +129,6 @@
- - - @@ -195,6 +172,7 @@ 1111000@odoo.comres.partner + billy.fox45@example.com @@ -204,6 +182,7 @@ 1111001@odoo.com res.partner + kim.snyder96@example.com @@ -213,6 +192,7 @@ 1111002@odoo.com res.partner + edith.sanchez68@example.com @@ -221,6 +201,7 @@ 1111003@odoo.com res.partner + theodore.gardner36@example.com @@ -229,14 +210,15 @@ 1111004@odoo.com res.partner + sandra.neal80@example.com - 1111004@odoo.com res.partner + julie.richards84@example.com @@ -245,6 +227,7 @@ 1111004@odoo.com res.partner + travis.mendoza24@example.com diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py index 79ab5c2d39e..845cd05fdfd 100644 --- a/addons/mass_mailing/models/mail_mail.py +++ b/addons/mass_mailing/models/mail_mail.py @@ -99,5 +99,5 @@ class MailMail(models.Model): if mail_sent is True and mail.mailing_trace_ids: mail.mailing_trace_ids.write({'sent': fields.Datetime.now(), 'exception': False}) elif mail_sent is False and mail.mailing_trace_ids: - mail.mailing_trace_ids.write({'exception': fields.Datetime.now()}) + mail.mailing_trace_ids.write({'exception': fields.Datetime.now(), 'failure_type': failure_type}) return super(MailMail, self)._postprocess_sent_message(success_pids, failure_reason=failure_reason, failure_type=failure_type) diff --git a/addons/mass_mailing/models/mailing.py b/addons/mass_mailing/models/mailing.py index 11716c19a90..fcf1d8fc4c1 100644 --- a/addons/mass_mailing/models/mailing.py +++ b/addons/mass_mailing/models/mailing.py @@ -33,6 +33,7 @@ MASS_MAILING_BUSINESS_MODELS = [ # Used to find inline images image_re = re.compile(r"data:(image/[A-Za-z]+);base64,(.*)") + class MassMailing(models.Model): """ MassMailing models a wave of emails for a mass mailign campaign. A mass mailing is an occurence of sending emails. """ @@ -85,6 +86,7 @@ class MassMailing(models.Model): color = fields.Integer(string='Color Index') user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.user) # mailing options + mailing_type = fields.Selection([('mail', 'Email')], string="Mailing Type", default="mail", required=True) reply_to_mode = fields.Selection( [('thread', 'Recipient Followers'), ('email', 'Specified Email Address')], string='Reply-To Mode', required=True) reply_to = fields.Char(string='Reply To', help='Preferred Reply-To Address', @@ -155,10 +157,9 @@ class MassMailing(models.Model): m.id as mailing_id, COUNT(s.id) AS expected, COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent, - COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null THEN 1 ELSE null END) AS scheduled, - COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed, + COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null AND s.bounced is null THEN 1 ELSE null END) AS scheduled, COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored, - COUNT(CASE WHEN s.sent is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered, + COUNT(CASE WHEN s.sent is not null AND s.exception is null AND s.bounced is null THEN 1 ELSE null END) AS delivered, COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened, COUNT(CASE WHEN s.clicked is not null THEN 1 ELSE null END) AS clicked, COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied, @@ -217,11 +218,21 @@ class MassMailing(models.Model): mailing_domain = [(0, '=', 1)] self.mailing_domain = repr(mailing_domain) + @api.onchange('mailing_type') + def _onchange_mailing_type(self): + if self.mailing_type == 'mail' and not self.medium_id: + self.medium_id = self.env.ref('utm.utm_medium_email').id + @api.onchange('subject') def _onchange_subject(self): if self.subject and not self.name: self.name = self.subject + @api.onchange('name') + def _onchange_name(self): + if self.name and not self.subject: + self.subject = self.name + # ------------------------------------------------------ # ORM # ------------------------------------------------------ @@ -232,7 +243,7 @@ class MassMailing(models.Model): values['subject'] = values['name'] if values.get('body_html'): values['body_html'] = self._convert_inline_images_to_urls(values['body_html']) - if 'medium_id' not in values: + if 'medium_id' not in values and values.get('mailing_type', 'mail') == 'mail': values['medium_id'] = self.env.ref('utm.utm_medium_email').id return super(MassMailing, self).create(values) @@ -270,7 +281,7 @@ class MassMailing(models.Model): } return False - def action_test_mailing(self): + def action_test(self): self.ensure_one() ctx = dict(self.env.context, default_mass_mailing_id=self.id) return { @@ -282,30 +293,44 @@ class MassMailing(models.Model): 'context': ctx, } - def action_schedule_date(self): + def action_schedule(self): self.ensure_one() action = self.env.ref('mass_mailing.mailing_mailing_schedule_date_action').read()[0] action['context'] = dict(self.env.context, default_mass_mailing_id=self.id) return action - def put_in_queue(self): + def action_put_in_queue(self): self.write({'state': 'in_queue'}) - def cancel_mass_mailing(self): + def action_cancel(self): self.write({'state': 'draft', 'schedule_date': False}) - def retry_failed_mail(self): - failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')]) + def action_retry_failed(self): + failed_mails = self.env['mail.mail'].sudo().search([ + ('mailing_id', 'in', self.ids), + ('state', '=', 'exception') + ]) failed_mails.mapped('mailing_trace_ids').unlink() - failed_mails.sudo().unlink() - res_ids = self._get_recipients() - except_mailed = self.env['mailing.trace'].search([ - ('model', '=', self.mailing_model_real), - ('res_id', 'in', res_ids), - ('exception', '!=', False), - ('mass_mailing_id', '=', self.id)]).unlink() + failed_mails.unlink() self.write({'state': 'in_queue'}) + def action_view_traces_scheduled(self): + return self._action_view_traces_filtered('scheduled') + + def action_view_traces_ignored(self): + return self._action_view_traces_filtered('ignored') + + def action_view_traces_failed(self): + return self._action_view_traces_filtered('failed') + + def _action_view_traces_filtered(self, view_filter): + action = self.env.ref('mass_mailing.mailing_trace_action').read()[0] + action['name'] = _('%s Traces') % (self.name) + action['context'] = {'search_default_mass_mailing_id': self.id,} + filter_key = 'search_default_filter_%s' % (view_filter) + action['context'][filter_key] = True + return action + def action_view_sent(self): return self._action_view_documents_filtered('sent') @@ -387,7 +412,7 @@ class MassMailing(models.Model): _logger.info("Mass-mailing %s targets %s, no opt out list available", self, target._name) return opt_out - def _get_convert_links(self): + def _get_link_tracker_values(self): self.ensure_one() vals = {'mass_mailing_id': self.id} @@ -456,7 +481,7 @@ class MassMailing(models.Model): return { 'mass_mailing_opt_out_list': self._get_opt_out_list(), 'mass_mailing_seen_list': self._get_seen_list(), - 'post_convert_links': self._get_convert_links(), + 'post_convert_links': self._get_link_tracker_values(), } def _get_recipients(self): diff --git a/addons/mass_mailing/models/mailing_contact.py b/addons/mass_mailing/models/mailing_contact.py index 966a5f29aaa..5c0e6a3a860 100644 --- a/addons/mass_mailing/models/mailing_contact.py +++ b/addons/mass_mailing/models/mailing_contact.py @@ -19,7 +19,6 @@ class MassMailingContactListRel(models.Model): opt_out = fields.Boolean(string='Opt Out', help='The contact has chosen not to receive mails anymore from this list', default=False) unsubscription_date = fields.Datetime(string='Unsubscription Date') - contact_count = fields.Integer(related='list_id.contact_nbr', store=False, readonly=False) message_bounce = fields.Integer(related='contact_id.message_bounce', store=False, readonly=False) is_blacklisted = fields.Boolean(related='contact_id.is_blacklisted', store=False, readonly=False) @@ -39,19 +38,6 @@ class MassMailingContactListRel(models.Model): vals['unsubscription_date'] = vals['opt_out'] and fields.Datetime.now() return super(MassMailingContactListRel, self).write(vals) - def action_open_mailing_list_contact(self): - """TODO DBE : To remove - Deprecated""" - contact_id = self.contact_id - action = { - 'name': _(contact_id.name), - 'type': 'ir.actions.act_window', - 'res_model': 'mailing.contact', - 'view_mode': 'form', - 'target': 'current', - 'res_id': contact_id.id - } - return action - class MassMailingContact(models.Model): """Model of a contact. This model is different from the partner model @@ -67,8 +53,7 @@ class MassMailingContact(models.Model): name = fields.Char() company_name = fields.Char(string='Company Name') title_id = fields.Many2one('res.partner.title', string='Title') - email = fields.Char(required=True) - is_email_valid = fields.Boolean(compute='_compute_is_email_valid', store=True) + email = fields.Char('Email') list_ids = fields.Many2many( 'mailing.list', 'mailing_contact_list_rel', 'contact_id', 'list_id', string='Mailing Lists') @@ -79,12 +64,6 @@ class MassMailingContact(models.Model): help='Opt out flag for a specific mailing list.' 'This field should not be used in a view without a unique and active mailing list context.') - @api.depends('email') - def _compute_is_email_valid(self): - for record in self: - normalized = tools.email_normalize(record.email) - record.is_email_valid = normalized if not normalized else True - @api.model def _search_opt_out(self, operator, value): # Assumes operator is '=' or '!=' and value is True or False diff --git a/addons/mass_mailing/models/mailing_list.py b/addons/mass_mailing/models/mailing_list.py index ad78b7ad85f..7d171692a6c 100644 --- a/addons/mass_mailing/models/mailing_list.py +++ b/addons/mass_mailing/models/mailing_list.py @@ -45,6 +45,13 @@ class MassMailingList(models.Model): def name_get(self): return [(list.id, "%s (%s)" % (list.name, list.contact_nbr)) for list in self] + def action_view_contacts(self): + action = self.env.ref('mass_mailing.action_view_mass_mailing_contacts').read()[0] + action['domain'] = [('list_ids', 'in', self.ids)] + context = dict(self.env.context, search_default_filter_valid_email_recipient=1, default_list_ids=self.ids) + action['context'] = context + return action + def action_merge(self, src_lists, archive): """ Insert all the contact from the mailing lists 'src_lists' to the diff --git a/addons/mass_mailing/models/mailing_trace.py b/addons/mass_mailing/models/mailing_trace.py index 6245d0093ff..38185845545 100644 --- a/addons/mass_mailing/models/mailing_trace.py +++ b/addons/mass_mailing/models/mailing_trace.py @@ -11,9 +11,11 @@ class MailingTrace(models.Model): without loosing the statistics about them. """ _name = 'mailing.trace' _description = 'Mailing Statistics' - _rec_name = 'message_id' - _order = 'message_id' + _rec_name = 'id' + _order = 'scheduled DESC' + trace_type = fields.Selection([('mail', 'Mail')], string='Type', default='mail', required=True) + display_name = fields.Char(compute='_compute_display_name') # mail data mail_mail_id = fields.Many2one('mail.mail', string='Mail', index=True) mail_mail_id_int = fields.Integer( @@ -23,16 +25,16 @@ class MailingTrace(models.Model): 'However the ID is needed for several action and controllers.', index=True, ) - email = fields.Char(string="Recipient email address") + email = fields.Char(string="Email") message_id = fields.Char(string='Message-ID') # document model = fields.Char(string='Document model') res_id = fields.Integer(string='Document ID') # campaign / wave data - mass_mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing', index=True) + mass_mailing_id = fields.Many2one('mailing.mailing', string='Mailing', index=True) campaign_id = fields.Many2one( related='mass_mailing_id.campaign_id', - string='Mass Mailing Campaign', + string='Campaign', store=True, readonly=True, index=True) # Bounce and tracking ignored = fields.Datetime(help='Date when the email has been invalidated. ' @@ -55,10 +57,21 @@ class MailingTrace(models.Model): ('replied', 'Replied'), ('bounced', 'Bounced'), ('ignored', 'Ignored')], store=True) + failure_type = fields.Selection(selection=[ + ("SMTP", "Connection failed (outgoing mail server problem)"), + ("RECIPIENT", "Invalid email address"), + ("BOUNCE", "Email address rejected by destination"), + ("UNKNOWN", "Unknown error"), + ], string='Failure type') state_update = fields.Datetime(compute="_compute_state", string='State Update', help='Last state update of the mail', store=True) + @api.depends('trace_type', 'mass_mailing_id') + def _compute_display_name(self): + for trace in self: + trace.display_name = '%s: %s (%s)' % (trace.trace_type, trace.mass_mailing_id.name, trace.id) + @api.depends('sent', 'opened', 'clicked', 'replied', 'bounced', 'exception', 'ignored') def _compute_state(self): self.update({'state_update': fields.Datetime.now()}) @@ -78,12 +91,12 @@ class MailingTrace(models.Model): else: stat.state = 'outgoing' - @api.model - def create(self, values): - if 'mail_mail_id' in values: - values['mail_mail_id_int'] = values['mail_mail_id'] - res = super(MailingTrace, self).create(values) - return res + @api.model_create_multi + def create(self, values_list): + for values in values_list: + if 'mail_mail_id' in values: + values['mail_mail_id_int'] = values['mail_mail_id'] + return super(MailingTrace, self).create(values_list) def _get_records(self, mail_mail_ids=None, mail_message_ids=None, domain=None): if not self.ids and mail_mail_ids: diff --git a/addons/mass_mailing/models/utm.py b/addons/mass_mailing/models/utm.py index 9418dd914f2..503f08e6c95 100644 --- a/addons/mass_mailing/models/utm.py +++ b/addons/mass_mailing/models/utm.py @@ -3,6 +3,7 @@ from odoo import api, fields, models + class UtmCampaign(models.Model): _inherit = 'utm.campaign' diff --git a/addons/mass_mailing/report/mailing_trace_report.py b/addons/mass_mailing/report/mailing_trace_report.py index 66f37c83599..fa2ead4201b 100644 --- a/addons/mass_mailing/report/mailing_trace_report.py +++ b/addons/mass_mailing/report/mailing_trace_report.py @@ -9,18 +9,22 @@ class MailingTraceReport(models.Model): _auto = False _description = 'Mass Mailing Statistics' - scheduled_date = fields.Datetime(string='Scheduled Date', readonly=True) + # mailing name = fields.Char(string='Mass Mail', readonly=True) + mailing_type = fields.Selection([('mail', 'Mail')], string='Type', default='mail', required=True) campaign = fields.Char(string='Mass Mail Campaign', readonly=True) + scheduled_date = fields.Datetime(string='Scheduled Date', readonly=True) + state = fields.Selection( + [('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')], + string='Status', readonly=True) + email_from = fields.Char('From', readonly=True) + # traces sent = fields.Integer(readonly=True) delivered = fields.Integer(readonly=True) opened = fields.Integer(readonly=True) - bounced = fields.Integer(readonly=True) replied = fields.Integer(readonly=True) clicked = fields.Integer(readonly=True) - state = fields.Selection([('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')], - string='Status', readonly=True) - email_from = fields.Char('From', readonly=True) + bounced = fields.Integer(readonly=True) def init(self): """Mass Mail Statistical Report: based on mailing.trace that models the various @@ -30,22 +34,23 @@ class MailingTraceReport(models.Model): self.env.cr.execute(""" CREATE OR REPLACE VIEW mailing_trace_report AS ( SELECT - min(ms.id) as id, - ms.scheduled as scheduled_date, + min(trace.id) as id, utm_source.name as name, + mailing.mailing_type, utm_campaign.name as campaign, - count(ms.bounced) as bounced, - count(ms.sent) as sent, - (count(ms.sent) - count(ms.bounced)) as delivered, - count(ms.opened) as opened, - count(ms.replied) as replied, - count(ms.clicked) as clicked, - mm.state, - mm.email_from + trace.scheduled as scheduled_date, + mailing.state, + mailing.email_from, + count(trace.sent) as sent, + (count(trace.sent) - count(trace.bounced)) as delivered, + count(trace.opened) as opened, + count(trace.replied) as replied, + count(trace.clicked) as clicked, + count(trace.bounced) as bounced FROM - mailing_trace as ms - left join mailing_mailing as mm ON (ms.mass_mailing_id=mm.id) - left join utm_campaign as utm_campaign ON (mm.campaign_id = utm_campaign.id) - left join utm_source as utm_source ON (mm.source_id = utm_source.id) - GROUP BY ms.scheduled, utm_source.name, utm_campaign.name, mm.state, mm.email_from + mailing_trace as trace + left join mailing_mailing as mailing ON (trace.mass_mailing_id=mailing.id) + left join utm_campaign as utm_campaign ON (mailing.campaign_id = utm_campaign.id) + left join utm_source as utm_source ON (mailing.source_id = utm_source.id) + GROUP BY trace.scheduled, utm_source.name, utm_campaign.name, mailing.mailing_type, mailing.state, mailing.email_from )""") diff --git a/addons/mass_mailing/report/mailing_trace_report_views.xml b/addons/mass_mailing/report/mailing_trace_report_views.xml index 0840fcb2254..0ec3e88e483 100644 --- a/addons/mass_mailing/report/mailing_trace_report_views.xml +++ b/addons/mass_mailing/report/mailing_trace_report_views.xml @@ -51,14 +51,16 @@ - + Mass Mailing Analysis mailing.trace.report + [('mailing_type', '=', 'mail')] graph,pivot - Mass Mailing Statistics allows you to check different mailing related information like number of bounced mails, opened mails, replied mails. You can sort out your analysis by different groups to get accurate grained analysis. +

Mass Mailing Statistics allows you to check different mailing related information like number of bounced mails, opened mails, replied mails. You can sort out your analysis by different groups to get accurate grained analysis.

- + parent="mass_mailing_menu_root" + action="mailing_trace_report_action_mail" + groups="mass_mailing.group_mass_mailing_user"/> diff --git a/addons/mass_mailing/static/tests/mass_mailing_html_tests.js b/addons/mass_mailing/static/tests/mass_mailing_html_tests.js index bf1d725dd90..ee5b45c1b0d 100644 --- a/addons/mass_mailing/static/tests/mass_mailing_html_tests.js +++ b/addons/mass_mailing/static/tests/mass_mailing_html_tests.js @@ -156,7 +156,7 @@ QUnit.test('save and edit arch wysiwyg', async function (assert) { data: this.data, arch: '
' + '
' + - '
' + ' test@odoo.com "keep_archives": True, }) - mass_mailing.put_in_queue() + mass_mailing.action_put_in_queue() mass_mailing._process_mass_mailing_queue() sent_mails = self.env['mail.mail'].search([('mailing_id', '=', mass_mailing.id)]) diff --git a/addons/mass_mailing/views/link_tracker_views.xml b/addons/mass_mailing/views/link_tracker_views.xml index db267948159..295afdcdd31 100644 --- a/addons/mass_mailing/views/link_tracker_views.xml +++ b/addons/mass_mailing/views/link_tracker_views.xml @@ -12,7 +12,6 @@ -
@@ -66,7 +65,6 @@ -
diff --git a/addons/mass_mailing/views/mailing_contact_views.xml b/addons/mass_mailing/views/mailing_contact_views.xml index 9a0b8989628..002ba900e7b 100644 --- a/addons/mass_mailing/views/mailing_contact_views.xml +++ b/addons/mass_mailing/views/mailing_contact_views.xml @@ -6,7 +6,7 @@ mailing.contact.subscription 10 - + @@ -30,13 +30,12 @@ mailing.contact.subscription.view.tree mailing.contact.subscription - + - @@ -131,7 +131,7 @@
- +
diff --git a/addons/mass_mailing/views/mailing_mailing_views.xml b/addons/mass_mailing/views/mailing_mailing_views.xml index 0242e7e193b..fdcaf0bd4f2 100644 --- a/addons/mass_mailing/views/mailing_mailing_views.xml +++ b/addons/mass_mailing/views/mailing_mailing_views.xml @@ -7,6 +7,7 @@ + + @@ -48,55 +50,52 @@
-
-
+ -
+ -
- Powered by - - Odoo + Powered by Odoo