1# -*- coding: utf-8 -*- 2# Part of Odoo. See LICENSE file for full copyright and licensing details. 3 4import logging 5import random 6import threading 7 8from datetime import datetime 9from dateutil.relativedelta import relativedelta 10 11from odoo import api, fields, models, tools 12from odoo.tools import exception_to_unicode 13from odoo.tools.translate import _ 14 15_logger = logging.getLogger(__name__) 16 17_INTERVALS = { 18 'hours': lambda interval: relativedelta(hours=interval), 19 'days': lambda interval: relativedelta(days=interval), 20 'weeks': lambda interval: relativedelta(days=7*interval), 21 'months': lambda interval: relativedelta(months=interval), 22 'now': lambda interval: relativedelta(hours=0), 23} 24 25 26class EventTypeMail(models.Model): 27 """ Template of event.mail to attach to event.type. Those will be copied 28 upon all events created in that type to ease event creation. """ 29 _name = 'event.type.mail' 30 _description = 'Mail Scheduling on Event Category' 31 32 event_type_id = fields.Many2one( 33 'event.type', string='Event Type', 34 ondelete='cascade', required=True) 35 notification_type = fields.Selection([('mail', 'Mail')], string='Send', default='mail', required=True) 36 interval_nbr = fields.Integer('Interval', default=1) 37 interval_unit = fields.Selection([ 38 ('now', 'Immediately'), 39 ('hours', 'Hours'), ('days', 'Days'), 40 ('weeks', 'Weeks'), ('months', 'Months')], 41 string='Unit', default='hours', required=True) 42 interval_type = fields.Selection([ 43 ('after_sub', 'After each registration'), 44 ('before_event', 'Before the event'), 45 ('after_event', 'After the event')], 46 string='Trigger', default="before_event", required=True) 47 template_id = fields.Many2one( 48 'mail.template', string='Email Template', 49 domain=[('model', '=', 'event.registration')], ondelete='restrict', 50 help='This field contains the template of the mail that will be automatically sent') 51 52 @api.model 53 def _get_event_mail_fields_whitelist(self): 54 """ Whitelist of fields that are copied from event_type_mail_ids to event_mail_ids when 55 changing the event_type_id field of event.event """ 56 return ['notification_type', 'template_id', 'interval_nbr', 'interval_unit', 'interval_type'] 57 58 59class EventMailScheduler(models.Model): 60 """ Event automated mailing. This model replaces all existing fields and 61 configuration allowing to send emails on events since Odoo 9. A cron exists 62 that periodically checks for mailing to run. """ 63 _name = 'event.mail' 64 _rec_name = 'event_id' 65 _description = 'Event Automated Mailing' 66 67 event_id = fields.Many2one('event.event', string='Event', required=True, ondelete='cascade') 68 sequence = fields.Integer('Display order') 69 notification_type = fields.Selection([('mail', 'Mail')], string='Send', default='mail', required=True) 70 interval_nbr = fields.Integer('Interval', default=1) 71 interval_unit = fields.Selection([ 72 ('now', 'Immediately'), 73 ('hours', 'Hours'), ('days', 'Days'), 74 ('weeks', 'Weeks'), ('months', 'Months')], 75 string='Unit', default='hours', required=True) 76 interval_type = fields.Selection([ 77 ('after_sub', 'After each registration'), 78 ('before_event', 'Before the event'), 79 ('after_event', 'After the event')], 80 string='Trigger ', default="before_event", required=True) 81 template_id = fields.Many2one( 82 'mail.template', string='Email Template', 83 domain=[('model', '=', 'event.registration')], ondelete='restrict', 84 help='This field contains the template of the mail that will be automatically sent') 85 scheduled_date = fields.Datetime('Scheduled Sent Mail', compute='_compute_scheduled_date', store=True) 86 mail_registration_ids = fields.One2many('event.mail.registration', 'scheduler_id') 87 mail_sent = fields.Boolean('Mail Sent on Event', copy=False) 88 done = fields.Boolean('Sent', compute='_compute_done', store=True) 89 90 @api.depends('mail_sent', 'interval_type', 'event_id.registration_ids', 'mail_registration_ids') 91 def _compute_done(self): 92 for mail in self: 93 if mail.interval_type in ['before_event', 'after_event']: 94 mail.done = mail.mail_sent 95 else: 96 mail.done = len(mail.mail_registration_ids) == len(mail.event_id.registration_ids) and all(mail.mail_sent for mail in mail.mail_registration_ids) 97 98 @api.depends('event_id.date_begin', 'interval_type', 'interval_unit', 'interval_nbr') 99 def _compute_scheduled_date(self): 100 for mail in self: 101 if mail.interval_type == 'after_sub': 102 date, sign = mail.event_id.create_date, 1 103 elif mail.interval_type == 'before_event': 104 date, sign = mail.event_id.date_begin, -1 105 else: 106 date, sign = mail.event_id.date_end, 1 107 108 mail.scheduled_date = date + _INTERVALS[mail.interval_unit](sign * mail.interval_nbr) if date else False 109 110 def execute(self): 111 for mail in self: 112 now = fields.Datetime.now() 113 if mail.interval_type == 'after_sub': 114 # update registration lines 115 lines = [ 116 (0, 0, {'registration_id': registration.id}) 117 for registration in (mail.event_id.registration_ids - mail.mapped('mail_registration_ids.registration_id')) 118 ] 119 if lines: 120 mail.write({'mail_registration_ids': lines}) 121 # execute scheduler on registrations 122 mail.mail_registration_ids.execute() 123 else: 124 # Do not send emails if the mailing was scheduled before the event but the event is over 125 if not mail.mail_sent and mail.scheduled_date <= now and mail.notification_type == 'mail' and \ 126 (mail.interval_type != 'before_event' or mail.event_id.date_end > now): 127 mail.event_id.mail_attendees(mail.template_id.id) 128 mail.write({'mail_sent': True}) 129 return True 130 131 @api.model 132 def _warn_template_error(self, scheduler, exception): 133 # We warn ~ once by hour ~ instead of every 10 min if the interval unit is more than 'hours'. 134 if random.random() < 0.1666 or scheduler.interval_unit in ('now', 'hours'): 135 ex_s = exception_to_unicode(exception) 136 try: 137 event, template = scheduler.event_id, scheduler.template_id 138 emails = list(set([event.organizer_id.email, event.user_id.email, template.write_uid.email])) 139 subject = _("WARNING: Event Scheduler Error for event: %s", event.name) 140 body = _("""Event Scheduler for: 141 - Event: %(event_name)s (%(event_id)s) 142 - Scheduled: %(date)s 143 - Template: %(template_name)s (%(template_id)s) 144 145Failed with error: 146 - %(error)s 147 148You receive this email because you are: 149 - the organizer of the event, 150 - or the responsible of the event, 151 - or the last writer of the template. 152""", 153 event_name=event.name, 154 event_id=event.id, 155 date=scheduler.scheduled_date, 156 template_name=template.name, 157 template_id=template.id, 158 error=ex_s) 159 email = self.env['ir.mail_server'].build_email( 160 email_from=self.env.user.email, 161 email_to=emails, 162 subject=subject, body=body, 163 ) 164 self.env['ir.mail_server'].send_email(email) 165 except Exception as e: 166 _logger.error("Exception while sending traceback by email: %s.\n Original Traceback:\n%s", e, exception) 167 pass 168 169 @api.model 170 def run(self, autocommit=False): 171 schedulers = self.search([('done', '=', False), ('scheduled_date', '<=', datetime.strftime(fields.datetime.now(), tools.DEFAULT_SERVER_DATETIME_FORMAT))]) 172 for scheduler in schedulers: 173 try: 174 with self.env.cr.savepoint(): 175 # Prevent a mega prefetch of the registration ids of all the events of all the schedulers 176 self.browse(scheduler.id).execute() 177 except Exception as e: 178 _logger.exception(e) 179 self.invalidate_cache() 180 self._warn_template_error(scheduler, e) 181 else: 182 if autocommit and not getattr(threading.currentThread(), 'testing', False): 183 self.env.cr.commit() 184 return True 185 186 187class EventMailRegistration(models.Model): 188 _name = 'event.mail.registration' 189 _description = 'Registration Mail Scheduler' 190 _rec_name = 'scheduler_id' 191 _order = 'scheduled_date DESC' 192 193 scheduler_id = fields.Many2one('event.mail', 'Mail Scheduler', required=True, ondelete='cascade') 194 registration_id = fields.Many2one('event.registration', 'Attendee', required=True, ondelete='cascade') 195 scheduled_date = fields.Datetime('Scheduled Time', compute='_compute_scheduled_date', store=True) 196 mail_sent = fields.Boolean('Mail Sent') 197 198 def execute(self): 199 now = fields.Datetime.now() 200 todo = self.filtered(lambda reg_mail: 201 not reg_mail.mail_sent and \ 202 reg_mail.registration_id.state in ['open', 'done'] and \ 203 (reg_mail.scheduled_date and reg_mail.scheduled_date <= now) and \ 204 reg_mail.scheduler_id.notification_type == 'mail' 205 ) 206 for reg_mail in todo: 207 reg_mail.scheduler_id.template_id.send_mail(reg_mail.registration_id.id) 208 todo.write({'mail_sent': True}) 209 210 @api.depends('registration_id', 'scheduler_id.interval_unit', 'scheduler_id.interval_type') 211 def _compute_scheduled_date(self): 212 for mail in self: 213 if mail.registration_id: 214 date_open = mail.registration_id.date_open 215 date_open_datetime = date_open or fields.Datetime.now() 216 mail.scheduled_date = date_open_datetime + _INTERVALS[mail.scheduler_id.interval_unit](mail.scheduler_id.interval_nbr) 217 else: 218 mail.scheduled_date = False 219