[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:
@@ -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"/>
|
||||
|
||||
@@ -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')]]"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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': [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,6 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import account_move
|
||||
from . import event_event
|
||||
from . import event_registration
|
||||
from . import event_ticket
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
-1
@@ -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',
|
||||
|
||||
@@ -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])}))
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) > 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>
|
||||
|
||||
Reference in New Issue
Block a user