[MERGE] event: remove autoconfirm, rework pay status

We remove the is_paid field and based the logic on the payment_status
as these two fields are a bit redundant. It is also in preparation
to the removal of the field auto_confirm on the event.event model.
The goal is to ease and simplify the management of registrations.

Remove autoconfirm logic and simplify the flow of event registrations. Now a
registration is by default considered as confirmed and the draft state is
not used anymore by default. It is used in combination with sales before
a registration is considered as paid. It allows to avoid blocking seats
in an event while registration fees are not paid.

task-3061849

closes odoo/odoo#126431

Related: odoo/upgrade#4836
Related: odoo/enterprise#47982
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2023-10-13 07:33:49 +00:00
committed by Odoo Online
43 changed files with 247 additions and 433 deletions
-2
View File
@@ -109,7 +109,6 @@
<field name="user_id" ref="base.user_admin"/>
<field eval="(DateTime.today()+ timedelta(days=5)).strftime('%Y-%m-%d 07:00:00')" name="date_begin"/>
<field eval="(DateTime.today()+ timedelta(days=5)).strftime('%Y-%m-%d 16:30:00')" name="date_end"/>
<field name="auto_confirm" eval="True"/>
<field name="address_id" ref="event.res_partner_location_2"/>
<field name="seats_limited">True</field>
<field name="seats_max">200</field>
@@ -240,7 +239,6 @@
<field name="event_type_id" ref="event_type_0"/>
<field name="stage_id" ref="event.event_stage_booked"/>
<field name="user_id" ref="base.user_demo"/>
<field name="auto_confirm" eval="True"/>
<field name="date_begin" eval="(DateTime.now() - timedelta(days=1)).strftime('%Y-%m-%d 05:00:00')"/>
<field name="date_end" eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 15:00:00')"/>
<field name="address_id" eval="False"/>
-3
View File
@@ -4,17 +4,14 @@
<!-- Event Type -->
<record id="event_type_0" model="event.type">
<field name="name">Exhibition</field>
<field name="auto_confirm" eval="False"/>
<field name="sequence">3</field>
</record>
<record id="event_type_1" model="event.type">
<field name="name">Training</field>
<field name="auto_confirm" eval="False"/>
<field name="sequence">4</field>
</record>
<record id="event_type_2" model="event.type">
<field name="name">Sport</field>
<field name="auto_confirm" eval="False"/>
<field name="default_timezone">US/Pacific</field>
<field name="sequence">5</field>
</record>
@@ -158,15 +158,6 @@
<field name="partner_id" ref="base.res_partner_address_25"/>
</record>
<function model="event.registration"
name="action_confirm"
context="{'install_mode' : True}"
eval="[[ref('event_registration_0_0'), ref('event_registration_0_1'),
ref('event_registration_1_0'), ref('event_registration_1_1'), ref('event_registration_1_2'),
ref('event_registration_2_0'), ref('event_registration_2_1'), ref('event_registration_2_2'), ref('event_registration_2_3'),
ref('event_registration_4_2')]]"
/>
<function model="event.registration"
name="action_set_done"
eval="[[ref('event_registration_4_0'), ref('event_registration_4_1')]]"
+6 -26
View File
@@ -63,10 +63,6 @@ class EventType(models.Model):
'Maximum Registrations', compute='_compute_seats_max',
readonly=False, store=True,
help="It will select this default maximum value when you choose this event")
auto_confirm = fields.Boolean(
'Automatically Confirm Registrations', default=False,
help="Events and registrations will automatically be confirmed "
"upon creation, easing the flow for simple events.")
default_timezone = fields.Selection(
_tz_get, string='Timezone', default=lambda self: self.env.user.tz or 'UTC')
# communication
@@ -166,19 +162,13 @@ class EventEvent(models.Model):
seats_available = fields.Integer(
string='Available Seats',
store=False, readonly=True, compute='_compute_seats')
seats_unconfirmed = fields.Integer(
string='Unconfirmed Registrations',
store=False, readonly=True, compute='_compute_seats')
seats_used = fields.Integer(
string='Number of Attendees',
store=False, readonly=True, compute='_compute_seats')
seats_expected = fields.Integer(
string='Number of Expected Attendees',
seats_taken = fields.Integer(
string='Number of Taken Seats',
store=False, readonly=True, compute='_compute_seats')
# Registration fields
auto_confirm = fields.Boolean(
string='Autoconfirmation', compute='_compute_auto_confirm', readonly=False, store=True,
help='Autoconfirm Registrations. Registrations will automatically be confirmed upon creation.')
registration_ids = fields.One2many('event.registration', 'event_id', string='Attendees')
event_ticket_ids = fields.One2many(
'event.event.ticket', 'event_id', string='Event Ticket', copy=True,
@@ -255,13 +245,12 @@ class EventEvent(models.Model):
@api.depends('seats_max', 'registration_ids.state', 'registration_ids.active')
def _compute_seats(self):
""" Determine reserved, available, reserved but unconfirmed and used seats. """
""" Determine available, reserved, used and taken seats. """
# initialize fields to 0
for event in self:
event.seats_unconfirmed = event.seats_reserved = event.seats_used = event.seats_available = 0
event.seats_reserved = event.seats_used = event.seats_available = 0
# aggregate registrations by event and by state
state_field = {
'draft': 'seats_unconfirmed',
'open': 'seats_reserved',
'done': 'seats_used',
}
@@ -270,7 +259,7 @@ 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') AND active = true
WHERE event_id IN %s AND state IN ('open', 'done') AND active = true
GROUP BY event_id, state
"""
self.env['event.registration'].flush_model(['event_id', 'state', 'active'])
@@ -285,7 +274,7 @@ class EventEvent(models.Model):
if event.seats_max > 0:
event.seats_available = event.seats_max - (event.seats_reserved + event.seats_used)
event.seats_expected = event.seats_unconfirmed + event.seats_reserved + event.seats_used
event.seats_taken = event.seats_reserved + event.seats_used
@api.depends('date_tz', 'start_sale_datetime')
def _compute_event_registrations_started(self):
@@ -463,15 +452,6 @@ class EventEvent(models.Model):
if not event.seats_limited:
event.seats_limited = False
@api.depends('event_type_id')
def _compute_auto_confirm(self):
""" Update event configuration from its event type. Depends are set only
on event_type_id itself, not its sub fields. Purpose is to emulate an
onchange: if event type is changed, update event configuration. Changing
event type content itself should not trigger this method. """
for event in self:
event.auto_confirm = event.event_type_id.auto_confirm
@api.depends('event_type_id')
def _compute_event_mail_ids(self):
""" Update event configuration from its event type. Depends are set only
+24 -26
View File
@@ -38,7 +38,7 @@ class EventRegistration(models.Model):
active = fields.Boolean(default=True)
barcode = fields.Char(string='Barcode', default=lambda self: self._get_random_barcode(), readonly=True, copy=False)
# utm informations
utm_campaign_id = fields.Many2one('utm.campaign', 'Campaign', index=True, ondelete='set null')
utm_campaign_id = fields.Many2one('utm.campaign', 'Campaign', index=True, ondelete='set null')
utm_source_id = fields.Many2one('utm.source', 'Source', index=True, ondelete='set null')
utm_medium_id = fields.Many2one('utm.medium', 'Medium', index=True, ondelete='set null')
# attendee
@@ -62,9 +62,16 @@ class EventRegistration(models.Model):
'res.company', string='Company', related='event_id.company_id',
store=True, readonly=False)
state = fields.Selection([
('draft', 'Unconfirmed'), ('cancel', 'Cancelled'),
('open', 'Confirmed'), ('done', 'Attended')],
string='Status', default='draft', readonly=True, copy=False, tracking=6)
('draft', 'Unconfirmed'),
('open', 'Registered'),
('done', 'Attended'),
('cancel', 'Cancelled')],
string='Status', default='open',
readonly=True, copy=False, tracking=6,
help='Unconfirmed: registrations in a pending state waiting for an action (specific case, notably with sale status)\n'
'Registered: registrations considered taken by a client\n'
'Attended: registrations for which the attendee attended the event\n'
'Cancelled: registrations cancelled manually')
# properties
registration_properties = fields.Properties(
'Properties', definition='event_id.registration_properties_definition', copy=True)
@@ -73,6 +80,12 @@ class EventRegistration(models.Model):
('barcode_event_uniq', 'unique(barcode)', "Barcode should be unique")
]
@api.constrains('state', 'event_id', 'event_ticket_id')
def _check_seats_availability(self):
registrations_confirmed = self.filtered(lambda registration: registration.state in ('open', 'done'))
registrations_confirmed.event_id._check_seats_availability()
registrations_confirmed.event_ticket_id._check_seats_availability()
@api.depends('partner_id')
def _compute_name(self):
for registration in self:
@@ -185,11 +198,7 @@ class EventRegistration(models.Model):
registrations = super(EventRegistration, self).create(vals_list)
# auto_confirm if possible; if not automatically confirmed, call mail schedulers in case
# some were created already open
if registrations._check_auto_confirmation():
registrations.sudo().action_confirm()
elif not self.env.context.get('install_mode', False):
if not self.env.context.get('install_mode', False):
# running the scheduler for demo data can cause an issue where wkhtmltopdf runs during
# server start and hangs indefinitely, leading to serious crashes
# we currently avoid this by not running the scheduler, would be best to find the actual
@@ -202,19 +211,12 @@ class EventRegistration(models.Model):
to_confirm = (self.filtered(lambda registration: registration.state in {'draft', 'cancel'})
if confirming else None)
ret = super(EventRegistration, self).write(vals)
# As these Event(Ticket) methods are model constraints, it is not necessary to call them
# explicitly when creating new registrations. However, it is necessary to trigger them here
# as changes in registration states cannot be used as constraints triggers.
if confirming:
to_confirm.event_id._check_seats_availability()
to_confirm.event_ticket_id._check_seats_availability()
if not self.env.context.get('install_mode', False):
# running the scheduler for demo data can cause an issue where wkhtmltopdf runs
# during server start and hangs indefinitely, leading to serious crashes we
# currently avoid this by not running the scheduler, would be best to find the
# actual reason for this issue and fix it so we can remove this check
to_confirm._update_mail_schedulers()
if confirming and not self.env.context.get('install_mode', False):
# running the scheduler for demo data can cause an issue where wkhtmltopdf runs
# during server start and hangs indefinitely, leading to serious crashes we
# currently avoid this by not running the scheduler, would be best to find the
# actual reason for this issue and fix it so we can remove this check
to_confirm._update_mail_schedulers()
return ret
@@ -233,10 +235,6 @@ class EventRegistration(models.Model):
pre_inactive.event_id._check_seats_availability()
pre_inactive.event_ticket_id._check_seats_availability()
def _check_auto_confirmation(self):
""" Checks that all registrations are for `auto-confirm` events. """
return all(event.auto_confirm for event in self.event_id)
# ------------------------------------------------------------
# ACTIONS / BUSINESS
# ------------------------------------------------------------
+6 -6
View File
@@ -42,7 +42,7 @@ class EventTemplateTicket(models.Model):
class EventTicket(models.Model):
""" Ticket model allowing to have differnt kind of registrations for a given
""" Ticket model allowing to have different kind of registrations for a given
event. Ticket are based on ticket type as they share some common fields
and behavior. Those models come from <= v13 Odoo event.event.ticket that
modeled both concept: tickets for event templates, and tickets for events. """
@@ -76,8 +76,8 @@ class EventTicket(models.Model):
# seats
seats_reserved = fields.Integer(string='Reserved Seats', compute='_compute_seats', store=False)
seats_available = fields.Integer(string='Available Seats', compute='_compute_seats', store=False)
seats_unconfirmed = fields.Integer(string='Unconfirmed Seats', compute='_compute_seats', store=False)
seats_used = fields.Integer(string='Used Seats', compute='_compute_seats', store=False)
seats_taken = fields.Integer(string="Taken Seats", compute="_compute_seats", store=False)
is_sold_out = fields.Boolean(
'Sold Out', compute='_compute_is_sold_out', help='Whether seats are not available for this ticket.')
# reports
@@ -113,21 +113,20 @@ class EventTicket(models.Model):
@api.depends('seats_max', 'registration_ids.state', 'registration_ids.active')
def _compute_seats(self):
""" Determine reserved, available, reserved but unconfirmed and used seats. """
""" Determine available, reserved, used and taken seats. """
# initialize fields to 0 + compute seats availability
for ticket in self:
ticket.seats_unconfirmed = ticket.seats_reserved = ticket.seats_used = ticket.seats_available = 0
ticket.seats_reserved = ticket.seats_used = ticket.seats_available = 0
# aggregate registrations by ticket and by state
results = {}
if self.ids:
state_field = {
'draft': 'seats_unconfirmed',
'open': 'seats_reserved',
'done': 'seats_used',
}
query = """ SELECT event_ticket_id, state, count(event_id)
FROM event_registration
WHERE event_ticket_id IN %s AND state IN ('draft', 'open', 'done') AND active = true
WHERE event_ticket_id IN %s AND state IN ('open', 'done') AND active = true
GROUP BY event_ticket_id, state
"""
self.env['event.registration'].flush_model(['event_id', 'event_ticket_id', 'state', 'active'])
@@ -140,6 +139,7 @@ class EventTicket(models.Model):
ticket.update(results.get(ticket._origin.id or ticket.id, {}))
if ticket.seats_max > 0:
ticket.seats_available = ticket.seats_max - (ticket.seats_reserved + ticket.seats_used)
ticket.seats_taken = ticket.seats_reserved + ticket.seats_used
@api.depends('seats_limited', 'seats_available')
def _compute_is_sold_out(self):
@@ -39,7 +39,7 @@
<tr t-if="!registration.event_id and registration.company_name"><td>Company</td><td><t t-out="registration.company_name"/></td></tr>
<tr><td>Name</td><td><t t-out="registration.name"/></td></tr>
<tr t-if="registration.ticket_name"><td>Ticket</td><td><t t-out="registration.ticket_name"/></td></tr>
<tr t-if="registration.payment_status_value"><td>Payment</td><td><t t-out="registration.payment_status_value"/></td></tr>
<tr t-if="registration.sale_status_value"><td>Payment</td><td><t t-out="registration.sale_status_value"/></td></tr>
<tr t-if="registration.registration_answers">
<td>Answers</td>
<td>
+6 -81
View File
@@ -7,62 +7,12 @@ from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo.addons.event.tests.common import EventCase
from odoo.exceptions import ValidationError
from odoo.tests.common import users
from odoo.tools import mute_logger
class TestEventFlow(EventCase):
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
def test_event_auto_confirm(self):
""" Basic event management with auto confirmation """
# EventUser creates a new event: ok
test_event = self.env['event.event'].with_user(self.user_eventmanager).create({
'name': 'TestEvent',
'auto_confirm': True,
'date_begin': datetime.datetime.now() + relativedelta(days=-1),
'date_end': datetime.datetime.now() + relativedelta(days=1),
'seats_max': 2,
'seats_limited': True,
})
self.assertTrue(test_event.auto_confirm)
# EventUser create registrations for this event
test_reg1 = self.env['event.registration'].with_user(self.user_eventuser).create({
'name': 'TestReg1',
'event_id': test_event.id,
})
scheduler = self.env['event.mail'].sudo().search([
('event_id', '=', test_event.id),
('interval_type', '=', 'after_sub')
])
self.assertTrue(scheduler.mail_registration_ids.mail_sent)
self.assertEqual(test_reg1.state, 'open', 'Event: auto_confirmation of registration failed')
self.assertEqual(test_event.seats_reserved, 1, 'Event: wrong number of reserved seats after confirmed registration')
test_reg2 = self.env['event.registration'].with_user(self.user_eventuser).create({
'name': 'TestReg2',
'event_id': test_event.id,
})
self.assertEqual(test_reg2.state, 'open', 'Event: auto_confirmation of registration failed')
self.assertEqual(test_event.seats_reserved, 2, 'Event: wrong number of reserved seats after confirmed registration')
# EventUser create registrations for this event: too much registrations
with self.assertRaises(ValidationError):
self.env['event.registration'].with_user(self.user_eventuser).create({
'name': 'TestReg3',
'event_id': test_event.id,
})
# EventUser validates registrations
test_reg1.action_set_done()
self.assertEqual(test_reg1.state, 'done', 'Event: wrong state of attended registration')
self.assertEqual(test_event.seats_used, 1, 'Event: incorrect number of attendees after closing registration')
test_reg2.action_set_done()
self.assertEqual(test_reg1.state, 'done', 'Event: wrong state of attended registration')
self.assertEqual(test_event.seats_used, 2, 'Event: incorrect number of attendees after closing registration')
@users('user_eventmanager')
def test_event_default_datetime(self):
""" Check that the default date_begin and date_end are correctly set """
@@ -89,29 +39,6 @@ class TestEventFlow(EventCase):
self.assertEqual(event.date_begin, specific_datetimes['date_begin'])
self.assertEqual(event.date_end, specific_datetimes['date_end'])
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
def test_event_flow(self):
""" Advanced event flow: no auto confirmation, manage minimum / maximum
seats, ... """
# EventUser creates a new event: ok
test_event = self.env['event.event'].with_user(self.user_eventmanager).create({
'name': 'TestEvent',
'date_begin': datetime.datetime.now() + relativedelta(days=-1),
'date_end': datetime.datetime.now() + relativedelta(days=1),
'seats_limited': True,
'seats_max': 10,
})
self.assertFalse(test_event.auto_confirm)
# EventUser create registrations for this event -> no auto confirmation
test_reg1 = self.env['event.registration'].with_user(self.user_eventuser).create({
'name': 'TestReg1',
'event_id': test_event.id,
})
self.assertEqual(
test_reg1.state, 'draft',
'Event: new registration should not be confirmed with auto_confirmation parameter being False')
@mute_logger('odoo.addons.event.models.event_mail')
def test_event_missed_mail_template(self):
""" Check that error on mail sending is ignored if corresponding mail template was deleted """
@@ -122,13 +49,6 @@ class TestEventFlow(EventCase):
'seats_max': 2,
'seats_limited': True,
})
self.assertFalse(test_event.auto_confirm)
# EventUser create registrations for this event
test_reg = self.env['event.registration'].with_user(self.user_eventuser).create({
'name': 'TestReg1',
'event_id': test_event.id,
})
scheduler = self.env['event.mail'].sudo().search([
('event_id', '=', test_event.id),
@@ -138,6 +58,11 @@ class TestEventFlow(EventCase):
# Imagine user deletes mail template for whatever reason
scheduler.template_ref.unlink()
# EventUser create registrations for this event
self.env['event.registration'].with_user(self.user_eventuser).create({
'name': 'TestReg1',
'event_id': test_event.id,
})
# Mails should not be sent
test_reg.action_confirm()
self.assertFalse(scheduler.mail_registration_ids.mail_sent)
+19 -43
View File
@@ -20,7 +20,6 @@ class TestEventInternalsCommon(EventCase):
cls.event_type_complex = cls.env['event.type'].create({
'name': 'Update Type',
'auto_confirm': True,
'has_seats_limitation': True,
'seats_max': 30,
'default_timezone': 'Europe/Paris',
@@ -47,7 +46,6 @@ class TestEventInternalsCommon(EventCase):
cls.reference_end = datetime(2020, 2, 4, 18, 45, 0)
cls.event_0 = cls.env['event.event'].create({
'auto_confirm': True,
'date_begin': cls.reference_beg,
'date_end': cls.reference_end,
'date_tz': 'Europe/Brussels',
@@ -123,12 +121,10 @@ class TestEventData(TestEventInternalsCommon):
})
self.assertEqual(event.date_tz, self.env.user.tz)
self.assertFalse(event.seats_limited)
self.assertFalse(event.auto_confirm)
self.assertEqual(event.event_mail_ids, self.env['event.mail'])
self.assertEqual(event.event_ticket_ids, self.env['event.event.ticket'])
registration = self._create_registrations(event, 1)
self.assertEqual(registration.state, 'draft') # event is not auto confirm
# ------------------------------------------------------------
# FILL SYNC TEST
@@ -146,7 +142,6 @@ class TestEventData(TestEventInternalsCommon):
self.assertEqual(event.date_tz, 'Europe/Paris')
self.assertTrue(event.seats_limited)
self.assertEqual(event.seats_max, event_type.seats_max)
self.assertTrue(event.auto_confirm)
# check 2many fields being populated
self.assertEqual(len(event.event_mail_ids), 1)
self.assertEqual(event.event_mail_ids.interval_nbr, 1)
@@ -182,12 +177,10 @@ class TestEventData(TestEventInternalsCommon):
# setup test records
event_type_default = self.env['event.type'].create({
'name': 'Type Default',
'auto_confirm': True,
'event_type_mail_ids': False,
})
event_type_mails = self.env['event.type'].create({
'name': 'Type Mails',
'auto_confirm': False,
'event_type_mail_ids': [
Command.clear(),
Command.create({
@@ -218,7 +211,7 @@ class TestEventData(TestEventInternalsCommon):
})
mail = event.event_mail_ids[0]
registration = self._create_registrations(event, 1)
self.assertEqual(registration.state, 'open') # event auto confirms
self.assertEqual(registration.state, 'open')
# verify that mail is linked to the registration
self.assertEqual(
set(mail.mapped('mail_registration_ids.registration_id.id')),
@@ -284,11 +277,9 @@ class TestEventData(TestEventInternalsCommon):
# setup test records
event_type_default = self.env['event.type'].create({
'name': 'Type Default',
'auto_confirm': True
})
event_type_tickets = self.env['event.type'].create({
'name': 'Type Tickets',
'auto_confirm': False
})
event_type_tickets.write({
'event_type_ticket_ids': [
@@ -349,7 +340,6 @@ class TestEventData(TestEventInternalsCommon):
})
self.assertEqual(event.date_tz, self.env.user.tz)
self.assertFalse(event.seats_limited)
self.assertFalse(event.auto_confirm)
#Event Communications: when no event type, default configuration
self.assertEqual(len(event.event_mail_ids), 3)
@@ -416,7 +406,6 @@ class TestEventData(TestEventInternalsCommon):
'name': 'Albert Test',
'event_id': event.id,
})
registration.action_confirm()
event.write({
'date_end': datetime(2020, 2, 1, 15, 0, 0),
'seats_max': 1,
@@ -480,13 +469,11 @@ class TestEventData(TestEventInternalsCommon):
# seats: coming from event type configuration
self.assertTrue(event.seats_limited)
self.assertEqual(event.seats_available, event.event_type_id.seats_max)
self.assertEqual(event.seats_unconfirmed, 0)
self.assertEqual(event.seats_reserved, 0)
self.assertEqual(event.seats_used, 0)
self.assertEqual(event.seats_expected, 0)
self.assertEqual(event.seats_taken, 0)
# create registration in order to check the seats computation
self.assertTrue(event.auto_confirm)
reg_open_multiple = self.env['event.registration'].create([{
'event_id': event.id,
'name': 'reg_open',
@@ -505,10 +492,9 @@ class TestEventData(TestEventInternalsCommon):
})
reg_done.write({'state': 'done'})
self.assertEqual(event.seats_available, event.event_type_id.seats_max - 6)
self.assertEqual(event.seats_unconfirmed, 1)
self.assertEqual(event.seats_reserved, 5)
self.assertEqual(event.seats_used, 1)
self.assertEqual(event.seats_expected, 7)
self.assertEqual(event.seats_taken, 6)
# ------------------------------------------------------------
# SEATS AVAILABILITY AND (UN-)ARCHIVING REGISTRATIONS
@@ -518,23 +504,21 @@ class TestEventData(TestEventInternalsCommon):
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)
self.assertEqual(event.seats_taken, 5)
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)
self.assertEqual(event.seats_taken, 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)
self.assertEqual(event.seats_taken, 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)
self.assertEqual(event.seats_taken, 6)
reg_open.action_archive()
self.assertEqual(event.seats_reserved, 4)
@@ -560,18 +544,19 @@ class TestEventData(TestEventInternalsCommon):
with self.assertRaises(exceptions.ValidationError):
reg_draft.write({'state': 'open'})
# With auto-confirm, it is also impossible to create a draft
# registration when the event is full
new_draft_to_autoconfirm = {
# It is not possible to create an open registration (default value)
# when the event is full
new_open_registration = {
'event_id': event.id,
'name': 'New registration with auto confirm'
'name': 'reg_open',
}
with self.assertRaises(exceptions.ValidationError):
self.env['event.registration'].create(new_draft_to_autoconfirm)
self.env['event.registration'].create(new_open_registration)
# If the seats limitation is removed, it becomes possible of course
event.write({'seats_limited': 0})
self.env['event.registration'].create(new_draft_to_autoconfirm)
self.env['event.registration'].create(new_open_registration)
reg_draft.write({'state': 'open'})
@tagged('event_registration')
@@ -803,7 +788,6 @@ class TestEventTicketData(TestEventInternalsCommon):
'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')
@@ -869,23 +853,23 @@ class TestEventTicketData(TestEventInternalsCommon):
'name': f'reg_draft #{idx}',
'event_ticket_id': first_ticket.id,
} for idx in range(3)])
# Draft registrations should not impact seats
reg_draft_multiple.state = 'draft'
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'
})
reg_done.action_set_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)
@@ -898,9 +882,6 @@ class TestEventTicketData(TestEventInternalsCommon):
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)
@@ -910,10 +891,6 @@ class TestEventTicketData(TestEventInternalsCommon):
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)
# It is not possible to set a seats_max value below the current number of confirmed
# registrations. (There is still 1 "used" seat too)
with self.assertRaises(exceptions.ValidationError):
@@ -928,10 +905,9 @@ class TestEventTicketData(TestEventInternalsCommon):
# SEATS AVAILABILITY
# With auto-confirm, it is impossible to create a draft
# registration when the ticket is fully booked (1 used + 1 reserved)
# It is impossible to create an open registration when the
# ticket is fully booked (1 used + 1 reserved)
self.assertEqual(event.seats_available, 0)
first_ticket.event_id.auto_confirm = True
with self.assertRaises(exceptions.ValidationError):
self.env['event.registration'].create({
'event_id': event.id,
@@ -36,7 +36,6 @@ class TestMailSchedule(EventCase, MockEmail):
'name': 'TestEventMail',
'create_date': now,
'user_id': self.user_eventmanager.id,
'auto_confirm': True,
'date_begin': event_date_begin,
'date_end': event_date_end,
'event_mail_ids': [
@@ -44,7 +43,7 @@ class TestMailSchedule(EventCase, MockEmail):
'interval_unit': 'now',
'interval_type': 'after_sub',
'template_ref': 'mail.template,%i' % self.env['ir.model.data']._xmlid_to_res_id('event.event_subscription')}),
(0, 0, { # one day after subscription
(0, 0, { # one hour after subscription
'interval_nbr': 1,
'interval_unit': 'hours',
'interval_type': 'after_sub',
@@ -208,13 +207,13 @@ class TestMailSchedule(EventCase, MockEmail):
# NEW REGISTRATION EFFECT ON SCHEDULERS
# --------------------------------------------------
test_event.write({'auto_confirm': False})
with freeze_time(now_start), self.mock_mail_gateway():
reg3 = self.env['event.registration'].create({
'create_date': now_start,
'event_id': test_event.id,
'name': 'Reg3',
'email': 'reg3@example.com',
'state': 'draft',
})
# no more seats
@@ -286,7 +285,6 @@ class TestMailSchedule(EventCase, MockEmail):
# create event with default event_mail_ids lines
test_event = self.env['event.event'].with_user(self.user_eventmanager).create({
'name': "TestEvent",
'auto_confirm': True,
'date_begin': datetime.now(),
'date_end': datetime.now() + relativedelta(days=1),
'seats_max': 2,
@@ -354,7 +352,6 @@ class TestMailSchedule(EventCase, MockEmail):
with freeze_time(now):
test_event = self.env['event.event'].with_user(self.user_eventmanager).create({
'name': 'TestEventMail',
'auto_confirm': True,
'date_begin': event_date_begin,
'date_end': event_date_end,
'event_mail_ids': [
+12 -26
View File
@@ -35,7 +35,7 @@
class="oe_stat_button"
icon="fa-users"
help="Total Registrations for this Event">
<field name="seats_expected" widget="statinfo" string="Attendees"/>
<field name="seats_taken" widget="statinfo" string="Attendees"/>
</button>
<button name="%(event_barcode_action_main_view)d"
@@ -78,9 +78,8 @@
<label for="seats_limited" string="Limit Registrations"/>
<div>
<field name="seats_limited"/>
<span invisible="not seats_limited" required="not seats_limited">to <field name="seats_max" class="oe_inline o_input_9ch"/> Confirmed Attendees</span>
<span invisible="not seats_limited" required="not seats_limited">to <field name="seats_max" class="oe_inline o_input_9ch"/> Attendees</span>
</div>
<field name="auto_confirm"/>
<field name="badge_format"/>
<field name="badge_image"/>
</group>
@@ -148,11 +147,10 @@
<field name="date_begin" readonly="1" widget="date"/>
<field name="date_end" readonly="1" widget="date"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="hide"/>
<field name="seats_expected" string="Expected Attendees" sum="Total" readonly="1"/>
<field name="seats_taken" string="Total Attendees" sum="Total" readonly="1"/>
<field name="seats_used" sum="Total" readonly="1"/>
<field name="seats_max" string="Maximum Seats" sum="Total" readonly="1" optional="hide"/>
<field name="seats_reserved" sum="Total" readonly="1" optional="hide"/>
<field name="seats_unconfirmed" string="Unconfirmed Seats" sum="Total" readonly="1" optional="hide"/>
<field name="stage_id" readonly="1"/>
<field name="message_needaction" readonly="1" column_invisible="True"/>
<field name="activity_exception_decoration" widget="activity_exception" readonly="1"/>
@@ -186,11 +184,9 @@
<field name="address_id"/>
<field name="date_begin"/>
<field name="date_end"/>
<field name="auto_confirm"/>
<field name="seats_unconfirmed"/>
<field name="seats_reserved"/>
<field name="seats_used"/>
<field name="seats_expected"/>
<field name="seats_taken"/>
<field name="legend_blocked"/>
<field name="legend_normal"/>
<field name="legend_done"/>
@@ -216,24 +212,15 @@
<div class="o_kanban_record_title o_text_overflow" t-att-title="record.name.value">
<field name="name"/>
</div>
<div t-if="record.address_id.value" class="d-flex">
<i class="fa fa-map-marker mt-1 me-1" title="Location"/>
<span t-esc="record.address_id.value"/>
<div t-if="record.address_id.value" class="d-flex ps-1">
<i class="fa fa-map-marker mt-1 me-2 text-center ps-1" title="Location"/>
<span t-esc="record.address_id.value" class="ms-1"/>
</div>
<div class="d-flex ps-1">
<i class="fa fa-group mt-1 me-2 text-center" title="Attendees"/>
<span t-out="record.seats_taken.raw_value" class="me-1"/> Attendees
</div>
</div>
<h5 class="p-0">
<a name="%(event_registration_action_stats_from_event)d"
type="action"
context="{'search_default_expected': True}">
<t t-esc="record.seats_expected.raw_value"/> Expected attendees
</a>
<t t-set="total_seats" t-value="record.seats_reserved.raw_value + record.seats_used.raw_value"/>
<div class="pt-2 pt-md-0" t-if="total_seats > 0 and ! record.auto_confirm.raw_value"><br/>
<a class="ps-2" name="%(event_registration_action_stats_from_event)d" type="action" context="{'search_default_confirmed': True}">
<i class="fa fa-level-up fa-rotate-90" title="Confirmed"/><span class="ps-2"><t t-esc="total_seats"/> Confirmed</span>
</a>
</div>
</h5>
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
<field name="activity_ids" widget="kanban_activity"/>
@@ -259,10 +246,9 @@
<field name="arch" type="xml">
<calendar date_start="date_begin" date_stop="date_end" string="Event Organization" mode="month" color="event_type_id" event_limit="5">
<field name="user_id" avatar_field="avatar_128"/>
<field name="seats_expected"/>
<field name="seats_taken"/>
<field name="seats_reserved"/>
<field name="seats_used"/>
<field name="seats_unconfirmed"/>
<field name="event_type_id" filters="1" invisible="1"/>
</calendar>
</field>
+14 -11
View File
@@ -24,7 +24,7 @@
decoration-muted="state == 'cancel'" widget="badge"/>
<field name="company_id" groups="base.group_multi_company" optional="hide" readonly="state != 'draft'"/>
<field name="message_needaction" column_invisible="True"/>
<button name="action_confirm" string="Confirm" type="object" icon="fa-check"
<button name="action_confirm" string="Registered" type="object" icon="fa-check"
invisible="not active or state != 'draft'"/>
<button name="action_set_done" string="Mark as Attending" type="object" icon="fa-level-down"
invisible="not active or state != 'open'"/>
@@ -45,15 +45,13 @@
<header>
<button name="action_send_badge_email" string="Send by Email" type="object" class="oe_highlight"
invisible="not active or state != 'open' and state != 'done'"/>
<button name="action_confirm" string="Confirm" type="object" class="oe_highlight"
<button name="action_confirm" string="Registered" type="object" class="oe_highlight"
invisible="not active or state != 'draft'"/>
<button name="action_set_done" string="Attended" type="object" class="oe_highlight"
invisible="not active or state != 'open'"/>
<button name="action_set_draft" string="Set To Unconfirmed" type="object"
invisible="not active or state != 'cancel' and state != 'done'"/>
<button name="action_cancel" string="Cancel Registration" type="object"
invisible="not active or state != 'open' and state != 'draft'"/>
<field name="state" nolabel="1" colspan="2" widget="statusbar" statusbar_visible="draft,open,done"
invisible="not active or state == 'cancel'"/>
<field name="state" nolabel="1" colspan="2" widget="statusbar" statusbar_visible="open,done"
readonly="false" options="{'clickable': '1'}"/>
</header>
<sheet string="Registration">
@@ -67,10 +65,10 @@
<field name="company_name" placeholder='e.g. "Azure Interior"'/>
</group>
<group string="Event Information" name="event">
<field class="text-break" name="event_id" readonly="state != 'draft'"
<field class="text-break" name="event_id" readonly="event_id and state != 'draft' and id"
context="{'name_with_seats_availability': True}" options="{'no_create': True}"/>
<field name="barcode" groups="base.group_no_one"/>
<field name="event_ticket_id" invisible="not event_id" readonly="state != 'draft'"
<field name="event_ticket_id" invisible="not event_id" readonly="event_ticket_id and state != 'draft' and id"
context="{'name_with_seats_availability': True}" options="{'no_open': True, 'no_create': True}"
domain="[('event_id', '=', event_id)]"/>
<field name="partner_id"/>
@@ -233,10 +231,10 @@
<field name="event_user_id" string="Responsible" invisible="1"/>
<field name="event_organizer_id" string="Organizer" invisible="1"/>
<filter string="Ongoing Events" name="filter_is_ongoing" domain="[('event_id.is_ongoing', '=', True)]"/>
<filter string="Expected" name="expected" domain="[('state', 'in', ['draft', 'open', 'done'])]"/>
<filter string="Taken" name="taken" domain="[('state', 'in', ['open', 'done'])]"/>
<separator/>
<filter string="Unconfirmed" name="unconfirmed" domain="[('state', '=', 'draft')]"/>
<filter string="Confirmed" name="confirmed" domain="[('state', '=', 'open')]"/>
<filter string="Registered" name="confirmed" domain="[('state', '=', 'open')]"/>
<filter string="Attended" name="attended" domain="[('state', '=', 'done')]"/>
<separator/>
<filter string="Registration Date" name="filter_create_date" date="create_date"/>
@@ -293,7 +291,11 @@
<field name="name">Attendees</field>
<field name="view_mode">tree,kanban,form,calendar,graph</field>
<field name="domain">[('event_id', '=', active_id)]</field>
<field name="context">{'default_event_id': active_id, 'name_with_seats_availability': True}</field>
<field name="context">{
'default_event_id': active_id,
'name_with_seats_availability': True,
'search_default_taken': True,
}</field>
<field name="search_view_id" ref="event_registration_view_search_event_specific"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
@@ -347,6 +349,7 @@
<field name="view_mode">graph,pivot,kanban,tree,form</field>
<field name="context">{
'search_default_filter_last_month_creation': 1,
'search_default_taken': 1,
'search_default_status': 2,
'search_default_group_by_create_date_day': 3,
'search_default_group_event': 1,
+2 -6
View File
@@ -77,8 +77,7 @@
<field name="start_sale_datetime" optional="show"/>
<field name="end_sale_datetime" optional="show"/>
<field name="seats_max" sum="Total" width="105px" string="Maximum"/>
<field name="seats_reserved" sum="Total" width="105px" string="Confirmed"/>
<field name="seats_unconfirmed" sum="Total" width="110px" string="Unconfirmed"/>
<field name="seats_taken" sum="Total" width="105px" string="Taken"/>
<field name="color" widget="color" optional="show"/>
</tree>
</field>
@@ -100,7 +99,6 @@
</group><group>
<field name="seats_max"/>
<field name="seats_reserved"/>
<field name="seats_unconfirmed"/>
</group>
</group>
</sheet>
@@ -117,7 +115,6 @@
<field name="name"/>
<field name="seats_max"/>
<field name="seats_reserved"/>
<field name="seats_unconfirmed"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
@@ -127,7 +124,7 @@
</div>
</div>
<div><i>
<t t-out="record.seats_reserved.value"/> reserved + <t t-out="record.seats_reserved.value"/> unconfirmed
<t t-out="record.seats_reserved.value"/> reserved
</i></div>
</div>
</t>
@@ -173,7 +170,6 @@
<group>
<field name="seats_max"/>
<field name="seats_reserved"/>
<field name="seats_unconfirmed"/>
<field name="seats_used"/>
<field name="is_expired"/>
</group>
+1 -3
View File
@@ -23,11 +23,9 @@
<field name="has_seats_limitation"/>
<span invisible="not has_seats_limitation" required="not has_seats_limitation">
to <field name="seats_max" class="oe_inline o_input_9ch"/>
Confirmed Attendees
Attendees
</span>
</div>
<label for="auto_confirm" string="Autoconfirmation"/>
<field name="auto_confirm" nolabel="1"/>
</group>
</group>
@@ -18,7 +18,6 @@ class TestEventBoothSaleWData(TestEventBoothSaleCommon, TestSalesCommon):
cls.event_0 = cls.env['event.event'].create({
'name': 'TestEvent',
'auto_confirm': True,
'date_begin': fields.Datetime.to_string(datetime.today() + timedelta(days=1)),
'date_end': fields.Datetime.to_string(datetime.today() + timedelta(days=15)),
'date_tz': 'Europe/Brussels',
+2 -2
View File
@@ -81,11 +81,11 @@ class EventLeadRule(models.Model):
'Per Order: A single Lead is created per Ticket Batch/Sale Order (B2B)')
lead_creation_trigger = fields.Selection([
('create', 'Attendees are created'),
('confirm', 'Attendees are confirmed'),
('confirm', 'Attendees are registered'),
('done', 'Attendees attended')],
string='When', default='create', required=True,
help='Creation: at attendee creation;\n'
'Confirmation: when attendee is confirmed, manually or automatically;\n'
'Registered: at attendee registration, manually or automatically;\n'
'Attended: when attendance is confirmed and registration set to done;')
# Filters
event_type_ids = fields.Many2many(
-1
View File
@@ -134,7 +134,6 @@ class TestEventCrmCommon(EventCrmCase):
cls.event_0 = cls.env['event.event'].create({
'name': 'TestEvent',
'auto_confirm': True,
'date_begin': fields.Datetime.to_string(datetime.today() + timedelta(days=1)),
'date_end': fields.Datetime.to_string(datetime.today() + timedelta(days=15)),
'date_tz': 'Europe/Brussels',
-1
View File
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from . import account_move
from . import event_event
from . import event_registration
from . import event_ticket
-16
View File
@@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountMove(models.Model):
_inherit = 'account.move'
def _invoice_paid_hook(self):
""" When an invoice linked to a sales order selling registrations is
paid confirm attendees. Attendees should indeed not be confirmed before
full payment. """
res = super(AccountMove, self)._invoice_paid_hook()
self.mapped('line_ids.sale_line_ids')._update_registrations(confirm=True, mark_as_paid=True)
return res
+22 -21
View File
@@ -8,15 +8,15 @@ from odoo.tools import float_is_zero
class EventRegistration(models.Model):
_inherit = 'event.registration'
is_paid = fields.Boolean('Is Paid')
# TDE FIXME: maybe add an onchange on sale_order_id
sale_order_id = fields.Many2one('sale.order', string='Sales Order', ondelete='cascade', copy=False)
sale_order_line_id = fields.Many2one('sale.order.line', string='Sales Order Line', ondelete='cascade', copy=False)
payment_status = fields.Selection(string="Payment Status", selection=[
('to_pay', 'Not Paid'),
('paid', 'Paid'),
sale_status = fields.Selection(string="Sale Status", selection=[
('to_pay', 'Not Sold'),
('sold', 'Sold'),
('free', 'Free'),
], compute="_compute_payment_status", compute_sudo=True)
], compute="_compute_registration_status", compute_sudo=True, store=True)
state = fields.Selection(default=None, compute="_compute_registration_status", store=True, readonly=False)
utm_campaign_id = fields.Many2one(compute='_compute_utm_campaign_id', readonly=False,
store=True, ondelete="set null")
utm_source_id = fields.Many2one(compute='_compute_utm_source_id', readonly=False,
@@ -24,17 +24,21 @@ class EventRegistration(models.Model):
utm_medium_id = fields.Many2one(compute='_compute_utm_medium_id', readonly=False,
store=True, ondelete="set null")
@api.depends('is_paid', 'sale_order_id.currency_id', 'sale_order_line_id.price_total')
def _compute_payment_status(self):
for record in self:
so = record.sale_order_id
so_line = record.sale_order_line_id
if not so or float_is_zero(so_line.price_total, precision_digits=so.currency_id.rounding):
record.payment_status = 'free'
elif record.is_paid:
record.payment_status = 'paid'
@api.depends('sale_order_id.state', 'sale_order_id.currency_id', 'sale_order_line_id.price_total')
def _compute_registration_status(self):
self.filtered(lambda reg: not reg.state).state = 'draft'
for so_line, registrations in self.grouped('sale_order_line_id').items():
cancelled_registrations = registrations.filtered(lambda reg: reg.sale_order_id.state == 'cancel')
cancelled_registrations.state = 'cancel'
if not so_line or float_is_zero(so_line.price_total, precision_digits=so_line.currency_id.rounding):
registrations.sale_status = 'free'
registrations.filtered(lambda reg: reg.state == 'draft').write({"state": "open"})
else:
record.payment_status = 'to_pay'
sold_registrations = registrations.filtered(lambda reg: reg.sale_order_id.state == 'sale')
sold_registrations.sale_status = 'sold'
(registrations - sold_registrations).sale_status = 'to_pay'
sold_registrations.filtered(lambda reg: reg.state in {'draft', 'cancel'}).write({"state": "open"})
(registrations - sold_registrations - cancelled_registrations).state = 'draft'
@api.depends('sale_order_id')
def _compute_utm_campaign_id(self):
@@ -124,14 +128,11 @@ class EventRegistration(models.Model):
views_or_xmlid='event_sale.event_ticket_id_change_exception',
render_context=render_context)
def _action_set_paid(self):
self.write({'is_paid': True})
def _get_registration_summary(self):
res = super(EventRegistration, self)._get_registration_summary()
res.update({
'payment_status': self.payment_status,
'payment_status_value': dict(self._fields['payment_status']._description_selection(self.env))[self.payment_status],
'has_to_pay': self.payment_status == 'to_pay',
'sale_status': self.sale_status,
'sale_status_value': dict(self._fields['sale_status']._description_selection(self.env))[self.sale_status],
'has_to_pay': self.sale_status == 'to_pay',
})
return res
+5 -12
View File
@@ -1,9 +1,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo import fields, models, _
from odoo.exceptions import ValidationError
from odoo.osv import expression
class SaleOrder(models.Model):
_inherit = "sale.order"
@@ -14,7 +15,7 @@ class SaleOrder(models.Model):
in website_sale controller shop/address that updates customer, but not
only. """
result = super(SaleOrder, self).write(vals)
if vals.get('partner_id'):
if any(line.product_type == 'event' for line in self.order_line) and vals.get('partner_id'):
registrations_toupdate = self.env['event.registration'].search([('sale_order_id', 'in', self.ids)])
registrations_toupdate.write({'partner_id': vals['partner_id']})
return result
@@ -28,18 +29,14 @@ class SaleOrder(models.Model):
if so_lines_missing_events:
so_lines_descriptions = "".join([f"- {so_line_description.name} \n" for so_line_description in so_lines_missing_events])
raise ValidationError(_("Please make sure all your event related lines are configured before confirming this order: \n%s" % so_lines_descriptions))
# confirm registration if it was free (otherwise it will be confirmed once invoice fully paid)
so.order_line._update_registrations(confirm=so.amount_total == 0, cancel_to_draft=False)
# Initialize registrations
so.order_line._init_registrations()
if len(self) == 1:
return self.env['ir.actions.act_window'].with_context(
default_sale_order_id=so.id
)._for_xml_id('event_sale.action_sale_order_event_registration')
return res
def _action_cancel(self):
self.order_line._cancel_associated_registrations()
return super()._action_cancel()
def action_view_attendee_list(self):
action = self.env["ir.actions.actions"]._for_xml_id("event.event_registration_action_tree")
action['domain'] = [('sale_order_id', 'in', self.ids)]
@@ -57,10 +54,6 @@ class SaleOrder(models.Model):
for sale_order in self:
sale_order.attendee_count = attendee_count_data.get(sale_order.id, 0)
def unlink(self):
self.order_line._unlink_associated_registrations()
return super(SaleOrder, self).unlink()
def _get_product_catalog_domain(self):
"""Override of `_get_product_catalog_domain` to extend the domain.
+7 -29
View File
@@ -14,6 +14,7 @@ class SaleOrderLine(models.Model):
'event.event.ticket', string='Event Ticket',
compute="_compute_event_ticket_id", store=True, readonly=False, precompute=True,
help="Choose an event ticket and it will automatically create a registration for this event ticket.")
registration_ids = fields.One2many('event.registration', 'sale_order_line_id', string="Registrations")
@api.depends('state', 'event_id')
def _compute_product_uom_readonly(self):
@@ -21,37 +22,24 @@ class SaleOrderLine(models.Model):
event_lines.update({'product_uom_readonly': True})
super(SaleOrderLine, self - event_lines)._compute_product_uom_readonly()
def _update_registrations(self, confirm=True, cancel_to_draft=False, registration_data=None, mark_as_paid=False):
""" Create or update registrations linked to a sales order line. A sale
def _init_registrations(self):
""" Create registrations linked to a sales order line. A sale
order line has a product_uom_qty attribute that will be the number of
registrations linked to this line. This method update existing registrations
and create new one for missing one. """
RegistrationSudo = self.env['event.registration'].sudo()
registrations = RegistrationSudo.search([('sale_order_line_id', 'in', self.ids)])
registrations linked to this line. """
registrations_vals = []
for so_line in self:
if not so_line.product_type == 'event':
continue
existing_registrations = registrations.filtered(lambda self: self.sale_order_line_id.id == so_line.id)
if confirm:
existing_registrations.filtered(lambda self: self.state not in ['open', 'cancel']).action_confirm()
if mark_as_paid:
existing_registrations.filtered(lambda self: not self.is_paid)._action_set_paid()
if cancel_to_draft:
existing_registrations.filtered(lambda self: self.state == 'cancel').action_set_draft()
for _count in range(int(so_line.product_uom_qty) - len(existing_registrations)):
for _count in range(int(so_line.product_uom_qty) - len(so_line.registration_ids)):
values = {
'sale_order_line_id': so_line.id,
'sale_order_id': so_line.order_id.id
'sale_order_id': so_line.order_id.id,
}
# TDE CHECK: auto confirmation
if registration_data:
values.update(registration_data.pop())
registrations_vals.append(values)
if registrations_vals:
RegistrationSudo.create(registrations_vals)
self.env['event.registration'].sudo().create(registrations_vals)
return True
@api.depends('product_id')
@@ -82,16 +70,6 @@ class SaleOrderLine(models.Model):
"""
super()._compute_name()
def unlink(self):
self._unlink_associated_registrations()
return super(SaleOrderLine, self).unlink()
def _cancel_associated_registrations(self):
self.env['event.registration'].search([('sale_order_line_id', 'in', self.ids)]).action_cancel()
def _unlink_associated_registrations(self):
self.env['event.registration'].search([('sale_order_line_id', 'in', self.ids)]).unlink()
def _get_sale_order_line_multiline_description_sale(self):
""" We override this method because we decided that:
The default description of a sales order line containing a ticket must be different than the default description when no ticket is present.
+5 -11
View File
@@ -39,10 +39,9 @@ class EventSaleReport(models.Model):
sale_price = fields.Float('Revenues', readonly=True)
sale_price_untaxed = fields.Float('Untaxed Revenues', readonly=True)
invoice_partner_id = fields.Many2one('res.partner', string='Invoice Address', readonly=True)
is_paid = fields.Boolean('Is Paid', readonly=True)
payment_status = fields.Selection(string="Payment Status", selection=[
('to_pay', 'Not Paid'),
('paid', 'Paid'),
sale_status = fields.Selection(string="Payment Status", selection=[
('to_pay', 'Not Sold'),
('sold', 'Sold'),
('free', 'Free'),
])
company_id = fields.Many2one('res.company', string='Company', readonly=True)
@@ -81,7 +80,7 @@ SELECT
event_registration.active AS active,
event_registration.sale_order_id AS sale_order_id,
event_registration.sale_order_line_id AS sale_order_line_id,
event_registration.is_paid AS is_paid,
event_registration.sale_status AS sale_status,
event_event.event_type_id AS event_type_id,
event_event.date_begin AS event_date_begin,
@@ -109,12 +108,7 @@ SELECT
sale_order_line.price_subtotal
/ CASE COALESCE(sale_order.currency_rate, 0) WHEN 0 THEN 1.0 ELSE sale_order.currency_rate END
/ sale_order_line.product_uom_qty
END AS sale_price_untaxed,
CASE
WHEN sale_order_line.price_total = 0 THEN 'free'
WHEN event_registration.is_paid THEN 'paid'
ELSE 'to_pay'
END payment_status""" + (',\n ' + ',\n '.join(select) if select else '')
END AS sale_price_untaxed""" + (',\n ' + ',\n '.join(select) if select else '')
def _from_clause(self, *join_):
# Extra clauses formatted as `column1`, `column2`...
@@ -96,9 +96,9 @@
<field name="company_id"/>
<filter string="Non-free tickets" name="priced_tickets" domain="[('event_ticket_price', '!=', 0)]"/>
<separator/>
<filter string="Free" name="free" domain="[('payment_status', '=', 'free')]"/>
<filter string="Pending payment" name="payment_pending" domain="[('payment_status', '=', 'to_pay')]"/>
<filter string="Paid" name="is_paid" domain="[('payment_status', '=', 'paid')]"/>
<filter string="Free" name="free" domain="[('sale_status', '=', 'free')]"/>
<filter string="Pending payment" name="payment_pending" domain="[('sale_status', '=', 'to_pay')]"/>
<filter string="Sold" name="is_sold" domain="[('sale_status', '=', 'sold')]"/>
<separator/>
<filter string="Registration Date" name="event_registration_create_date" date="event_registration_create_date" default_period="this_year"/>
<separator/>
-2
View File
@@ -24,7 +24,6 @@ class TestEventSaleCommon(EventCase, TestSalesCommon):
cls.event_type_tickets = cls.env['event.type'].create({
'name': 'Update Type',
'auto_confirm': True,
'has_seats_limitation': True,
'seats_max': 30,
'default_timezone': 'Europe/Paris',
@@ -39,7 +38,6 @@ class TestEventSaleCommon(EventCase, TestSalesCommon):
cls.event_0 = cls.env['event.event'].create({
'name': 'TestEvent',
'auto_confirm': True,
'date_begin': fields.Datetime.to_string(datetime.today() + timedelta(days=1)),
'date_end': fields.Datetime.to_string(datetime.today() + timedelta(days=15)),
'date_tz': 'Europe/Brussels',
+26 -22
View File
@@ -3,7 +3,7 @@
from odoo.addons.event_sale.tests.common import TestEventSaleCommon
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.exceptions import ValidationError
from odoo.tests import tagged
from odoo.tests.common import users
@@ -197,7 +197,7 @@ class TestEventSale(TestEventSaleCommon):
@users('user_sales_salesman')
def test_event_sale_free_confirm(self):
"""Check that even with the event's `no_confirm`, free registrations are immediately
"""Check that free registrations are immediately
confirmed if the seats are available.
"""
TICKET_COUNT = 3
@@ -206,7 +206,6 @@ class TestEventSale(TestEventSaleCommon):
# Limiting seats
self.event_0.write({
"auto_confirm": False,
"seats_limited": True,
"seats_max": 5
})
@@ -235,7 +234,7 @@ class TestEventSale(TestEventSaleCommon):
@users('user_sales_salesman')
def test_event_sale_free_full_event_no_confirm(self):
"""Check that even free registrations are not immediately confirmed if there are not
"""Check that even free registrations are not confirmed if there are not
enough seats available for the event.
"""
TICKET_COUNT = 3
@@ -244,7 +243,6 @@ class TestEventSale(TestEventSaleCommon):
# Limiting event seats
self.event_0.write({
"auto_confirm": False,
"seats_limited": True,
"seats_max": 2
})
@@ -269,26 +267,28 @@ class TestEventSale(TestEventSaleCommon):
]
})
# Confirming the SO will raise an error if there is not enough seats
with self.assertRaises(ValidationError):
customer_so.action_confirm()
editor = self.env['registration.editor'].with_context({
'default_sale_order_id': customer_so.id
}).create({})
self.assertTrue(editor.seats_available_insufficient)
editor.action_make_registration()
self.assertEqual(len(self.event_0.registration_ids), TICKET_COUNT)
self.assertTrue(all(reg.state == "draft" for reg in self.event_0.registration_ids))
with self.assertRaises(ValidationError):
editor.action_make_registration()
@users('user_sales_salesman')
def test_event_sale_free_full_ticket_no_confirm(self):
"""Check that even free registrations are not immediately confirmed if there are not enough
"""Check that even free registrations are not confirmed if there are not enough
seats available for the requested tickets.
"""
TICKET_COUNT = 3
customer_so = self.customer_so.with_user(self.env.user)
ticket = self.event_0.event_ticket_ids[0]
self.event_0.write({"auto_confirm": False})
# Limiting ticket seats
ticket.write({
"seats_limited": True,
@@ -314,15 +314,18 @@ class TestEventSale(TestEventSaleCommon):
]
})
# Confirming the SO will raise an error if there is not enough seats
with self.assertRaises(ValidationError):
customer_so.action_confirm()
editor = self.env['registration.editor'].with_context({
'default_sale_order_id': customer_so.id
}).create({})
self.assertTrue(editor.seats_available_insufficient)
editor.action_make_registration()
self.assertEqual(len(self.event_0.registration_ids), TICKET_COUNT)
self.assertTrue(all(reg.state == "draft" for reg in self.event_0.registration_ids))
with self.assertRaises(ValidationError):
editor.action_make_registration()
def test_ticket_price_with_currency_conversion(self):
def _prepare_currency(self, currency_name):
@@ -446,29 +449,30 @@ class TestEventSale(TestEventSaleCommon):
@users('user_salesman')
def test_unlink_so(self):
""" This test ensures that when deleting a sale order, if the latter is linked to an event registration,
the number of expected seats will be correctly updated """
it is also deleted """
event = self.env['event.event'].browse(self.event_0.ids)
self.register_person.action_make_registration()
self.assertEqual(event.seats_expected, 1)
self.assertEqual(len(event.registration_ids), 1)
self.sale_order.unlink()
self.assertEqual(event.seats_expected, 0)
self.assertEqual(len(event.registration_ids), 0)
@users('user_salesman')
def test_unlink_soline(self):
""" This test ensures that when deleting a sale order line, if the latter is linked to an event registration,
the number of expected seats will be correctly updated """
it is also deleted """
event = self.env['event.event'].browse(self.event_0.ids)
self.register_person.action_make_registration()
self.assertEqual(event.seats_expected, 1)
self.assertEqual(len(event.registration_ids), 1)
self.sale_order.order_line.unlink()
self.assertEqual(event.seats_expected, 0)
self.assertEqual(len(event.registration_ids), 0)
@users('user_salesman')
def test_cancel_so(self):
""" This test ensures that when canceling a sale order, if the latter is linked to an event registration,
the number of expected seats will be correctly updated """
it is also cancelled """
event = self.env['event.event'].browse(self.event_0.ids)
self.register_person.action_make_registration()
self.assertEqual(event.seats_expected, 1)
self.assertEqual(len(event.registration_ids), 1)
self.sale_order._action_cancel()
self.assertEqual(event.seats_expected, 0)
self.assertEqual(len(event.registration_ids), 1)
self.assertEqual(event.registration_ids.state, 'cancel')
@@ -30,7 +30,6 @@ class TestEventSpecific(TestEventSaleCommon):
event_event_form.date_begin = datetime.datetime.now() + relativedelta(days=2)
event_event_form.date_end = datetime.datetime.now() + relativedelta(days=3)
event_event_form.event_type_id = event_type # Set the template
event_event_form.auto_confirm = True
# Create second ticket (VIP)
with event_event_form.event_ticket_ids.new() as ticket_line:
ticket_line.name = 'VIP (Very Important Pastafarian)'
@@ -8,7 +8,11 @@
<field name="arch" type="xml">
<field name="event_id" position="after">
<field name="sale_order_id" optional="hide"/>
<field name="payment_status" optional="hide"/>
</field>
<field name="state" position="after">
<field name="sale_status" optional="show" widget="badge"
decoration-success="sale_status == 'sold'"
decoration-danger="sale_status == 'to_pay'"/>
</field>
</field>
</record>
@@ -20,17 +24,28 @@
<field name="arch" type="xml">
<xpath expr="//div[@id='event_attendees_kanban_icons_desktop']" position="inside">
<div class="mt-auto pt-2">
<field name="payment_status"/>
<field name="sale_status"/>
</div>
</xpath>
<xpath expr="//div[@id='event_ticket_id']" position="before">
<div class="d-md-none" >
<field name="payment_status"/>
<field name="sale_status"/>
</div>
</xpath>
</field>
</record>
<record id="event_registration_view_graph" model="ir.ui.view">
<field name="name">event.registration.graph.inherit.event.sale</field>
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.view_event_registration_graph"/>
<field name="arch" type="xml">
<field name="event_id" position="after">
<field name="sale_status"/>
</field>
</field>
</record>
<record id="event_registration_ticket_view_form" model="ir.ui.view">
<field name="name">event.registration.form.inherit</field>
<field name="model">event.registration</field>
@@ -48,8 +63,11 @@
<field name="sale_order_id" invisible="1"/>
</xpath>
<xpath expr="//group" position="before">
<field name="is_paid" invisible="1"/>
<widget name="web_ribbon" title="Paid" bg_color="text-bg-success" invisible="not is_paid"/>
<field name="sale_status" invisible="1"/>
<widget name="web_ribbon" title="Sold" bg_color="text-bg-success"
invisible="sale_status != 'sold'"/>
<widget name="web_ribbon" title="Not Sold" bg_color="text-bg-danger"
invisible="sale_status in ('sold', 'free') or not id"/>
</xpath>
<group name="utm_link" position="before">
<group string="Transaction" groups="base.group_no_one">
@@ -56,28 +56,30 @@ class RegistrationEditor(models.TransientModel):
('event_ticket_id', 'in', sale_order.mapped('order_line.event_ticket_id').ids),
('state', '!=', 'cancel')])
so_lines = sale_order.order_line.filtered('event_ticket_id')
so_line_to_reg = registrations.grouped('sale_order_line_id')
attendee_list = []
for so_line in [l for l in sale_order.order_line if l.event_ticket_id]:
existing_registrations = [r for r in registrations if r.event_ticket_id == so_line.event_ticket_id and r.sale_order_line_id == so_line]
for reg in existing_registrations:
attendee_list.append([0, 0, {
'event_id': reg.event_id.id,
'event_ticket_id': reg.event_ticket_id.id,
'registration_id': reg.id,
'name': reg.name,
'email': reg.email,
'phone': reg.phone,
'sale_order_line_id': so_line.id,
}])
for count in range(int(so_line.product_uom_qty) - len(existing_registrations)):
attendee_list.append([0, 0, {
'event_id': so_line.event_id.id,
'event_ticket_id': so_line.event_ticket_id.id,
'sale_order_line_id': so_line.id,
'name': so_line.order_partner_id.name,
'email': so_line.order_partner_id.email,
'phone': so_line.order_partner_id.phone,
}])
for so_line in so_lines:
registrations = so_line_to_reg.get(so_line, self.env['event.registration'])
# Add existing registrations
attendee_list += [[0, 0, {
'event_id': reg.event_id.id,
'event_ticket_id': reg.event_ticket_id.id,
'registration_id': reg.id,
'name': reg.name,
'email': reg.email,
'phone': reg.phone,
'sale_order_line_id': so_line.id,
}] for reg in registrations]
# Add new registrations
attendee_list += [[0, 0, {
'event_id': so_line.event_id.id,
'event_ticket_id': so_line.event_ticket_id.id,
'sale_order_line_id': so_line.id,
'name': so_line.order_partner_id.name,
'email': so_line.order_partner_id.email,
'phone': so_line.order_partner_id.phone,
}] for _count in range(int(so_line.product_uom_qty) - len(registrations))]
res['event_registration_ids'] = attendee_list
res = self._convert_to_write(res)
return res
@@ -86,15 +88,12 @@ class RegistrationEditor(models.TransientModel):
self.ensure_one()
registrations_to_create = []
for registration_line in self.event_registration_ids:
values = registration_line.get_registration_data()
if registration_line.registration_id:
registration_line.registration_id.write(values)
registration_line.registration_id.write(registration_line._prepare_registration_data())
else:
registrations_to_create.append(values)
registrations_to_create.append(registration_line._prepare_registration_data(include_event_values=True))
self.env['event.registration'].create(registrations_to_create)
self.sale_order_id.order_line._update_registrations(
confirm=self.sale_order_id.amount_total == 0 and not self.seats_available_insufficient)
return {'type': 'ir.actions.act_window_close'}
@@ -114,15 +113,19 @@ class RegistrationEditorLine(models.TransientModel):
phone = fields.Char(string='Phone')
name = fields.Char(string='Name')
def get_registration_data(self):
def _prepare_registration_data(self, include_event_values=False):
self.ensure_one()
return {
'event_id': self.event_id.id,
'event_ticket_id': self.event_ticket_id.id,
registration_data = {
'partner_id': self.editor_id.sale_order_id.partner_id.id,
'name': self.name or self.editor_id.sale_order_id.partner_id.name,
'phone': self.phone or self.editor_id.sale_order_id.partner_id.phone or self.editor_id.sale_order_id.partner_id.mobile,
'email': self.email or self.editor_id.sale_order_id.partner_id.email,
'sale_order_id': self.editor_id.sale_order_id.id,
'sale_order_line_id': self.sale_order_line_id.id,
}
if include_event_values:
registration_data.update({
'event_id': self.event_id.id,
'event_ticket_id': self.event_ticket_id.id,
'sale_order_id': self.editor_id.sale_order_id.id,
'sale_order_line_id': self.sale_order_line_id.id,
})
return registration_data
@@ -30,7 +30,6 @@ class TestSMSSchedule(EventCase, SMSCase):
})
cls.test_event = cls.env['event.event'].create({
'auto_confirm': True,
'date_begin': fields.Datetime.to_string(datetime.today() + timedelta(days=1)),
'date_end': fields.Datetime.to_string(datetime.today() + timedelta(days=15)),
'date_tz': 'Europe/Brussels',
@@ -18,7 +18,7 @@
invisible="event_registrations_open"/>
<button name="action_mass_mailing_attendees" type="object" string="Contact Attendees"
groups="mass_mailing.group_mass_mailing_user"
invisible="seats_expected == 0"/>
invisible="seats_taken == 0"/>
</xpath>
</field>
</record>
@@ -14,7 +14,6 @@
</record>
<record id="event_type_data_full" model="event.type">
<field name="auto_confirm" eval="True"/>
<field name="default_timezone">Europe/Paris</field>
<field name="event_type_booth_ids" eval="[
(5, 0),
-3
View File
@@ -121,7 +121,6 @@ class TestEventFullCommon(EventCrmCase, TestSalesCommon, MockVisitor):
subscription_template = cls.env.ref('event.event_subscription')
subscription_template.write({'report_template_ids': [(6, 0, test_registration_report.ids)]})
cls.test_event_type = cls.env['event.type'].create({
'auto_confirm': True,
'default_timezone': 'Europe/Paris',
'event_type_booth_ids': [
(0, 0, {'booth_category_id': cls.event_booth_categories[0].id,
@@ -229,7 +228,6 @@ class TestEventFullCommon(EventCrmCase, TestSalesCommon, MockVisitor):
cls.test_event = cls.env['event.event'].create({
'name': 'Test Event',
'auto_confirm': True,
'date_begin': datetime.now() + timedelta(days=1),
'date_end': datetime.now() + timedelta(days=5),
'date_tz': 'Europe/Brussels',
@@ -339,7 +337,6 @@ class TestWEventCommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal, MockVisitor
)
self.event = self.env['event.event'].create({
'name': 'Online Reveal TestEvent',
'auto_confirm': True,
'stage_id': self.env.ref('event.event_stage_booked').id,
'address_id': False,
'user_id': self.user_demo.id,
@@ -36,7 +36,6 @@ class TestEventEvent(TestEventFullCommon):
# check result
self.assertEqual(event.address_id, self.env.user.company_id.partner_id)
self.assertTrue(event.auto_confirm)
self.assertEqual(event.country_id, self.env.user.company_id.country_id)
self.assertEqual(event.date_tz, 'Europe/Paris')
self.assertEqual(event.event_booth_count, 4)
@@ -83,7 +83,6 @@ class TestEventProductConfiguratorUi(HttpCase):
# Create the event and link it to the product variants as event tickets
cls.event = cls.env['event.event'].create({
'name': 'TestEvent',
'auto_confirm': True,
'date_begin': fields.Datetime.to_string(datetime.today() + timedelta(days=1)),
'date_end': fields.Datetime.to_string(datetime.today() + timedelta(days=15)),
'date_tz': 'Europe/Brussels',
+12 -3
View File
@@ -185,6 +185,7 @@ class WebsiteEventController(http.Controller):
'range': range,
'google_url': lazy(lambda: urls.get('google_url')),
'iCal_url': lazy(lambda: urls.get('iCal_url')),
'registration_error_code': post.get('registration_error_code'),
}
def _process_tickets_form(self, event, form_details):
@@ -240,7 +241,7 @@ class WebsiteEventController(http.Controller):
visitor = request.env['website.visitor']._get_visitor_from_request()
if visitor.email:
default_first_attendee = {
"name": visitor.name,
"name": visitor.display_name,
"email": visitor.email,
"phone": visitor.mobile,
}
@@ -352,8 +353,16 @@ class WebsiteEventController(http.Controller):
@http.route(['''/event/<model("event.event"):event>/registration/confirm'''], type='http', auth="public", methods=['POST'], website=True)
def registration_confirm(self, event, **post):
registrations = self._process_attendees_form(event, post)
attendees_sudo = self._create_attendees_from_registration_post(event, registrations)
""" Check before creating and finalize the creation of the registrations
that we have enough seats for all selected tickets.
If we don't, the user is instead redirected to page to register with a
formatted error message. """
registrations_data = self._process_attendees_form(event, post)
event_ticket_ids = {registration['event_ticket_id'] for registration in registrations_data}
event_tickets = request.env['event.event.ticket'].browse(event_ticket_ids)
if any(event_ticket.seats_limited and event_ticket.seats_available < len(registrations_data) for event_ticket in event_tickets):
return request.redirect('/event/%s/register?registration_error_code=insufficient_seats' % event.id)
attendees_sudo = self._create_attendees_from_registration_post(event, registrations_data)
return request.redirect(('/event/%s/registration/success?' % event.id) + werkzeug.urls.url_encode({'registration_ids': ",".join([str(id) for id in attendees_sudo.ids])}))
+7 -9
View File
@@ -105,6 +105,8 @@ class Event(models.Model):
"""Heuristic
* public, no visitor: not participating as we have no information;
* check only confirmed and attended registrations, a draft registration
does not make the attendee participating;
* public and visitor: check visitor is linked to a registration. As
visitors are merged on the top parent, current visitor check is
sufficient even for successive visits;
@@ -114,14 +116,13 @@ class Event(models.Model):
registration;
"""
current_visitor = self.env['website.visitor']._get_visitor_from_request(force_create=False)
base_domain = [('event_id', 'in', self.ids), ('state', 'in', ['open', 'done'])]
if self.env.user._is_public() and not current_visitor:
events = self.env['event.event']
elif self.env.user._is_public():
events = self.env['event.registration'].sudo().search([
('event_id', 'in', self.ids),
('state', '!=', 'cancel'),
('visitor_id', '=', current_visitor.id),
]).event_id
events = self.env['event.registration'].sudo().search(
expression.AND([base_domain, [('visitor_id', '=', current_visitor.id)]])
).event_id
else:
if current_visitor:
domain = [
@@ -132,10 +133,7 @@ class Event(models.Model):
else:
domain = [('partner_id', '=', self.env.user.partner_id.id)]
events = self.env['event.registration'].sudo().search(
expression.AND([
domain,
['&', ('event_id', 'in', self.ids), ('state', '!=', 'cancel')]
])
expression.AND([base_domain, domain])
).event_id
for event in self:
-2
View File
@@ -69,7 +69,6 @@ class TestEventOnlineCommon(OnlineEventCase):
# event if 8-18 in Europe/Brussels (DST) (first day: begins at 9, last day: ends at 15)
cls.event_0 = cls.env['event.event'].create({
'name': 'TestEvent',
'auto_confirm': True,
'date_begin': datetime.combine(cls.reference_now, time(7, 0)) - timedelta(days=1),
'date_end': datetime.combine(cls.reference_now, time(13, 0)) + timedelta(days=1),
'date_tz': 'Europe/Brussels',
@@ -84,7 +83,6 @@ class TestEventQuestionCommon(EventCase):
cls.event_type_questions = cls.env['event.type'].create({
'name': 'Update Type',
'auto_confirm': True,
'has_seats_limitation': True,
'seats_max': 30,
'default_timezone': 'Europe/Paris',
@@ -37,7 +37,6 @@ class TestEventVisitor(TestEventOnlineCommon, WebsiteVisitorTests):
event_1 = self.env['event.event'].create({
'name': 'OtherEvent',
'auto_confirm': True,
'date_begin': fields.Datetime.to_string(datetime.today() + timedelta(days=1)),
'date_end': fields.Datetime.to_string(datetime.today() + timedelta(days=15)),
})
@@ -148,6 +148,11 @@
itemscope="itemscope" itemprop="offers" itemtype="http://schema.org/AggregateOffer">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div id="o_wevent_tickets" class="o_wevent_theme_bg_base shadow-sm o_wevent_js_ticket_details" data-folded-by-default="0">
<div t-if="registration_error_code == 'insufficient_seats'" class="alert alert-danger" role="alert">
<p class="mb-0">
Registration failed! These tickets are not available anymore.
</p>
</div>
<t t-set="tickets" t-value="event.event_ticket_ids.filtered(lambda ticket: not ticket.is_expired)"/>
<!-- If some tickets expired and there is only one type left, we keep the same layout -->
<t t-if="len(event.event_ticket_ids) &gt; 1">
@@ -15,7 +15,6 @@ class TestWebsiteEventMeet(EventCase):
super(TestWebsiteEventMeet, cls).setUpClass()
cls.event_0 = cls.env['event.event'].create({
'name': 'TestEvent',
'auto_confirm': True,
'date_begin': fields.Datetime.to_string(datetime.today() + timedelta(days=1)),
'date_end': fields.Datetime.to_string(datetime.today() + timedelta(days=15)),
'date_tz': 'Europe/Brussels',
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import _
from odoo.http import request, route
from odoo.addons.website_event.controllers.main import WebsiteEventController
@@ -61,7 +61,7 @@
<span t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'format': 'LLL'}" class="o_wevent_event_month" />
<span t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'format': 'dd'}" class="o_wevent_event_day oe_hide_on_date_edit" />
</div>
<small class="o_wevent_participating text-bg-success">
<small t-if="event.is_participating" class="o_wevent_participating text-bg-success">
<i class="fa fa-check me-2" />
Registered
</small>