[FIX] report: do not fail if PDF cannot be saved as attachment due to AccessError
[odoo/odoo.git] / addons / event / event.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21 from datetime import timedelta
22
23 import pytz
24
25 from openerp import models, fields, api, _
26 from openerp.exceptions import Warning
27
28 class event_type(models.Model):
29     """ Event Type """
30     _name = 'event.type'
31     _description = 'Event Type'
32
33     name = fields.Char(string='Event Type', required=True)
34     default_reply_to = fields.Char(string='Default Reply-To',
35         help="The email address of the organizer which is put in the 'Reply-To' of all emails sent automatically at event or registrations confirmation. You can also put your email address of your mail gateway if you use one.")
36     default_email_event = fields.Many2one('email.template', string='Event Confirmation Email',
37         help="It will select this default confirmation event mail value when you choose this event")
38     default_email_registration = fields.Many2one('email.template', string='Registration Confirmation Email',
39         help="It will select this default confirmation registration mail value when you choose this event")
40     default_registration_min = fields.Integer(string='Default Minimum Registration', default=0,
41         help="It will select this default minimum value when you choose this event")
42     default_registration_max = fields.Integer(string='Default Maximum Registration', default=0,
43         help="It will select this default maximum value when you choose this event")
44
45
46 class event_event(models.Model):
47     """Event"""
48     _name = 'event.event'
49     _description = 'Event'
50     _inherit = ['mail.thread', 'ir.needaction_mixin']
51     _order = 'date_begin'
52
53     name = fields.Char(string='Event Name', translate=True, required=True,
54         readonly=False, states={'done': [('readonly', True)]})
55     user_id = fields.Many2one('res.users', string='Responsible User',
56         default=lambda self: self.env.user,
57         readonly=False, states={'done': [('readonly', True)]})
58     type = fields.Many2one('event.type', string='Type of Event',
59         readonly=False, states={'done': [('readonly', True)]})
60     seats_max = fields.Integer(string='Maximum Available Seats', oldname='register_max',
61         readonly=True, states={'draft': [('readonly', False)]},
62         help="You can for each event define a maximum registration level. If you have too much registrations you are not able to confirm your event. (put 0 to ignore this rule )")
63     seats_min = fields.Integer(string='Minimum Reserved Seats', oldname='register_min',
64         readonly=True, states={'draft': [('readonly', False)]},
65         help="You can for each event define a minimum registration level. If you do not enough registrations you are not able to confirm your event. (put 0 to ignore this rule )")
66
67     seats_reserved = fields.Integer(oldname='register_current', string='Reserved Seats',
68         store=True, readonly=True, compute='_compute_seats')
69     seats_available = fields.Integer(oldname='register_avail', string='Available Seats',
70         store=True, readonly=True, compute='_compute_seats')
71     seats_unconfirmed = fields.Integer(oldname='register_prospect', string='Unconfirmed Seat Reservations',
72         store=True, readonly=True, compute='_compute_seats')
73     seats_used = fields.Integer(oldname='register_attended', string='Number of Participations',
74         store=True, readonly=True, compute='_compute_seats')
75
76     @api.multi
77     @api.depends('seats_max', 'registration_ids.state', 'registration_ids.nb_register')
78     def _compute_seats(self):
79         """ Determine reserved, available, reserved but unconfirmed and used seats. """
80         # initialize fields to 0
81         for event in self:
82             event.seats_unconfirmed = event.seats_reserved = event.seats_used = 0
83         # aggregate registrations by event and by state
84         if self.ids:
85             state_field = {
86                 'draft': 'seats_unconfirmed',
87                 'open':'seats_reserved',
88                 'done': 'seats_used',
89             }
90             query = """ SELECT event_id, state, sum(nb_register)
91                         FROM event_registration
92                         WHERE event_id IN %s AND state IN ('draft', 'open', 'done')
93                         GROUP BY event_id, state
94                     """
95             self._cr.execute(query, (tuple(self.ids),))
96             for event_id, state, num in self._cr.fetchall():
97                 event = self.browse(event_id)
98                 event[state_field[state]] += num
99         # compute seats_available
100         for event in self:
101             event.seats_available = \
102                 event.seats_max - (event.seats_reserved + event.seats_used) \
103                 if event.seats_max > 0 else 0
104
105     registration_ids = fields.One2many('event.registration', 'event_id', string='Registrations',
106         readonly=False, states={'done': [('readonly', True)]})
107     count_registrations = fields.Integer(string='Registrations',
108         compute='_count_registrations')
109
110     date_begin = fields.Datetime(string='Start Date', required=True,
111         readonly=True, states={'draft': [('readonly', False)]})
112     date_end = fields.Datetime(string='End Date', required=True,
113         readonly=True, states={'draft': [('readonly', False)]})
114
115     @api.model
116     def _tz_get(self):
117         return [(x, x) for x in pytz.all_timezones]
118
119     date_tz = fields.Selection('_tz_get', string='Timezone',
120                         default=lambda self: self._context.get('tz', 'UTC'))
121
122     @api.one
123     @api.depends('date_tz', 'date_begin')
124     def _compute_date_begin_tz(self):
125         if self.date_begin:
126             self_in_tz = self.with_context(tz=(self.date_tz or 'UTC'))
127             date_begin = fields.Datetime.from_string(self.date_begin)
128             self.date_begin_located = fields.Datetime.to_string(fields.Datetime.context_timestamp(self_in_tz, date_begin))
129         else:
130             self.date_begin_located = False
131
132     @api.one
133     @api.depends('date_tz', 'date_end')
134     def _compute_date_end_tz(self):
135         if self.date_end:
136             self_in_tz = self.with_context(tz=(self.date_tz or 'UTC'))
137             date_end = fields.Datetime.from_string(self.date_end)
138             self.date_end_located = fields.Datetime.to_string(fields.Datetime.context_timestamp(self_in_tz, date_end))
139         else:
140             self.date_end_located = False
141
142     @api.one
143     @api.depends('address_id')
144     def _compute_country(self):
145         self.country_id = self.address_id.country_id
146
147     date_begin_located = fields.Datetime(string='Start Date Located', compute='_compute_date_begin_tz')
148     date_end_located = fields.Datetime(string='End Date Located', compute='_compute_date_end_tz')
149
150     state = fields.Selection([
151             ('draft', 'Unconfirmed'),
152             ('cancel', 'Cancelled'),
153             ('confirm', 'Confirmed'),
154             ('done', 'Done')
155         ], string='Status', default='draft', readonly=True, required=True, copy=False,
156         help="If event is created, the status is 'Draft'. If event is confirmed for the particular dates the status is set to 'Confirmed'. If the event is over, the status is set to 'Done'. If event is cancelled the status is set to 'Cancelled'.")
157     email_registration_id = fields.Many2one(
158         'email.template', string='Registration Confirmation Email',
159         domain=[('model', '=', 'event.registration')],
160         help='This field contains the template of the mail that will be automatically sent each time a registration for this event is confirmed.')
161     email_confirmation_id = fields.Many2one(
162         'email.template', string='Event Confirmation Email',
163         domain=[('model', '=', 'event.registration')],
164         help="If you set an email template, each participant will receive this email announcing the confirmation of the event.")
165     reply_to = fields.Char(string='Reply-To Email',
166         readonly=False, states={'done': [('readonly', True)]},
167         help="The email address of the organizer is likely to be put here, with the effect to be in the 'Reply-To' of the mails sent automatically at event or registrations confirmation. You can also put the email address of your mail gateway if you use one.")
168     address_id = fields.Many2one('res.partner', string='Location',
169         default=lambda self: self.env.user.company_id.partner_id,
170         readonly=False, states={'done': [('readonly', True)]})
171     country_id = fields.Many2one('res.country', string='Country',
172         store=True, compute='_compute_country')
173     description = fields.Html(string='Description', oldname='note', translate=True,
174         readonly=False, states={'done': [('readonly', True)]})
175     company_id = fields.Many2one('res.company', string='Company', change_default=True,
176         default=lambda self: self.env['res.company']._company_default_get('event.event'),
177         required=False, readonly=False, states={'done': [('readonly', True)]})
178     organizer_id = fields.Many2one('res.partner', string='Organizer',
179         default=lambda self: self.env.user.company_id.partner_id)
180
181     is_subscribed = fields.Boolean(string='Subscribed',
182         compute='_compute_subscribe')
183
184     @api.one
185     @api.depends('registration_ids')
186     def _count_registrations(self):
187         self.count_registrations = len(self.registration_ids)
188
189     @api.one
190     @api.depends('registration_ids.user_id', 'registration_ids.state')
191     def _compute_subscribe(self):
192         """ Determine whether the current user is already subscribed to any event in `self` """
193         user = self.env.user
194         self.is_subscribed = any(
195             reg.user_id == user and reg.state in ('open', 'done')
196             for reg in self.registration_ids
197         )
198
199     @api.multi
200     @api.depends('name', 'date_begin', 'date_end')
201     def name_get(self):
202         result = []
203         for event in self:
204             dates = [dt.split(' ')[0] for dt in [event.date_begin, event.date_end] if dt]
205             dates = sorted(set(dates))
206             result.append((event.id, '%s (%s)' % (event.name, ' - '.join(dates))))
207         return result
208
209     @api.one
210     @api.constrains('seats_max', 'seats_available')
211     def _check_seats_limit(self):
212         if self.seats_max and self.seats_available < 0:
213             raise Warning(_('No more available seats.'))
214
215     @api.one
216     @api.constrains('date_begin', 'date_end')
217     def _check_closing_date(self):
218         if self.date_end < self.date_begin:
219             raise Warning(_('Closing Date cannot be set before Beginning Date.'))
220
221     @api.one
222     def button_draft(self):
223         self.state = 'draft'
224
225     @api.one
226     def button_cancel(self):
227         for event_reg in self.registration_ids:
228             if event_reg.state == 'done':
229                 raise Warning(_("You have already set a registration for this event as 'Attended'. Please reset it to draft if you want to cancel this event."))
230         self.registration_ids.write({'state': 'cancel'})
231         self.state = 'cancel'                
232
233     @api.one
234     def button_done(self):
235         self.state = 'done'
236
237     @api.one
238     def confirm_event(self):
239         if self.email_confirmation_id:
240             # send reminder that will confirm the event for all the people that were already confirmed
241             regs = self.registration_ids.filtered(lambda reg: reg.state not in ('draft', 'cancel'))
242             regs.mail_user_confirm()
243         self.state = 'confirm'
244
245     @api.one
246     def button_confirm(self):
247         """ Confirm Event and send confirmation email to all register peoples """
248         self.confirm_event()
249
250     @api.one
251     def subscribe_to_event(self):
252         """ Subscribe the current user to a given event """
253         user = self.env.user
254         num_of_seats = int(self._context.get('ticket', 1))
255         regs = self.registration_ids.filtered(lambda reg: reg.user_id == user)
256         # the subscription is done as SUPERUSER_ID because in case we share the
257         # kanban view, we want anyone to be able to subscribe
258         if not regs:
259             regs = regs.sudo().create({
260                 'event_id': self.id,
261                 'email': user.email,
262                 'name':user.name,
263                 'user_id': user.id,
264                 'nb_register': num_of_seats,
265             })
266         else:
267             regs.write({'nb_register': num_of_seats})
268         regs.sudo().confirm_registration()
269
270     @api.one
271     def unsubscribe_to_event(self):
272         """ Unsubscribe the current user from a given event """
273         # the unsubscription is done as SUPERUSER_ID because in case we share
274         # the kanban view, we want anyone to be able to unsubscribe
275         user = self.env.user
276         regs = self.sudo().registration_ids.filtered(lambda reg: reg.user_id == user)
277         regs.button_reg_cancel()
278
279     @api.onchange('type')
280     def _onchange_type(self):
281         if self.type:
282             self.reply_to = self.type.default_reply_to
283             self.email_registration_id = self.type.default_email_registration
284             self.email_confirmation_id = self.type.default_email_event
285             self.seats_min = self.type.default_registration_min
286             self.seats_max = self.type.default_registration_max
287
288     @api.onchange('date_begin')
289     def _onchange_date_begin(self):
290         if self.date_begin and not self.date_end:
291             date_begin = fields.Datetime.from_string(self.date_begin)
292             self.date_end = fields.Datetime.to_string(date_begin + timedelta(hours=1))
293
294
295 class event_registration(models.Model):
296     _name = 'event.registration'
297     _description = 'Event Registration'
298     _inherit = ['mail.thread', 'ir.needaction_mixin']
299     _order = 'name, create_date desc'
300
301     origin = fields.Char(string='Source Document', readonly=True,
302         help="Reference of the sales order which created the registration")
303     nb_register = fields.Integer(string='Number of Participants', required=True, default=1,
304         readonly=True, states={'draft': [('readonly', False)]})
305     event_id = fields.Many2one('event.event', string='Event', required=True,
306         readonly=True, states={'draft': [('readonly', False)]})
307     partner_id = fields.Many2one('res.partner', string='Partner',
308         states={'done': [('readonly', True)]})
309     date_open = fields.Datetime(string='Registration Date', readonly=True)
310     date_closed = fields.Datetime(string='Attended Date', readonly=True)
311     reply_to = fields.Char(string='Reply-to Email', related='event_id.reply_to',
312         readonly=True)
313     log_ids = fields.One2many('mail.message', 'res_id', string='Logs',
314         domain=[('model', '=', _name)])
315     event_begin_date = fields.Datetime(string="Event Start Date", related='event_id.date_begin',
316         readonly=True)
317     event_end_date = fields.Datetime(string="Event End Date", related='event_id.date_end',
318         readonly=True)
319     user_id = fields.Many2one('res.users', string='User', states={'done': [('readonly', True)]})
320     company_id = fields.Many2one('res.company', string='Company', related='event_id.company_id',
321         store=True, readonly=True, states={'draft':[('readonly', False)]})
322     state = fields.Selection([
323             ('draft', 'Unconfirmed'),
324             ('cancel', 'Cancelled'),
325             ('open', 'Confirmed'),
326             ('done', 'Attended'),
327         ], string='Status', default='draft', readonly=True, copy=False)
328     email = fields.Char(string='Email')
329     phone = fields.Char(string='Phone')
330     name = fields.Char(string='Name', select=True)
331
332     @api.one
333     @api.constrains('event_id', 'state', 'nb_register')
334     def _check_seats_limit(self):
335         if self.event_id.seats_max and \
336             self.event_id.seats_available < (self.nb_register if self.state == 'draft' else 0):
337                 raise Warning(_('No more available seats.'))
338
339     @api.one
340     def do_draft(self):
341         self.state = 'draft'
342
343     @api.one
344     def confirm_registration(self):
345         self.event_id.message_post(
346             body=_('New registration confirmed: %s.') % (self.name or ''),
347             subtype="event.mt_event_registration")
348         self.message_post(body=_('Event Registration confirmed.'))
349         self.state = 'open'
350
351     @api.one
352     def registration_open(self):
353         """ Open Registration """
354         self.confirm_registration()
355         self.mail_user()
356
357     @api.one
358     def button_reg_close(self):
359         """ Close Registration """
360         today = fields.Datetime.now()
361         if self.event_id.date_begin <= today:
362             self.write({'state': 'done', 'date_closed': today})
363         else:
364             raise Warning(_("You must wait for the starting day of the event to do this action."))
365
366     @api.one
367     def button_reg_cancel(self):
368         self.state = 'cancel'
369
370     @api.one
371     def mail_user(self):
372         """Send email to user with email_template when registration is done """
373         if self.event_id.state == 'confirm' and self.event_id.email_confirmation_id:
374             self.mail_user_confirm()
375         else:
376             template = self.event_id.email_registration_id
377             if template:
378                 mail_message = template.send_mail(self.id)
379
380     @api.one
381     def mail_user_confirm(self):
382         """Send email to user when the event is confirmed """
383         template = self.event_id.email_confirmation_id
384         if template:
385             mail_message = template.send_mail(self.id)
386
387     @api.onchange('partner_id')
388     def _onchange_partner(self):
389         if self.partner_id:
390             contact_id = self.partner_id.address_get().get('default', False)
391             if contact_id:
392                 contact = self.env['res.partner'].browse(contact_id)
393                 self.name = contact.name
394                 self.email = contact.email
395                 self.phone = contact.phone
396
397 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: