diff --git a/addons/event/models/event_event.py b/addons/event/models/event_event.py index 8ba70e498f8..cd4b33c864e 100644 --- a/addons/event/models/event_event.py +++ b/addons/event/models/event_event.py @@ -208,7 +208,7 @@ class EventEvent(models.Model): else: event.kanban_state_label = event.stage_id.legend_done - @api.depends('seats_max', 'registration_ids.state') + @api.depends('seats_max', 'registration_ids.state', 'registration_ids.active') def _compute_seats(self): """ Determine reserved, available, reserved but unconfirmed and used seats. """ # initialize fields to 0 @@ -225,10 +225,10 @@ class EventEvent(models.Model): if self.ids: query = """ SELECT event_id, state, count(event_id) FROM event_registration - WHERE event_id IN %s AND state IN ('draft', 'open', 'done') + WHERE event_id IN %s AND state IN ('draft', 'open', 'done') AND active = true GROUP BY event_id, state """ - self.env['event.registration'].flush(['event_id', 'state']) + self.env['event.registration'].flush(['event_id', 'state', 'active']) self._cr.execute(query, (tuple(self.ids),)) res = self._cr.fetchall() for event_id, state, num in res: @@ -496,8 +496,11 @@ class EventEvent(models.Model): @api.constrains('seats_max', 'seats_available', 'seats_limited') def _check_seats_limit(self): - if any(event.seats_limited and event.seats_max and event.seats_available < 0 for event in self): - raise ValidationError(_('No more available seats.')) + for event in self: + if event.seats_limited and event.seats_max and event.seats_available < 0: + raise ValidationError(_('No more available seats for %s. ' + 'Raise the limit or remove some other confirmed registrations first.', + event.name)) @api.constrains('date_begin', 'date_end') def _check_closing_date(self): diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index e104434459f..18fea786c42 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -96,8 +96,9 @@ class EventRegistration(models.Model): @api.constrains('event_id', 'state') def _check_seats_limit(self): for registration in self: - if registration.event_id.seats_limited and registration.event_id.seats_max and registration.event_id.seats_available < (1 if registration.state == 'draft' else 0): - raise ValidationError(_('No more seats available for this event.')) + event_id = registration.event_id + if event_id.seats_limited and event_id.seats_max and event_id.seats_available < (1 if registration.state == 'draft' and event_id.auto_confirm else 0): + raise ValidationError(_('There are no more available seats for %s. Raise the limit or remove some other confirmed registrations first.', event_id.name)) @api.constrains('event_ticket_id', 'state') def _check_ticket_seats_limit(self): diff --git a/addons/event/models/event_ticket.py b/addons/event/models/event_ticket.py index 61ef734e96c..d5207f61f08 100644 --- a/addons/event/models/event_ticket.py +++ b/addons/event/models/event_ticket.py @@ -91,7 +91,7 @@ class EventTicket(models.Model): else: ticket.sale_available = True - @api.depends('seats_max', 'registration_ids.state') + @api.depends('seats_max', 'registration_ids.state', 'registration_ids.active') def _compute_seats(self): """ Determine reserved, available, reserved but unconfirmed and used seats. """ # initialize fields to 0 + compute seats availability @@ -107,10 +107,10 @@ class EventTicket(models.Model): } query = """ SELECT event_ticket_id, state, count(event_id) FROM event_registration - WHERE event_ticket_id IN %s AND state IN ('draft', 'open', 'done') + WHERE event_ticket_id IN %s AND state IN ('draft', 'open', 'done') AND active = true GROUP BY event_ticket_id, state """ - self.env['event.registration'].flush(['event_id', 'event_ticket_id', 'state']) + self.env['event.registration'].flush(['event_id', 'event_ticket_id', 'state', 'active']) self.env.cr.execute(query, (tuple(self.ids),)) for event_ticket_id, state, num in self.env.cr.fetchall(): results.setdefault(event_ticket_id, {})[state_field[state]] = num @@ -129,8 +129,12 @@ class EventTicket(models.Model): @api.constrains('seats_available', 'seats_max') def _constrains_seats_available(self): - if any(record.seats_max and record.seats_available < 0 for record in self): - raise ValidationError(_('No more available seats for this ticket.')) + for record in self: + if record.seats_max and record.seats_available < 0: + raise ValidationError( + _('No more available seats for the ticket %s (%s). ' + 'Raise the limit or remove some other confirmed registrations first.', + record.name, record.event_id.name)) def _get_ticket_multiline_description(self): """ Compute a multiline description of this ticket. It is used when ticket diff --git a/addons/event/static/src/scss/event.scss b/addons/event/static/src/scss/event.scss index 703f90377e8..439b7a125da 100644 --- a/addons/event/static/src/scss/event.scss +++ b/addons/event/static/src/scss/event.scss @@ -23,6 +23,32 @@ min-height: 80px; } } + .oe_kanban_card_ribbon { + min-height: 95px; + .ribbon { + &::before, &::after { + display: none; + } + span { + padding: 5px; + font-size: small; + z-index: 0; + } + } + .ribbon-top-right { + margin-top: -1px; + span { + left: 7px; + right: 30px; + height: 25px; + top: 18px; + } + } + // Used for "Group By" + div.row { + min-height: 95px; + } + } } .o_event_registration_view_tree { diff --git a/addons/event/tests/test_event_internals.py b/addons/event/tests/test_event_internals.py index e02a42aedf5..2b1b3b9b546 100644 --- a/addons/event/tests/test_event_internals.py +++ b/addons/event/tests/test_event_internals.py @@ -486,12 +486,13 @@ class TestEventData(TestEventInternalsCommon): # create registration in order to check the seats computation self.assertTrue(event.auto_confirm) - for x in range(5): - reg = self.env['event.registration'].create({ - 'event_id': event.id, - 'name': 'reg_open', - }) - self.assertEqual(reg.state, 'open') + reg_open_multiple = self.env['event.registration'].create([{ + 'event_id': event.id, + 'name': 'reg_open', + } for _ in range(5)]) + self.assertEqual(set(reg_open_multiple.mapped('state')), {'open'}) + reg_open = reg_open_multiple[0] + reg_draft = self.env['event.registration'].create({ 'event_id': event.id, 'name': 'reg_draft', @@ -508,6 +509,38 @@ class TestEventData(TestEventInternalsCommon): self.assertEqual(event.seats_used, 1) self.assertEqual(event.seats_expected, 7) + # ------------------------------------------------------------ + # (UN-)ARCHIVING REGISTRATIONS + # ------------------------------------------------------------ + + # Archiving and seats availability + reg_open.action_archive() + self.assertEqual(event.seats_reserved, 4) + self.assertEqual(event.seats_available, event.event_type_id.seats_max - 5) + self.assertEqual(event.seats_expected, 6) + + reg_draft.action_archive() + self.assertEqual(event.seats_unconfirmed, 0) + self.assertEqual(event.seats_available, event.event_type_id.seats_max - 5) + self.assertEqual(event.seats_expected, 5) + + # Un-archiving confirmed seats requires available seat(s) + reg_open.action_unarchive() + self.assertEqual(event.seats_reserved, 5) + self.assertEqual(event.seats_available, event.event_type_id.seats_max - 6) + self.assertEqual(event.seats_expected, 6) + + reg_draft.action_unarchive() + self.assertEqual(event.seats_unconfirmed, 1) + self.assertEqual(event.seats_available, event.event_type_id.seats_max - 6) + self.assertEqual(event.seats_expected, 7) + + reg_open.action_archive() + event.write({'seats_max': 5}) + # It is not possible to unarchive confirmed seat if event is full + with self.assertRaises(exceptions.ValidationError): + reg_open.action_unarchive() + class TestEventRegistrationData(TestEventInternalsCommon): @@ -609,19 +642,21 @@ class TestEventTicketData(TestEventInternalsCommon): @users('user_eventmanager') def test_event_ticket_fields(self): """ Test event ticket fields synchronization """ + INITIAL_TICKET_SEATS_MAX = 30 event = self.event_0.with_user(self.env.user) event.write({ 'event_ticket_ids': [ (5, 0), (0, 0, { 'name': 'First Ticket', - 'seats_max': 30, + 'seats_max': INITIAL_TICKET_SEATS_MAX, }), (0, 0, { # limited in time, available (01/10 (start) < 01/31 (today) < 02/10 (end)) 'name': 'Second Ticket', 'start_sale_datetime': datetime(2020, 1, 10, 0, 0, 0), 'end_sale_datetime': datetime(2020, 2, 10, 23, 59, 59), }) ], + 'auto_confirm': False # to interact with registrations states }) first_ticket = event.event_ticket_ids.filtered(lambda t: t.name == 'First Ticket') second_ticket = event.event_ticket_ids.filtered(lambda t: t.name == 'Second Ticket') @@ -677,6 +712,67 @@ class TestEventTicketData(TestEventInternalsCommon): self.assertFalse(second_ticket.sale_available) self.assertTrue(second_ticket.is_expired) + # ------------------------------------------------------------ + # (UN-)ARCHIVING TICKET REGISTRATIONS + # ------------------------------------------------------------ + + # Archiving and seats availability + reg_draft_multiple = self.env['event.registration'].create([{ + 'event_id': event.id, + 'name': f'reg_draft #{idx}', + 'event_ticket_id': first_ticket.id, + } for idx in range(3)]) + reg_draft = reg_draft_multiple[0] + + reg_open = self.env['event.registration'].create({ + 'event_id': event.id, + 'name': 'reg_open', + 'event_ticket_id': first_ticket.id, + 'state': 'open' + }) + + reg_done = self.env['event.registration'].create({ + 'event_id': event.id, + 'name': 'reg_done', + 'event_ticket_id': first_ticket.id, + 'state': 'done' + }) + + self.assertEqual(first_ticket.seats_unconfirmed, 3) + self.assertEqual(first_ticket.seats_reserved, 1) + self.assertEqual(first_ticket.seats_used, 1) + self.assertEqual(first_ticket.seats_available, INITIAL_TICKET_SEATS_MAX - 2) + + reg_done.action_archive() + self.assertEqual(first_ticket.seats_used, 0) + self.assertEqual(first_ticket.seats_available, INITIAL_TICKET_SEATS_MAX - 1) + + reg_open.action_archive() + self.assertEqual(first_ticket.seats_reserved, 0) + self.assertEqual(first_ticket.seats_available, INITIAL_TICKET_SEATS_MAX) + + reg_draft.action_archive() + self.assertEqual(first_ticket.seats_unconfirmed, 2) + + # Un-archiving confirmed/done seats requires available seat(s) + reg_open.action_unarchive() + self.assertEqual(first_ticket.seats_reserved, 1) + self.assertEqual(first_ticket.seats_available, INITIAL_TICKET_SEATS_MAX - 1) + + reg_done.action_unarchive() + self.assertEqual(first_ticket.seats_used, 1) + self.assertEqual(first_ticket.seats_available, INITIAL_TICKET_SEATS_MAX - 2) + + reg_draft.action_unarchive() + self.assertEqual(first_ticket.seats_unconfirmed, 3) + self.assertEqual(first_ticket.seats_available, INITIAL_TICKET_SEATS_MAX - 2) + + reg_open.action_archive() + first_ticket.write({'seats_max': 1}) + # It is not possible to unarchive confirmed seat if ticket is fully booked + with self.assertRaises(exceptions.ValidationError): + reg_open.action_unarchive() + class TestEventTypeData(TestEventInternalsCommon): diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index 2dfb2c4c691..83fe16fc936 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -30,7 +30,7 @@ to - Attendees + Confirmed Attendees
@@ -206,6 +206,7 @@
+ @@ -238,7 +239,7 @@
@@ -445,7 +446,7 @@ - + + @@ -533,11 +535,13 @@ decoration-muted="state == 'cancel'" widget="badge"/> -