From 47171fea296109a4a821ffc8d00456cd1cd7f903 Mon Sep 17 00:00:00 2001 From: Nikos Tsirintanis Date: Wed, 5 Dec 2018 12:03:30 +0100 Subject: [PATCH] [WIP][ADD] reintroduced seat calculations, added security for new models --- event_session/models/event.py | 74 +++++++++++++++- event_session/models/event_mail.py | 1 - event_session/models/event_session.py | 95 +++++++++++++++++++++ event_session/models/res_config_settings.py | 1 - event_session/security/ir.model.access.csv | 1 + event_session/tests/test_session.py | 83 +++++++++++++++++- event_session/views/event_session_view.xml | 54 +++++++++++- event_session/views/event_view.xml | 13 +++ 8 files changed, 311 insertions(+), 11 deletions(-) diff --git a/event_session/models/event.py b/event_session/models/event.py index cefb3f7e1..7e52cb681 100644 --- a/event_session/models/event.py +++ b/event_session/models/event.py @@ -1,7 +1,8 @@ # Copyright 2017 David Vidal # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError class EventEvent(models.Model): @@ -17,6 +18,28 @@ class EventEvent(models.Model): string='Total event sessions', store=True, ) + seats_expected = fields.Integer(store=True) + seats_available_expected = fields.Integer( + compute='_compute_seats_available_expected', + string='Available expected seats', + readonly=True, + store=True, + ) + draft_state = fields.Integer( + compute='_compute_state_numbers', + string=' # No of Draft Registrations', + store=True, + ) + cancel_state = fields.Integer( + compute='_compute_state_numbers', + string=' # No of Cancelled Registrations', + store=True, + ) + confirm_state = fields.Integer( + compute='_compute_state_numbers', + string=' # No of Confirmed Registrations', + store=True, + ) @api.multi @api.depends('session_ids') @@ -24,6 +47,34 @@ def _compute_sessions_count(self): for event in self: event.sessions_count = len(event.session_ids) + @api.multi + @api.constrains('seats_max', 'seats_available') + def _check_seats_limit(self): + for event in self: + if not event.session_ids: + return super(EventEvent, event)._check_seats_limit() + + @api.multi + @api.depends('seats_max', 'seats_expected') + def _compute_seats_available_expected(self): + for this in self: + seats = this.seats_max - this.seats_expected + this.seats_available_expected = seats + + @api.multi + @api.depends('registration_ids.state') + def _compute_state_numbers(self): + for this in self: + this.draft_state = len(this.registration_ids.filtered( + lambda x: x.state == 'draft' + )) + this.cancel_state = len(this.registration_ids.filtered( + lambda x: x.state == 'cancel' + )) + this.confirm_state = len(this.registration_ids.filtered( + lambda x: x.state == 'confirm' + )) + class EventRegistration(models.Model): _inherit = 'event.registration' @@ -37,3 +88,24 @@ class EventRegistration(models.Model): string='Session', ondelete='restrict', ) + + @api.multi + @api.constrains('event_id', 'session_id', 'state') + def _check_seats_limit(self): + for registration in self.filtered('session_id'): + if (registration.session_id.seats_availability == 'limited' and + registration.session_id.seats_available < 1 and + registration.state == 'open'): + raise ValidationError( + _('No more seats available for this event.')) + + @api.multi + def confirm_registration(self): + for reg in self: + if not reg.event_id.session_ids: + super(EventRegistration, reg).confirm_registration() + reg.state = 'open' + onsubscribe_schedulers = \ + reg.session_id.event_mail_ids.filtered( + lambda s: s.interval_type == 'after_sub') + onsubscribe_schedulers.execute() diff --git a/event_session/models/event_mail.py b/event_session/models/event_mail.py index 077325d55..97e7d369e 100644 --- a/event_session/models/event_mail.py +++ b/event_session/models/event_mail.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 David Vidal # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). diff --git a/event_session/models/event_session.py b/event_session/models/event_session.py index 3e2922dc2..f997005c0 100644 --- a/event_session/models/event_session.py +++ b/event_session/models/event_session.py @@ -27,6 +27,42 @@ class EventSession(models.Model): comodel_name='event.event', string='Event', ) + seats_min = fields.Integer( + string='Minimum seats', + ) + seats_max = fields.Integer( + string="Maximum seats", + ) + seats_availability = fields.Selection( + [('limited', 'Limited'), ('unlimited', 'Unlimited')], + 'Maximum Attendees', required=True, default='unlimited', + ) + seats_reserved = fields.Integer( + string='Reserved Seats', store=True, readonly=True, + compute='_compute_seats', + ) + seats_available = fields.Integer( + oldname='register_avail', string='Available Seats', + store=True, readonly=True, compute='_compute_seats') + seats_unconfirmed = fields.Integer( + oldname='register_prospect', string='Unconfirmed Seat Reservations', + store=True, readonly=True, compute='_compute_seats') + seats_used = fields.Integer( + oldname='register_attended', string='Number of Participants', + store=True, readonly=True, compute='_compute_seats') + seats_expected = fields.Integer( + string='Number of Expected Attendees', + readonly=True, compute='_compute_seats', + store=True) + seats_available_expected = fields.Integer( + string='Available Expected Seats', + readonly=True, compute='_compute_seats', + store=True) + seats_available_pc = fields.Float( + string='Full %', + readonly=True, + compute='_compute_seats', + ) date_tz = fields.Selection( string='Timezone', related="event_id.date_tz", ) @@ -110,6 +146,11 @@ def name_get(self): @api.model def create(self, vals): + # Config availabilities based on event + if vals.get('event_id', False): + event = self.env['event.event'].browse(vals.get('event_id')) + vals['seats_availability'] = event.seats_availability + vals['seats_max'] = event.seats_max if not vals.get('event_mail_ids', False): vals.update({ 'event_mail_ids': @@ -117,6 +158,48 @@ def create(self, vals): }) return super(EventSession, self).create(vals) + @api.multi + def unlink(self): + for this in self: + if this.registration_ids: + raise ValidationError(_("You are trying to delete one or more \ + sessions with active registrations")) + return super(EventSession, self).unlink() + + @api.multi + @api.depends('seats_max', 'registration_ids.state') + def _compute_seats(self): + """Determine reserved, available, reserved but unconfirmed and used + seats by session. + """ + # aggregate registrations by event session and by state + if len(self.ids) > 0: + state_field = { + 'draft': 'seats_unconfirmed', + 'open': 'seats_reserved', + 'done': 'seats_used', + } + result = self.env['event.registration'].read_group([ + ('session_id', 'in', self.ids), + ('state', 'in', ['draft', 'open', 'done']) + ], ['state', 'session_id'], ['session_id', 'state'], lazy=False) + for res in result: + session = self.browse(res['session_id'][0]) + session[state_field[res['state']]] += res['__count'] + # compute seats_available + for session in self: + if session.seats_max > 0: + session.seats_available = session.seats_max - ( + session.seats_reserved + session.seats_used) + session.seats_expected = ( + session.seats_unconfirmed + session.seats_reserved + + session.seats_used) + session.seats_available_expected = ( + session.seats_max - session.seats_expected) + if session.seats_max > 0: + session.seats_available_pc = ( + session.seats_expected * 100 / float(session.seats_max)) + @api.multi @api.depends('date_tz', 'date_begin') def _compute_date_begin_located(self): @@ -144,10 +227,22 @@ def _compute_date_end_located(self): @api.onchange('event_id') def onchange_event_id(self): self.update({ + 'seats_min': self.event_id.seats_min, + 'seats_max': self.event_id.seats_max, + 'seats_availability': self.event_id.seats_availability, 'date_begin': self.event_id.date_begin, 'date_end': self.event_id.date_end, }) + @api.multi + @api.constrains('seats_max', 'seats_available') + def _check_seats_limit(self): + for session in self: + if (session.seats_availability == 'limited' and + session.seats_max and session.seats_available < 0): + raise ValidationError( + _('No more available seats for this session.')) + @api.multi @api.constrains('date_begin', 'date_end') def _check_dates(self): diff --git a/event_session/models/res_config_settings.py b/event_session/models/res_config_settings.py index 8f73c6785..65fd81567 100644 --- a/event_session/models/res_config_settings.py +++ b/event_session/models/res_config_settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # © 2016 Sergio Teruel # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). diff --git a/event_session/security/ir.model.access.csv b/event_session/security/ir.model.access.csv index 26b364c5d..9ec0f59eb 100644 --- a/event_session/security/ir.model.access.csv +++ b/event_session/security/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_event_session_user,event.session.user,event_session.model_event_session,event.group_event_user,1,0,0,0 access_event_session_admin,event.session.admin,event_session.model_event_session,event.group_event_manager,1,1,1,1 +access_event_mail_template,access_event_mail_template,model_event_mail_template,base.group_user,1,0,0,0 diff --git a/event_session/tests/test_session.py b/event_session/tests/test_session.py index 960154483..9dd00cc1c 100644 --- a/event_session/tests/test_session.py +++ b/event_session/tests/test_session.py @@ -1,6 +1,6 @@ # Copyright 2017 Tecnativa - David Vidal # Copyright 2017 Tecnativa - Pedro M. Baeza -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl-3.0). from odoo.tests import common from odoo.exceptions import ValidationError @@ -15,11 +15,22 @@ def setUpClass(cls): 'name': 'Test event', 'date_begin': '2017-05-26 20:00:00', 'date_end': '2017-05-30 22:00:00', + 'seats_availability': 'limited', + 'seats_max': '5', + 'seats_min': '1', }) cls.session = cls.env['event.session'].create({ 'date_begin': '2017-05-26 20:00:00', 'date_end': '2017-05-26 22:00:00', 'event_id': cls.event.id, + 'seats_availability': cls.event.seats_availability, + 'seats_max': cls.event.seats_max, + 'seats_min': cls.event.seats_min, + }) + cls.attendee = cls.env['event.registration'].create({ + 'name': 'Test attendee', + 'event_id': cls.event.id, + 'session_id': cls.session.id, }) cls.wizard = cls.env['wizard.event.session'].create({ 'event_id': cls.event.id, @@ -29,12 +40,20 @@ def setUpClass(cls): 'thursdays': True, 'fridays': True, 'sundays': True, - 'saturdays': False, + 'saturdays': True, 'delete_existing_sessions': False, 'session_hour_ids': [ (0, 0, {'start_time': 20.0, 'end_time': 21.0}), ], }) + cls.template = cls.env['event.mail.template'].create({ + 'name': 'Template test 01', + 'scheduler_template_ids': [(0, 0, { + 'interval_nbr': 15, + 'interval_unit': 'days', + 'interval_type': 'before_event', + 'template_id': cls.env.ref('event.event_reminder').id})], + }) def test_session_name_get(self): self.assertEqual( @@ -62,6 +81,52 @@ def test_check_zero_duration(self): 'date_end': '2017-05-28 22:00:00', }) + def test_open_registrations(self): + # registrations button + res = self.session.button_open_registration() + attendees = self.env['event.registration'].search([ + ['session_id', '=', self.session.id] + ]) + self.assertEqual(res['domain'], [('id', 'in', attendees.ids)]) + + def test_assign_mail_template(self): + vals = ({ + 'event_mail_ids': + self.session._session_mails_from_template(self.event.id) + }) + self.session.write(vals) + self.assertEqual(len(self.session.event_mail_ids), 0) + vals = ({ + 'event_mail_ids': + self.session._session_mails_from_template(self.event.id, + self.template) + }) + self.session.write(vals) + self.assertEqual(len(self.session.event_mail_ids), 1) + + def test_session_seats(self): + """ Session seat """ + self.assertEqual( + self.event.seats_available, + self.session.seats_available) + self.assertEqual( + self.event.seats_unconfirmed, + self.session.seats_unconfirmed + ) + self.assertEqual( + self.event.seats_used, + self.session.seats_used + ) + with self.assertRaises(ValidationError), self.cr.savepoint(): + # check limit regs + for i in range(int(self.session.seats_available)+1): + self.env['event.registration'].create({ + 'name': 'Test Attendee', + 'event_id': self.event.id, + 'session_id': self.session.id, + 'state': 'open', + }) + def test_compute_name(self): vals = { 'date_begin': '2017-05-28 22:00:00', @@ -80,11 +145,22 @@ def test_wizard(self): self.wizard.action_generate_sessions() # delete previous sessions self.wizard.update({'delete_existing_sessions': True}) + self.wizard.update({'event_mail_template_id': self.template}) + with self.assertRaises(ValidationError) as error, self.cr.savepoint(): + self.wizard.action_generate_sessions() + self.assertEqual(error, "You are trying to delete one or more \ + sessions with active registrations") + self.attendee.session_id = False self.wizard.action_generate_sessions() sessions = self.env['event.session'].search([ ['event_id', '=', self.event.id] ]) - self.assertEqual(len(sessions), 6) + self.assertEqual(len(sessions), 7) + for session in sessions: + self.assertTrue(session.event_mail_ids) + self.assertEqual(session.seats_max, self.event.seats_max) + self.assertEqual(session.seats_availability, + self.event.seats_availability) with self.assertRaises(ValidationError), self.cr.savepoint(): # session duration = 0 self.wizard.update({'session_hour_ids': [ @@ -105,7 +181,6 @@ def test_wizard(self): ], }) with self.assertRaises(ValidationError), self.cr.savepoint(): - # schedules overlap self.wizard.update({'session_hour_ids': [ (0, 0, {'start_time': 20.0, 'end_time': 21.0}), (0, 0, {'start_time': 19.5, 'end_time': 21.5}), diff --git a/event_session/views/event_session_view.xml b/event_session/views/event_session_view.xml index 59b1d7bd1..f6693f356 100644 --- a/event_session/views/event_session_view.xml +++ b/event_session/views/event_session_view.xml @@ -23,11 +23,41 @@
+
+ +
- - - - + + + + + + + + + + + + + + + + + + @@ -70,6 +100,22 @@ + + + + + + + + diff --git a/event_session/views/event_view.xml b/event_session/views/event_view.xml index afe9af112..95a7a31e8 100644 --- a/event_session/views/event_view.xml +++ b/event_session/views/event_view.xml @@ -48,7 +48,20 @@ + + + + + event.event + + + + + + +