[MERGE] forward port of branch 8.0 up to e883193
[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
22 import datetime
23 import pytz
24
25 from openerp import models, fields, api, _
26 from openerp.exceptions import Warning
27
28
29 class event_type(models.Model):
30     """ Event Type """
31     _name = 'event.type'
32     _description = 'Event Type'
33
34     name = fields.Char(string='Event Type', required=True)
35     default_reply_to = fields.Char(string='Default Reply-To',
36         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.")
37     default_email_event = fields.Many2one('email.template', string='Event Confirmation Email',
38         help="It will select this default confirmation event mail value when you choose this event")
39     default_email_registration = fields.Many2one('email.template', string='Registration Confirmation Email',
40         help="It will select this default confirmation registration mail value when you choose this event")
41     default_registration_min = fields.Integer(string='Default Minimum Registration', default=0,
42         help="It will select this default minimum value when you choose this event")
43     default_registration_max = fields.Integer(string='Default Maximum Registration', default=0,
44         help="It will select this default maximum value when you choose this event")
45
46
47 class event_event(models.Model):
48     """Event"""
49     _name = 'event.event'
50     _description = 'Event'
51     _inherit = ['mail.thread', 'ir.needaction_mixin']
52     _order = 'date_begin'
53
54     name = fields.Char(string='Event Name', translate=True, required=True,
55         readonly=False, states={'done': [('readonly', True)]})
56     user_id = fields.Many2one('res.users', string='Responsible User',
57         default=lambda self: self.env.user,
58         readonly=False, states={'done': [('readonly', True)]})
59     type = fields.Many2one('event.type', string='Type of Event',
60         readonly=False, states={'done': [('readonly', True)]})
61     color = fields.Integer('Kanban Color Index')
62     seats_max = fields.Integer(string='Maximum Available Seats', oldname='register_max',
63         readonly=True, states={'draft': [('readonly', False)]},
64         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 )")
65     seats_min = fields.Integer(string='Minimum Reserved Seats', oldname='register_min',
66         readonly=True, states={'draft': [('readonly', False)]},
67         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 )")
68
69     seats_reserved = fields.Integer(oldname='register_current', string='Reserved Seats',
70         store=True, readonly=True, compute='_compute_seats')
71     seats_available = fields.Integer(oldname='register_avail', string='Available Seats',
72         store=True, readonly=True, compute='_compute_seats')
73     seats_unconfirmed = fields.Integer(oldname='register_prospect', string='Unconfirmed Seat Reservations',
74         store=True, readonly=True, compute='_compute_seats')
75     seats_used = fields.Integer(oldname='register_attended', string='Number of Participations',
76         store=True, readonly=True, compute='_compute_seats')
77
78     @api.multi
79     @api.depends('seats_max', 'registration_ids.state', 'registration_ids.nb_register')
80     def _compute_seats(self):
81         """ Determine reserved, available, reserved but unconfirmed and used seats. """
82         # initialize fields to 0
83         for event in self:
84             event.seats_unconfirmed = event.seats_reserved = event.seats_used = 0
85         # aggregate registrations by event and by state
86         if self.ids:
87             state_field = {
88                 'draft': 'seats_unconfirmed',
89                 'open':'seats_reserved',
90                 'done': 'seats_used',
91             }
92             query = """ SELECT event_id, state, sum(nb_register)
93                         FROM event_registration
94                         WHERE event_id IN %s AND state IN ('draft', 'open', 'done')
95                         GROUP BY event_id, state
96                     """
97             self._cr.execute(query, (tuple(self.ids),))
98             for event_id, state, num in self._cr.fetchall():
99                 event = self.browse(event_id)
100                 event[state_field[state]] += num
101         # compute seats_available
102         for event in self:
103             event.seats_available = \
104                 event.seats_max - (event.seats_reserved + event.seats_used) \
105                 if event.seats_max > 0 else 0
106
107     registration_ids = fields.One2many('event.registration', 'event_id', string='Registrations',
108         readonly=False, states={'done': [('readonly', True)]})
109     count_registrations = fields.Integer(string='Registrations',
110         compute='_count_registrations')
111
112     date_begin = fields.Datetime(string='Start Date', required=True,
113         readonly=True, states={'draft': [('readonly', False)]})
114     date_end = fields.Datetime(string='End Date', required=True,
115         readonly=True, states={'draft': [('readonly', False)]})
116
117     @api.model
118     def _tz_get(self):
119         return [(x, x) for x in pytz.all_timezones]
120
121     date_tz = fields.Selection('_tz_get', string='Timezone',
122                         default=lambda self: self._context.get('tz', 'UTC'))
123
124     @api.one
125     @api.depends('date_tz', 'date_begin')
126     def _compute_date_begin_tz(self):
127         if self.date_begin:
128             self_in_tz = self.with_context(tz=(self.date_tz or 'UTC'))
129             date_begin = fields.Datetime.from_string(self.date_begin)
130             self.date_begin_located = fields.Datetime.to_string(fields.Datetime.context_timestamp(self_in_tz, date_begin))
131         else:
132             self.date_begin_located = False
133
134     @api.one
135     @api.depends('date_tz', 'date_end')
136     def _compute_date_end_tz(self):
137         if self.date_end:
138             self_in_tz = self.with_context(tz=(self.date_tz or 'UTC'))
139             date_end = fields.Datetime.from_string(self.date_end)
140             self.date_end_located = fields.Datetime.to_string(fields.Datetime.context_timestamp(self_in_tz, date_end))
141         else:
142             self.date_end_located = False
143
144     date_begin_located = fields.Datetime(string='Start Date Located', compute='_compute_date_begin_tz')
145     date_end_located = fields.Datetime(string='End Date Located', compute='_compute_date_end_tz')
146
147     state = fields.Selection([
148             ('draft', 'Unconfirmed'),
149             ('cancel', 'Cancelled'),
150             ('confirm', 'Confirmed'),
151             ('done', 'Done')
152         ], string='Status', default='draft', readonly=True, required=True, copy=False,
153         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'.")
154     email_registration_id = fields.Many2one(
155         'email.template', string='Registration Confirmation Email',
156         domain=[('model', '=', 'event.registration')],
157         help='This field contains the template of the mail that will be automatically sent each time a registration for this event is confirmed.')
158     email_confirmation_id = fields.Many2one(
159         'email.template', string='Event Confirmation Email',
160         domain=[('model', '=', 'event.registration')],
161         help="If you set an email template, each participant will receive this email announcing the confirmation of the event.")
162     reply_to = fields.Char(string='Reply-To Email',
163         readonly=False, states={'done': [('readonly', True)]},
164         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.")
165     address_id = fields.Many2one('res.partner', string='Location',
166         default=lambda self: self.env.user.company_id.partner_id,
167         readonly=False, states={'done': [('readonly', True)]})
168     country_id = fields.Many2one('res.country', string='Country', related='address_id.country_id',
169         store=True, readonly=False, states={'done': [('readonly', True)]})
170     description = fields.Html(string='Description', oldname='note', translate=True,
171         readonly=False, states={'done': [('readonly', True)]})
172     company_id = fields.Many2one('res.company', string='Company', change_default=True,
173         default=lambda self: self.env['res.company']._company_default_get('event.event'),
174         required=False, readonly=False, states={'done': [('readonly', True)]})
175     organizer_id = fields.Many2one('res.partner', string='Organizer',
176         default=lambda self: self.env.user.company_id.partner_id)
177
178     is_subscribed = fields.Boolean(string='Subscribed',
179         compute='_compute_subscribe')
180
181     @api.one
182     @api.depends('registration_ids')
183     def _count_registrations(self):
184         self.count_registrations = len(self.registration_ids)
185
186     @api.one
187     @api.depends('registration_ids.user_id', 'registration_ids.state')
188     def _compute_subscribe(self):
189         """ Determine whether the current user is already subscribed to any event in `self` """
190         user = self.env.user
191         self.is_subscribed = any(
192             reg.user_id == user and reg.state in ('open', 'done')
193             for reg in self.registration_ids
194         )
195
196     @api.multi
197     @api.depends('name', 'date_begin', 'date_end')
198     def name_get(self):
199         result = []
200         for event in self:
201             dates = [dt.split(' ')[0] for dt in [event.date_begin, event.date_end] if dt]
202             dates = sorted(set(dates))
203             result.append((event.id, '%s (%s)' % (event.name, ' - '.join(dates))))
204         return result
205
206     @api.one
207     @api.constrains('seats_max', 'seats_available')
208     def _check_seats_limit(self):
209         if self.seats_max and self.seats_available < 0:
210             raise Warning(_('No more available seats.'))
211
212     @api.one
213     @api.constrains('date_begin', 'date_end')
214     def _check_closing_date(self):
215         if self.date_end < self.date_begin:
216             raise Warning(_('Closing Date cannot be set before Beginning Date.'))
217
218     @api.one
219     def button_draft(self):
220         self.state = 'draft'
221
222     @api.one
223     def button_cancel(self):
224         for event_reg in self.registration_ids:
225             if event_reg.state == 'done':
226                 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."))
227         self.registration_ids.write({'state': 'cancel'})
228         self.state = 'cancel'                
229
230     @api.one
231     def button_done(self):
232         self.state = 'done'
233
234     @api.one
235     def confirm_event(self):
236         if self.email_confirmation_id:
237             # send reminder that will confirm the event for all the people that were already confirmed
238             regs = self.registration_ids.filtered(lambda reg: reg.state not in ('draft', 'cancel'))
239             regs.mail_user_confirm()
240         self.state = 'confirm'
241
242     @api.one
243     def button_confirm(self):
244         """ Confirm Event and send confirmation email to all register peoples """
245         self.confirm_event()
246
247     @api.one
248     def subscribe_to_event(self):
249         """ Subscribe the current user to a given event """
250         user = self.env.user
251         num_of_seats = int(self._context.get('ticket', 1))
252         regs = self.registration_ids.filtered(lambda reg: reg.user_id == user)
253         # the subscription is done as SUPERUSER_ID because in case we share the
254         # kanban view, we want anyone to be able to subscribe
255         if not regs:
256             regs = regs.sudo().create({
257                 'event_id': self.id,
258                 'email': user.email,
259                 'name': user.name,
260                 'user_id': user.id,
261                 'nb_register': num_of_seats,
262             })
263         else:
264             regs.write({'nb_register': num_of_seats})
265         regs.sudo().confirm_registration()
266
267     @api.one
268     def unsubscribe_to_event(self):
269         """ Unsubscribe the current user from a given event """
270         # the unsubscription is done as SUPERUSER_ID because in case we share
271         # the kanban view, we want anyone to be able to unsubscribe
272         user = self.env.user
273         regs = self.sudo().registration_ids.filtered(lambda reg: reg.user_id == user)
274         regs.button_reg_cancel()
275
276     @api.onchange('type')
277     def _onchange_type(self):
278         if self.type:
279             self.reply_to = self.type.default_reply_to
280             self.email_registration_id = self.type.default_email_registration
281             self.email_confirmation_id = self.type.default_email_event
282             self.seats_min = self.type.default_registration_min
283             self.seats_max = self.type.default_registration_max
284
285     @api.multi
286     def action_event_registration_report(self):
287         res = self.env['ir.actions.act_window'].for_xml_id('event', 'action_report_event_registration')
288         res['context'] = {
289             "search_default_event_id": self.id,
290             "group_by": ['event_date:day'],
291         }
292         return res
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: