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