Forward port of 7.0 up to rev 9b87d6f
[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 from datetime import datetime, timedelta
23 from openerp.osv import fields, osv
24 from openerp.tools.translate import _
25 from openerp import SUPERUSER_ID
26
27 class event_type(osv.osv):
28     """ Event Type """
29     _name = 'event.type'
30     _description = __doc__
31     _columns = {
32         'name': fields.char('Event Type', size=64, required=True),
33         'default_reply_to': fields.char('Default Reply-To', size=64,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." ),
34         'default_email_event': fields.many2one('email.template','Event Confirmation Email', help="It will select this default confirmation event mail value when you choose this event"),
35         'default_email_registration': fields.many2one('email.template','Registration Confirmation Email', help="It will select this default confirmation registration mail value when you choose this event"),
36         'default_registration_min': fields.integer('Default Minimum Registration', help="It will select this default minimum value when you choose this event"),
37         'default_registration_max': fields.integer('Default Maximum Registration', help="It will select this default maximum value when you choose this event"),
38     }
39     _defaults = {
40         'default_registration_min': 0,
41         'default_registration_max': 0,
42     }
43
44 class event_event(osv.osv):
45     """Event"""
46     _name = 'event.event'
47     _description = __doc__
48     _order = 'date_begin'
49     _inherit = ['mail.thread', 'ir.needaction_mixin']
50
51     def name_get(self, cr, uid, ids, context=None):
52         if not ids:
53             return []
54
55         if isinstance(ids, (long, int)):
56             ids = [ids]
57
58         res = []
59         for record in self.browse(cr, uid, ids, context=context):
60             date = record.date_begin.split(" ")[0]
61             date_end = record.date_end.split(" ")[0]
62             if date != date_end:
63                 date += ' - ' + date_end
64             display_name = record.name + ' (' + date + ')'
65             res.append((record['id'], display_name))
66         return res
67
68     def copy(self, cr, uid, id, default=None, context=None):
69         """ Reset the state and the registrations while copying an event
70         """
71         if not default:
72             default = {}
73         default.update({
74             'state': 'draft',
75             'registration_ids': False,
76         })
77         return super(event_event, self).copy(cr, uid, id, default=default, context=context)
78
79     def button_draft(self, cr, uid, ids, context=None):
80         return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
81
82     def button_cancel(self, cr, uid, ids, context=None):
83         registration = self.pool.get('event.registration')
84         reg_ids = registration.search(cr, uid, [('event_id','in',ids)], context=context)
85         for event_reg in registration.browse(cr,uid,reg_ids,context=context):
86             if event_reg.state == 'done':
87                 raise osv.except_osv(_('Error!'),_("You have already set a registration for this event as 'Attended'. Please reset it to draft if you want to cancel this event.") )
88         registration.write(cr, uid, reg_ids, {'state': 'cancel'}, context=context)
89         return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
90
91     def button_done(self, cr, uid, ids, context=None):
92         return self.write(cr, uid, ids, {'state': 'done'}, context=context)
93
94     def confirm_event(self, cr, uid, ids, context=None):
95         register_pool = self.pool.get('event.registration')
96         for event in self.browse(cr, uid, ids, context=context):
97             if event.email_confirmation_id:
98             #send reminder that will confirm the event for all the people that were already confirmed
99                 reg_ids = register_pool.search(cr, uid, [
100                                    ('event_id', '=', event.id),
101                                    ('state', 'not in', ['draft', 'cancel'])], context=context)
102                 register_pool.mail_user_confirm(cr, uid, reg_ids)
103         return self.write(cr, uid, ids, {'state': 'confirm'}, context=context)
104
105     def button_confirm(self, cr, uid, ids, context=None):
106         """ Confirm Event and send confirmation email to all register peoples
107         """
108         return self.confirm_event(cr, uid, isinstance(ids, (int, long)) and [ids] or ids, context=context)
109
110     def _get_seats(self, cr, uid, ids, fields, args, context=None):
111         """Get reserved, available, reserved but unconfirmed and used seats.
112         @return: Dictionary of function field values.
113         """
114         res = dict([(id, {}) for id in ids])
115         for event in self.browse(cr, uid, ids, context=context):
116             res[event.id]['seats_reserved'] = sum(reg.nb_register for reg in event.registration_ids if reg.state == "open")
117             res[event.id]['seats_used'] = sum(reg.nb_register for reg in event.registration_ids if reg.state == "done")
118             res[event.id]['seats_unconfirmed'] = sum(reg.nb_register for reg in event.registration_ids if reg.state == "draft")
119             res[event.id]['seats_available'] = event.seats_max - \
120                 (res[event.id]['seats_reserved'] + res[event.id]['seats_used']) \
121                 if event.seats_max > 0 else None
122         return res
123
124     def _subscribe_fnc(self, cr, uid, ids, fields, args, context=None):
125         """This functional fields compute if the current user (uid) is already subscribed or not to the event passed in parameter (ids)
126         """
127         register_pool = self.pool.get('event.registration')
128         res = {}
129         for event in self.browse(cr, uid, ids, context=context):
130             res[event.id] = False
131             curr_reg_id = register_pool.search(cr, uid, [('user_id', '=', uid), ('event_id', '=' ,event.id)])
132             if curr_reg_id:
133                 for reg in register_pool.browse(cr, uid, curr_reg_id, context=context):
134                     if reg.state in ('open','done'):
135                         res[event.id]= True
136                         continue
137         return res
138
139     _columns = {
140         'name': fields.char('Event Name', size=64, required=True, translate=True, readonly=False, states={'done': [('readonly', True)]}),
141         'user_id': fields.many2one('res.users', 'Responsible User', readonly=False, states={'done': [('readonly', True)]}),
142         'type': fields.many2one('event.type', 'Type of Event', readonly=False, states={'done': [('readonly', True)]}),
143         'seats_max': fields.integer('Maximum Available Seats', oldname='register_max', 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 )", readonly=True, states={'draft': [('readonly', False)]}),
144         'seats_min': fields.integer('Minimum Reserved Seats', oldname='register_min', 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 )", readonly=True, states={'draft': [('readonly', False)]}),
145         'seats_reserved': fields.function(_get_seats, oldname='register_current', string='Reserved Seats', type='integer', multi='seats_reserved'),
146         'seats_available': fields.function(_get_seats, oldname='register_avail', string='Available Seats', type='integer', multi='seats_reserved'),
147         'seats_unconfirmed': fields.function(_get_seats, oldname='register_prospect', string='Unconfirmed Seat Reservations', type='integer', multi='seats_reserved'),
148         'seats_used': fields.function(_get_seats, oldname='register_attended', string='Number of Participations', type='integer', multi='seats_reserved'),
149         'registration_ids': fields.one2many('event.registration', 'event_id', 'Registrations', readonly=False, states={'done': [('readonly', True)]}),
150         'date_begin': fields.datetime('Start Date', required=True, readonly=True, states={'draft': [('readonly', False)]}),
151         'date_end': fields.datetime('End Date', required=True, readonly=True, states={'draft': [('readonly', False)]}),
152         'state': fields.selection([
153             ('draft', 'Unconfirmed'),
154             ('cancel', 'Cancelled'),
155             ('confirm', 'Confirmed'),
156             ('done', 'Done')],
157             'Status', readonly=True, required=True,
158             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\'.'),
159         'email_registration_id' : fields.many2one('email.template','Registration Confirmation Email', help='This field contains the template of the mail that will be automatically sent each time a registration for this event is confirmed.'),
160         'email_confirmation_id' : fields.many2one('email.template','Event Confirmation Email', help="If you set an email template, each participant will receive this email announcing the confirmation of the event."),
161         'reply_to': fields.char('Reply-To Email', size=64, readonly=False, states={'done': [('readonly', True)]}, 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."),
162         'address_id': fields.many2one('res.partner','Location', readonly=False, states={'done': [('readonly', True)]}),
163         'country_id': fields.related('address_id', 'country_id',
164                     type='many2one', relation='res.country', string='Country', readonly=False, states={'done': [('readonly', True)]}, store=True),
165         'description': fields.html(
166             'Description', readonly=False,
167             states={'done': [('readonly', True)]},
168             oldname='note'),
169         'company_id': fields.many2one('res.company', 'Company', required=False, change_default=True, readonly=False, states={'done': [('readonly', True)]}),
170         'is_subscribed' : fields.function(_subscribe_fnc, type="boolean", string='Subscribed'),
171         'organizer_id': fields.many2one('res.partner', "Organizer"),
172     }
173     _defaults = {
174         'state': 'draft',
175         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'event.event', context=c),
176         'user_id': lambda obj, cr, uid, context: uid,
177         'organizer_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, context=c).company_id.partner_id.id,
178         'address_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, context=c).company_id.partner_id.id
179     }
180
181     def _check_seats_limit(self, cr, uid, ids, context=None):
182         print "event _check_seats_limit"
183         for event in self.browse(cr, uid, ids, context=context):
184             if event.seats_max and event.seats_available < 0:
185                 return False
186         return True
187
188     _constraints = [
189         (_check_seats_limit, 'No more available seats.', ['registration_ids','seats_max']),
190     ]
191
192     def subscribe_to_event(self, cr, uid, ids, context=None):
193         register_pool = self.pool.get('event.registration')
194         user_pool = self.pool.get('res.users')
195         num_of_seats = int(context.get('ticket', 1))
196         user = user_pool.browse(cr, uid, uid, context=context)
197         curr_reg_ids = register_pool.search(cr, uid, [('user_id', '=', user.id), ('event_id', '=' , ids[0])])
198         #the subscription is done with SUPERUSER_ID because in case we share the kanban view, we want anyone to be able to subscribe
199         if not curr_reg_ids:
200             curr_reg_ids = [register_pool.create(cr, SUPERUSER_ID, {'event_id': ids[0] ,'email': user.email, 'name':user.name, 'user_id': user.id, 'nb_register': num_of_seats})]
201         else:
202             register_pool.write(cr, uid, curr_reg_ids, {'nb_register': num_of_seats}, context=context)
203         return register_pool.confirm_registration(cr, SUPERUSER_ID, curr_reg_ids, context=context)
204
205     def unsubscribe_to_event(self, cr, uid, ids, context=None):
206         register_pool = self.pool.get('event.registration')
207         #the unsubscription is done with SUPERUSER_ID because in case we share the kanban view, we want anyone to be able to unsubscribe
208         curr_reg_ids = register_pool.search(cr, SUPERUSER_ID, [('user_id', '=', uid), ('event_id', '=', ids[0])])
209         return register_pool.button_reg_cancel(cr, SUPERUSER_ID, curr_reg_ids, context=context)
210
211     def _check_closing_date(self, cr, uid, ids, context=None):
212         for event in self.browse(cr, uid, ids, context=context):
213             if event.date_end < event.date_begin:
214                 return False
215         return True
216
217     _constraints = [
218         (_check_closing_date, 'Error ! Closing Date cannot be set before Beginning Date.', ['date_end']),
219     ]
220
221     def onchange_event_type(self, cr, uid, ids, type_event, context=None):
222         values = {}
223         if type_event:
224             type_info =  self.pool.get('event.type').browse(cr,uid,type_event,context)
225             dic ={
226               'reply_to': type_info.default_reply_to,
227               'email_registration_id': type_info.default_email_registration.id,
228               'email_confirmation_id': type_info.default_email_event.id,
229               'seats_min': type_info.default_registration_min,
230               'seats_max': type_info.default_registration_max,
231             }
232             values.update(dic)
233         return values
234
235     def onchange_start_date(self, cr, uid, ids, date_begin=False, date_end=False, context=None):
236         res = {'value':{}}
237         if date_end:
238             return res
239         if date_begin and isinstance(date_begin, str):
240             date_begin = datetime.strptime(date_begin, "%Y-%m-%d %H:%M:%S")
241             date_end = date_begin + timedelta(hours=1)
242             res['value'] = {'date_end': date_end.strftime("%Y-%m-%d %H:%M:%S")}
243         return res
244
245
246 class event_registration(osv.osv):
247     """Event Registration"""
248     _name= 'event.registration'
249     _description = __doc__
250     _inherit = ['mail.thread', 'ir.needaction_mixin']
251     _columns = {
252         'id': fields.integer('ID'),
253         'origin': fields.char('Source Document', size=124,readonly=True,help="Reference of the sales order which created the registration"),
254         'nb_register': fields.integer('Number of Participants', required=True, readonly=True, states={'draft': [('readonly', False)]}),
255         'event_id': fields.many2one('event.event', 'Event', required=True, readonly=True, states={'draft': [('readonly', False)]}),
256         'partner_id': fields.many2one('res.partner', 'Partner', states={'done': [('readonly', True)]}),
257         'create_date': fields.datetime('Creation Date' , readonly=True),
258         'date_closed': fields.datetime('Attended Date', readonly=True),
259         'date_open': fields.datetime('Registration Date', readonly=True),
260         'reply_to': fields.related('event_id','reply_to',string='Reply-to Email', type='char', size=128, readonly=True,),
261         'log_ids': fields.one2many('mail.message', 'res_id', 'Logs', domain=[('model','=',_name)]),
262         'event_end_date': fields.related('event_id','date_end', type='datetime', string="Event End Date", readonly=True),
263         'event_begin_date': fields.related('event_id', 'date_begin', type='datetime', string="Event Start Date", readonly=True),
264         'user_id': fields.many2one('res.users', 'User', states={'done': [('readonly', True)]}),
265         'company_id': fields.related('event_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True, states={'draft':[('readonly',False)]}),
266         'state': fields.selection([('draft', 'Unconfirmed'),
267                                     ('cancel', 'Cancelled'),
268                                     ('open', 'Confirmed'),
269                                     ('done', 'Attended')], 'Status',
270                                     size=16, readonly=True),
271         'email': fields.char('Email', size=64),
272         'phone': fields.char('Phone', size=64),
273         'name': fields.char('Name', size=128, select=True),
274     }
275     _defaults = {
276         'nb_register': 1,
277         'state': 'draft',
278     }
279     _order = 'name, create_date desc'
280
281
282     def _check_seats_limit(self, cr, uid, ids, context=None):
283         for registration in self.browse(cr, uid, ids, context=context):
284             if registration.event_id.seats_max and \
285                 registration.event_id.seats_available < (registration.state == 'draft' and registration.nb_register or 0):
286                 return False
287         return True
288
289     _constraints = [
290         (_check_seats_limit, 'No more available seats.', ['event_id','nb_register','state']),
291     ]
292
293     def do_draft(self, cr, uid, ids, context=None):
294         return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
295
296     def confirm_registration(self, cr, uid, ids, context=None):
297         for reg in self.browse(cr, uid, ids, context=context or {}):
298             self.pool.get('event.event').message_post(cr, uid, [reg.event_id.id], body=_('New registration confirmed: %s.') % (reg.name or '', ),subtype="event.mt_event_registration", context=context)
299         return self.write(cr, uid, ids, {'state': 'open'}, context=context)
300
301     def registration_open(self, cr, uid, ids, context=None):
302         """ Open Registration
303         """
304         res = self.confirm_registration(cr, uid, ids, context=context)
305         self.mail_user(cr, uid, ids, context=context)
306         return res
307
308     def button_reg_close(self, cr, uid, ids, context=None):
309         """ Close Registration
310         """
311         if context is None:
312             context = {}
313         today = fields.datetime.now()
314         for registration in self.browse(cr, uid, ids, context=context):
315             if today >= registration.event_id.date_begin:
316                 values = {'state': 'done', 'date_closed': today}
317                 self.write(cr, uid, ids, values)
318             else:
319                 raise osv.except_osv(_('Error!'), _("You must wait for the starting day of the event to do this action."))
320         return True
321
322     def button_reg_cancel(self, cr, uid, ids, context=None, *args):
323         return self.write(cr, uid, ids, {'state': 'cancel'})
324
325     def mail_user(self, cr, uid, ids, context=None):
326         """
327         Send email to user with email_template when registration is done
328         """
329         for registration in self.browse(cr, uid, ids, context=context):
330             if registration.event_id.state == 'confirm' and registration.event_id.email_confirmation_id.id:
331                 self.mail_user_confirm(cr, uid, ids, context=context)
332             else:
333                 template_id = registration.event_id.email_registration_id.id
334                 if template_id:
335                     self.pool.get('email.template').send_mail(cr,uid,template_id,registration.id, context=context)
336         return True
337
338     def mail_user_confirm(self, cr, uid, ids, context=None):
339         """
340         Send email to user when the event is confirmed
341         """
342         for registration in self.browse(cr, uid, ids, context=context):
343             template_id = registration.event_id.email_confirmation_id.id
344             if template_id:
345                 self.pool.get('email.template').send_mail(cr,uid,template_id,registration.id, context=context)
346         return True
347
348     def onchange_contact_id(self, cr, uid, ids, contact, partner, context=None):
349         if not contact:
350             return {}
351         addr_obj = self.pool.get('res.partner')
352         contact_id =  addr_obj.browse(cr, uid, contact, context=context)
353         return {'value': {
354             'email':contact_id.email,
355             'name':contact_id.name,
356             'phone':contact_id.phone,
357             }}
358
359     def onchange_partner_id(self, cr, uid, ids, part, context=None):
360         res_obj = self.pool.get('res.partner')
361         data = {}
362         if not part:
363             return {'value': data}
364         addr = res_obj.address_get(cr, uid, [part]).get('default', False)
365         if addr:
366             d = self.onchange_contact_id(cr, uid, ids, addr, part, context)
367             data.update(d['value'])
368         return {'value': data}
369
370 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: