[IMP] event: Improve logic and appearence of archived registrations

When an attendee's registration was archived, their seat was still
considered taken, which could be problematic such as in cases of
limited seat availability.

Only non-archived registrations are now counted as seats. The same
error as with regular registrations will be raised if there are not
enough seats available to un-archive a registration. These ValidationError
messages now show the name of the fully booked event.

A few python tests are included to verify the impact of (un)archiving on
seats availability for events and for event tickets.

The appearence of archived registrations was also not different in form
and kanban views, which is somewhat confusing and inconsistent with the
aspect of archived records in Odoo. Actions buttons are not available on
archived records.

Filtering in the archived records needed to be simplified from a "Custom
Filter" to a one-click feature, already available for many models.

Task-2646298

closes odoo/odoo#77715

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Florian Charlier
2021-12-17 11:04:53 +00:00
parent 846367530a
commit 9540dc7772
6 changed files with 204 additions and 53 deletions
+8 -5
View File
@@ -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):
+3 -2
View File
@@ -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):
+9 -5
View File
@@ -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
+26
View File
@@ -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 {
+103 -7
View File
@@ -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):
+55 -34
View File
@@ -30,7 +30,7 @@
<field name="has_seats_limitation"/>
<span attrs="{'invisible': [('has_seats_limitation', '=', False)], 'required': [('has_seats_limitation', '=', False)]}">
to <field name="seats_max" class="oe_inline"/>
Attendees
Confirmed Attendees
</span>
</div>
<div colspan="2" class="o_checkbox_optional_field">
@@ -206,6 +206,7 @@
<field name="seats_expected" widget="statinfo" string="Attendees"/>
</button>
</div>
<field name="active" invisible="1"/>
<field name="legend_blocked" invisible="1"/>
<field name="legend_normal" invisible="1"/>
<field name="legend_done" invisible="1"/>
@@ -238,7 +239,7 @@
<div colspan="2" class="o_checkbox_optional_field">
<label for="seats_limited" string="Limit Registrations"/>
<field name="seats_limited"/>
<span attrs="{'invisible': [('seats_limited', '=', False)], 'required': [('seats_limited', '=', False)]}">to <field name="seats_max" class="oe_inline"/> Attendees</span>
<span attrs="{'invisible': [('seats_limited', '=', False)], 'required': [('seats_limited', '=', False)]}">to <field name="seats_max" class="oe_inline"/> Confirmed Attendees</span>
</div>
<field name="auto_confirm"/>
</group>
@@ -445,7 +446,7 @@
<separator/>
<filter string="Start Date" name="start_date" date="date_begin"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<filter string="Archived" name="filter_inactive" domain="[('active', '=', False)]"/>
<separator/>
<filter invisible="1" string="Late Activities" name="activities_overdue"
domain="[('my_activity_date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"
@@ -519,6 +520,7 @@
<tree string="Registration" multi_edit="1" sample="1"
expand="1" default_order="create_date desc"
class="o_event_registration_view_tree">
<field name="active" invisible="1"/>
<field name="create_date" optional="show" string="Registration Date"/>
<field name="name"/>
<field name="partner_id" optional="hide"/>
@@ -533,11 +535,13 @@
decoration-muted="state == 'cancel'" widget="badge"/>
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
<field name="message_needaction" invisible="1"/>
<button name="action_confirm" string="Confirm" states="draft" type="object" icon="fa-check"/>
<button name="action_set_done" string="Mark as Attending" states="open" type="object" icon="fa-level-down"/>
<button name="action_cancel" string="Cancel"
states="draft,open" type="object"
class="o_btn_cancel_registration" icon="fa-times"/>
<button name="action_confirm" string="Confirm" type="object" icon="fa-check"
attrs="{'invisible': ['|', ('active', '=', False), ('state', '!=', 'draft')]}"/>
<button name="action_set_done" string="Mark as Attending" type="object" icon="fa-level-down"
attrs="{'invisible': ['|', ('active', '=', False), ('state', '!=', 'open')]}"/>
<button name="action_cancel" string="Cancel" type="object"
class="o_btn_cancel_registration" icon="fa-times"
attrs="{'invisible': ['|', ('active', '=', False), '&amp;', ('state', '!=', 'open'), ('state', '!=', 'draft')]}"/>
<field name="activity_exception_decoration" widget="activity_exception"/>
</tree>
</field>
@@ -548,16 +552,23 @@
<field name="model">event.registration</field>
<field name="arch" type="xml">
<form string="Event Registration">
<field name="active" invisible="1"/>
<header>
<button name="action_send_badge_email" string="Send by Email" type="object" states="open,done" class="oe_highlight"/>
<button name="action_confirm" string="Confirm" states="draft" type="object" class="oe_highlight"/>
<button name="action_set_done" string="Attended" states="open" type="object" class="oe_highlight"/>
<button name="action_set_draft" string="Set To Unconfirmed" states="cancel,done" type="object" />
<button name="action_cancel" string="Cancel Registration" states="draft,open" type="object"/>
<button name="action_send_badge_email" string="Send by Email" type="object" class="oe_highlight"
attrs="{'invisible': ['|', ('active', '=', False), '&amp;', ('state', '!=', 'open'), ('state', '!=', 'done')]}"/>
<button name="action_confirm" string="Confirm" type="object" class="oe_highlight"
attrs="{'invisible': ['|', ('active', '=', False), ('state', '!=', 'draft')]}"/>
<button name="action_set_done" string="Attended" type="object" class="oe_highlight"
attrs="{'invisible': ['|', ('active', '=', False), ('state', '!=', 'open')]}"/>
<button name="action_set_draft" string="Set To Unconfirmed" type="object"
attrs="{'invisible': ['|', ('active', '=', False), '&amp;', ('state', '!=', 'cancel'), ('state', '!=', 'done')]}"/>
<button name="action_cancel" string="Cancel Registration" type="object"
attrs="{'invisible': ['|', ('active', '=', False), '&amp;', ('state', '!=', 'open'), ('state', '!=', 'draft')]}"/>
<field name="state" nolabel="1" colspan="2" widget="statusbar" statusbar_visible="draft,open,done"/>
</header>
<sheet string="Registration">
<div class="oe_button_box" name="button_box"/>
<widget name="web_ribbon" text="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<group>
<group string="Attendee" name="attendee">
<field class="o_text_overflow" name="name"/>
@@ -605,39 +616,47 @@
<field name="state"/>
<field name="email"/>
<field name="event_ticket_id"/>
<field name="active" invisible="1"/>
<templates>
<t t-name="event_attendees_kanban_icons_desktop">
<div class="d-none d-md-block h-100">
<div id="event_attendees_kanban_icons_desktop" class="h-100 float-right p-2 d-flex align-items-end flex-column">
<a class="btn btn-md btn-primary" string="Confirm Registration" name="action_confirm" type="object" states="draft" role="button">
<i class="fa fa-check" role="img" aria-label="Confirm button" title="Confirm Registration"/>
</a>
<a class="btn btn-md btn-primary" string="Confirm Attendance" name="action_set_done" type="object" states="open" role="button">
<i class="fa fa-user-plus" role="img" aria-label="Attended button" title="Confirm Attendance"/>
</a>
<span class="text-muted" states="done">Attended</span>
<span class="text-muted" states="cancel">Canceled</span>
<t t-if="record.active.raw_value">
<a class="btn btn-md btn-primary" string="Confirm Registration" name="action_confirm" type="object" states="draft" role="button">
<i class="fa fa-check" role="img" aria-label="Confirm button" title="Confirm Registration"/>
</a>
<a class="btn btn-md btn-primary" string="Confirm Attendance" name="action_set_done" type="object" states="open" role="button">
<i class="fa fa-user-plus" role="img" aria-label="Attended button" title="Confirm Attendance"/>
</a>
<span class="text-muted" states="done">Attended</span>
<span class="text-muted" states="cancel">Canceled</span>
</t>
</div>
</div>
</t>
<t t-name="event_attendees_kanban_icons_mobile">
<div id="event_attendees_kanban_icons_mobile" class="d-md-none h-100 pl-4">
<a class="btn btn-primary d-flex justify-content-center align-items-center h-100 w-100"
string="Confirm Registration" name="action_confirm" type="object" states="draft" role="button">
<i class="fa fa-check fa-3x" role="img" aria-label="Confirm button" title="Confirm Registration"/>
</a>
<a class="btn btn-primary d-flex justify-content-center align-items-center h-100 w-100"
string="Confirm Attendance" name="action_set_done" type="object" states="open" role="button">
<i class="fa fa-user-plus fa-3x" role="img" aria-label="Attended button" title="Confirm Attendance"/>
</a>
<div class="d-flex justify-content-center align-items-center h-100 w-100">
<span class="text-muted" states="done" >Attended</span>
<span class="text-muted" states="cancel" >Canceled</span>
</div>
<t t-if="record.active.raw_value">
<a class="btn btn-primary d-flex justify-content-center align-items-center h-100 w-100"
string="Confirm Registration" name="action_confirm" type="object" states="draft" role="button">
<i class="fa fa-check fa-3x" role="img" aria-label="Confirm button" title="Confirm Registration"/>
</a>
<a class="btn btn-primary d-flex justify-content-center align-items-center h-100 w-100"
string="Confirm Attendance" name="action_set_done" type="object" states="open" role="button">
<i class="fa fa-user-plus fa-3x" role="img" aria-label="Attended button" title="Confirm Attendance"/>
</a>
<div class="d-flex justify-content-center align-items-center h-100 w-100">
<span class="text-muted" states="done" >Attended</span>
<span class="text-muted" states="cancel" >Canceled</span>
</div>
</t>
</div>
</t>
<t t-name="kanban-box">
<div class="oe_kanban_global_click o_event_registration_kanban container-fluid p-0">
<div t-attf-class="#{!record.active.raw_value ? 'oe_kanban_card_ribbon' : ''} oe_kanban_global_click o_event_registration_kanban container-fluid p-0">
<div t-if="!record.active.raw_value" class="ribbon ribbon-top-right">
<span class="bg-danger">Archived</span>
</div>
<div class="row h-100">
<div class="col-9 pr-0">
<div class="oe_kanban_content h-100">
@@ -719,6 +738,8 @@
<field name="event_id"/>
<field name="partner_id"/>
<field name="company_id"/>
<separator/>
<filter string="Archived" name="filter_inactive" domain="[('active', '=', False)]"/>
<filter invisible="1" string="Late Activities" name="activities_overdue"
domain="[('my_activity_date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"
help="Show all records which has next action date is before today"/>