19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_event_flow
from . import test_event_internals
from . import test_event_mail_schedule
from . import test_event_slot
from . import test_mailing

View file

@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from contextlib import contextmanager
from freezegun import freeze_time
from unittest.mock import patch
from odoo import fields
from odoo import Command, fields
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.tests import common
@ -10,7 +11,7 @@ class EventCase(common.TransactionCase):
@classmethod
def setUpClass(cls):
super(EventCase, cls).setUpClass()
super().setUpClass()
cls.admin_user = cls.env.ref('base.user_admin')
cls.admin_user.write({
@ -22,6 +23,7 @@ class EventCase(common.TransactionCase):
# set country in order to format Belgian numbers
cls.company_admin.write({
'country_id': cls.env.ref('base.be').id,
'email': 'noreply@company.com',
})
# Test users to use through the various tests
@ -35,6 +37,16 @@ class EventCase(common.TransactionCase):
notification_type='email',
tz='Europe/Brussels',
)
cls.user_public = mail_new_test_user(
cls.env,
company_id=cls.company_admin.id,
email='paulette.public@test.example.com',
groups='base.group_public',
login='public_test',
name='Paulette Public',
notification_type='email',
tz='Europe/Brussels',
)
cls.user_employee = mail_new_test_user(
cls.env,
company_id=cls.company_admin.id,
@ -76,21 +88,62 @@ class EventCase(common.TransactionCase):
tz='Europe/Brussels',
)
cls.event_organizer = cls.env['res.partner'].create({
'city': 'Bruxelles',
'country_id': cls.env.ref('base.be').id,
'email': 'organizer@example.com',
'name': 'Organizer',
'phone': '+32455123456',
'street': 'Organizer Street',
})
cls.event_customer = cls.env['res.partner'].create({
'name': 'Constantin Customer',
'email': 'constantin@test.example.com',
'country_id': cls.env.ref('base.be').id,
'phone': '0485112233',
'mobile': False,
})
cls.event_customer2 = cls.env['res.partner'].create({
'name': 'Constantin Customer 2',
'email': 'constantin2@test.example.com',
'country_id': cls.env.ref('base.be').id,
'phone': '0456987654',
'mobile': '0456654321',
})
cls.reference_now = fields.Datetime.from_string('2022-09-05 15:11:34')
cls.event_type_questions = cls.env['event.type'].create({
'name': 'Update Type',
'has_seats_limitation': True,
'seats_max': 30,
'default_timezone': 'Europe/Paris',
'event_type_ticket_ids': [],
'event_type_mail_ids': [],
})
cls.event_question_1 = cls.env['event.question'].create({
'title': 'Question1',
'question_type': 'simple_choice',
'event_type_ids': [Command.set(cls.event_type_questions.ids)],
'once_per_order': False,
'answer_ids': [
(0, 0, {'name': 'Q1-Answer1'}),
(0, 0, {'name': 'Q1-Answer2'})
],
})
cls.event_question_2 = cls.env['event.question'].create({
'title': 'Question2',
'question_type': 'simple_choice',
'event_type_ids': [Command.set(cls.event_type_questions.ids)],
'once_per_order': True,
'answer_ids': [
(0, 0, {'name': 'Q2-Answer1'}),
(0, 0, {'name': 'Q2-Answer2'})
],
})
cls.event_question_3 = cls.env['event.question'].create({
'title': 'Question3',
'question_type': 'text_box',
'event_type_ids': [Command.set(cls.event_type_questions.ids)],
'once_per_order': True,
})
@classmethod
def _create_registrations(cls, event, reg_count):
@ -99,8 +152,90 @@ class EventCase(common.TransactionCase):
registrations = cls.env['event.registration'].create([{
'create_date': create_date,
'event_id': event.id,
'name': 'Test Registration %s' % x,
'email': '_test_reg_%s@example.com' % x,
'phone': '04560000%s%s' % (x, x),
} for x in range(0, reg_count)])
'name': f'Test Registration {idx}',
'email': f'_test_reg_{idx}@example.com',
'phone': f'04560000{idx}{idx}',
} for idx in range(0, reg_count)])
return registrations
@classmethod
def _create_registrations_for_slot_and_ticket(cls, event, slot, ticket, count, **add_values):
return cls.env['event.registration'].create([
dict(
{
'email': f'{slot.id if slot else "NoSlot"}.{ticket.id if ticket else "NoTicket"}@test.example.com',
'event_id': event.id,
'event_slot_id': slot.id if slot else False,
'event_ticket_id': ticket.id if ticket else False,
'name': f'{slot.id if slot else "NoSlot"}.{ticket.id if ticket else "NoTicket"}',
}, **add_values
) for idx in range(0, count)
])
@classmethod
def _setup_test_reports(cls):
cls.test_report_view = cls.env["ir.ui.view"].create({
"arch_db": """
<t t-call="web.html_container">
<t t-foreach="docs" t-as="registration">
<t t-call="web.external_layout">
<div class="page">
<p>This is a sample of an external report.</p>
</div>
</t>
</t>
</t>""",
"key": "event_registration_test_report",
"name": "event_registration_test_report",
"type": "qweb",
})
cls.env["ir.model.data"].create({
"model": "ir.ui.view",
"module": "event",
"name": "event_registration_test_report",
"res_id": cls.test_report_view.id,
})
cls.test_report_action = cls.env['ir.actions.report'].create({
'name': 'Test Report on event.registration',
'model': 'event.registration',
'print_report_name': "f'TestReport for {object.name}'",
'report_type': 'qweb-pdf',
'report_name': 'event.event_registration_test_report',
})
cls.template_subscription = cls.env['mail.template'].create({
"body_html": """<div>Hello your registration to <t t-out="object.event_id.name"/> is confirmed.</div>""",
"email_from": "{{ (object.event_id.organizer_id.email_formatted or object.event_id.user_id.email_formatted or '') }}",
"lang": "{{ object.event_id.lang or object.partner_id.lang }}",
"model_id": cls.env['ir.model']._get_id("event.registration"),
"name": "Event: Registration Confirmation TEST",
"subject": "Confirmation for {{ object.event_id.name }}",
"report_template_ids": [(4, cls.test_report_action.id)],
"use_default_to": True,
})
cls.template_reminder = cls.env['mail.template'].create({
"body_html": """<div>Hello this is a reminder for your registration to <t t-out="object.event_id.name"/>.</div>""",
"email_from": "{{ (object.event_id.organizer_id.email_formatted or object.event_id.user_id.email_formatted or '') }}",
"lang": "{{ object.event_id.lang or object.partner_id.lang }}",
"model_id": cls.env['ir.model']._get_id("event.registration"),
"name": "Event: Registration Reminder TEST",
"subject": "Reminder for {{ object.event_id.name }}: {{ object.event_date_range }}",
"report_template_ids": [(4, cls.test_report_action.id)],
"use_default_to": True,
})
def assertSchedulerCronTriggers(self, capture, call_at_list):
self.assertEqual(len(capture.records), len(call_at_list))
for record, call_at in zip(capture.records, call_at_list):
self.assertEqual(record.call_at, call_at.replace(microsecond=0))
self.assertEqual(record.cron_id, self.env.ref('event.event_mail_scheduler'))
@contextmanager
def mock_datetime_and_now(self, mock_dt):
""" Used when synchronization date (using env.cr.now()) is important
in addition to standard datetime mocks. Used mainly to detect sync
issues. """
with freeze_time(mock_dt), \
patch.object(self.env.cr, 'now', lambda: mock_dt):
yield

View file

@ -1,143 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
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 """
# Should apply default datetimes
with freeze_time(self.reference_now):
default_event = self.env['event.event'].create({
'name': 'Test Default Event',
})
self.assertEqual(default_event.date_begin, datetime.datetime.strptime('2022-09-05 15:30:00', '%Y-%m-%d %H:%M:%S'))
self.assertEqual(default_event.date_end, datetime.datetime.strptime('2022-09-06 15:30:00', '%Y-%m-%d %H:%M:%S'))
specific_datetimes = {
'date_begin': self.reference_now + relativedelta(days=1),
'date_end': self.reference_now + relativedelta(days=3),
}
# Should not apply default datetimes if values are set manually
with freeze_time(self.reference_now):
event = self.env['event.event'].create({
'name': 'Test Event',
**specific_datetimes,
})
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 """
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_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),
('interval_type', '=', 'after_sub')
])
# Imagine user deletes mail template for whatever reason
scheduler.template_ref.unlink()
# Mails should not be sent
test_reg.action_confirm()
self.assertFalse(scheduler.mail_registration_ids.mail_sent)

View file

@ -1,14 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from datetime import date, datetime, timedelta
from freezegun import freeze_time
from odoo import Command
from odoo.addons.event.tests.common import EventCase
from odoo import exceptions
from odoo.fields import Datetime as FieldsDatetime
from odoo.tests.common import users, Form, tagged
from odoo.tests import Form, users, tagged
from odoo.tools import mute_logger
@ -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',
@ -59,73 +57,53 @@ class TestEventInternalsCommon(EventCase):
class TestEventData(TestEventInternalsCommon):
@users('user_eventmanager')
def test_event_date_computation(self):
event = self.event_0.with_user(self.env.user)
with freeze_time(self.reference_now):
event.write({
'registration_ids': [(0, 0, {'partner_id': self.event_customer.id, 'name': 'test_reg'})],
'date_begin': datetime(2020, 1, 31, 15, 0, 0),
'date_end': datetime(2020, 4, 5, 18, 0, 0),
})
registration = event.registration_ids[0]
self.assertEqual(registration.get_date_range_str(), u'today')
def test_event_configuration_question_from_type(self):
""" Enure configuration & translations are copied from Event Type on Event creation """
self.env['res.lang'].sudo()._activate_lang('nl_NL')
event.date_begin = datetime(2020, 2, 1, 15, 0, 0)
self.assertEqual(registration.get_date_range_str(), u'tomorrow')
event_type = self.event_type_questions.with_user(self.env.user)
event_type_question_nl = self.event_question_1.with_context(lang='nl_NL')
event_type_question_nl.title = "Vraag1"
event_type_question_nl.answer_ids[0].name = "V1-Antwoord1"
event.date_begin = datetime(2020, 2, 2, 6, 0, 0)
self.assertEqual(registration.get_date_range_str(), u'in 2 days')
event.date_begin = datetime(2020, 2, 20, 17, 0, 0)
self.assertEqual(registration.get_date_range_str(), u'next month')
event.date_begin = datetime(2020, 3, 1, 10, 0, 0)
self.assertEqual(registration.get_date_range_str(), u'on Mar 1, 2020')
# Is actually 8:30 to 20:00 in Mexico
event.write({
'date_begin': datetime(2020, 1, 31, 14, 30, 0),
'date_end': datetime(2020, 2, 1, 2, 0, 0),
'date_tz': 'America/Mexico_City'
})
self.assertTrue(event.is_one_day)
# Checks case when mocked today changes date before event, when event.date_tz considered
with freeze_time(datetime(2020, 6, 20, 20, 0, 0)):
event.write({
'date_begin': datetime(2020, 6, 27, 1, 0, 0),
'date_end': datetime(2020, 7, 8, 2, 0, 0),
'date_tz': 'America/Los_Angeles'
})
# event_date_tz = 2020-06-26 18:00
# today_tz = 2020-06-20 13:00
# event_date_tz.date() - today_tz.date() = 6 days
self.assertEqual(registration.get_date_range_str(), 'in 6 days')
# Checks case when event changes date before mocked today, when event.date_tz considered
with freeze_time(datetime(2020, 6, 20, 13, 0, 0)):
event.write({
'date_begin': datetime(2020, 6, 25, 20, 0, 0),
'date_end': datetime(2020, 7, 8, 2, 0, 0),
'date_tz': 'Australia/Sydney'
})
# event_date_tz = 2020-06-26 06:00
# today_tz = 2020-06-20 23:00
# event_date_tz.date() - today_tz.date() = 6 days
self.assertEqual(registration.get_date_range_str(), 'in 6 days')
@freeze_time('2020-1-31 10:00:00')
@users('user_eventmanager')
def test_event_date_timezone(self):
event = self.event_0.with_user(self.env.user)
# Is actually 8:30 to 20:00 in Mexico
event.write({
'date_begin': datetime(2020, 1, 31, 14, 30, 0),
'date_end': datetime(2020, 2, 1, 2, 0, 0),
'date_tz': 'America/Mexico_City'
event = self.env['event.event'].create({
'name': 'Event Update Type',
'event_type_id': event_type.id,
'date_begin': FieldsDatetime.to_string(datetime.today() + timedelta(days=1)),
'date_end': FieldsDatetime.to_string(datetime.today() + timedelta(days=15)),
})
self.assertTrue(event.is_one_day)
self.assertFalse(event.is_ongoing)
event.invalidate_recordset(['specific_question_ids', 'general_question_ids'])
self.assertEqual(
sorted(event.question_ids.mapped('question_type')),
['email', 'name', 'phone', 'simple_choice', 'simple_choice', 'text_box'])
self.assertEqual(event.specific_question_ids.filtered(
lambda q: q.question_type in ['simple_choice', 'text_box']).title, 'Question1')
self.assertEqual(event.specific_question_ids.filtered(
lambda q: q.question_type in ['name', 'email', 'phone', 'company_name'])
.mapped('title'), ['Name', 'Email', 'Phone'])
self.assertEqual(
set(event.specific_question_ids.filtered(
lambda q: q.question_type in ['simple_choice', 'text_box']).mapped('answer_ids.name')),
{'Q1-Answer1', 'Q1-Answer2'})
self.assertEqual(len(event.general_question_ids), 2)
self.assertEqual(event.general_question_ids[0].title, 'Question2')
self.assertEqual(event.general_question_ids[1].title, 'Question3')
self.assertEqual(
set(event.general_question_ids[0].mapped('answer_ids.name')),
{'Q2-Answer1', 'Q2-Answer2'})
# verify translations
event_question_nl = event.specific_question_ids.filtered_domain([
('title', '=', self.event_question_1.title),
]).with_context(lang='nl_NL')
self.assertNotEqual(event_question_nl.title, self.event_question_1.title,
"Translated title should differ from untranslated title.")
self.assertEqual(event_question_nl.title, event_type_question_nl.title,
"Translated title should be copied.")
self.assertEqual(
set(event_question_nl.answer_ids.mapped('name')),
set(event_type_question_nl.answer_ids.mapped('name')),
"Translated answer names should be copied.")
@users('user_eventmanager')
@mute_logger('odoo.models.unlink')
@ -147,12 +125,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
@ -170,7 +146,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)
@ -206,16 +181,13 @@ 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({
'notification_type': 'mail',
'interval_nbr': 77,
'interval_unit': 'days',
'interval_type': 'after_event',
@ -233,7 +205,6 @@ class TestEventData(TestEventInternalsCommon):
'event_mail_ids': [
Command.clear(),
Command.create({
'notification_type': 'mail',
'interval_unit': 'now',
'interval_type': 'after_sub',
'template_ref': 'mail.template,%i' % self.env['ir.model.data']._xmlid_to_res_id('event.event_subscription'),
@ -242,7 +213,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')),
@ -297,6 +268,53 @@ class TestEventData(TestEventInternalsCommon):
event.write({'event_type_id': event_type.id})
self.assertEqual(event.note, '<p>Event Type Note</p>')
@users('user_eventmanager')
def test_event_configuration_questions_from_type(self):
""" Test that the questions of an event are updated as the event type changes. """
event_type_1_question, event_type_1_removed_question, event_type_2_question, event_type_common_question = self.env['event.question'].create([{
'title': 'Event Type 1 Question'
}, {
'title': 'Event Type 1 Removed Question'
}, {
'title': 'Event Type 2 Question'
}, {
# To check that a question removed from an event can be added again using an event_type.
'title': 'Event Type Common Question'
}])
event_type_1_questions = event_type_1_question + event_type_1_removed_question + event_type_common_question
event_type_1, event_type_2 = self.env['event.type'].create([{
'name': 'Event Type 1',
'question_ids': [Command.set(event_type_1_questions.ids)]
}, {
'name': 'Event Type 2',
'question_ids': [Command.set((event_type_2_question + event_type_common_question).ids)]
}])
event = self.env['event.event'].create({
'name': 'Event',
'event_type_id': event_type_1.id,
'date_begin': self.reference_beg,
'date_end': self.reference_end,
})
# Check that the questions of the event are updated with those of the event type.
self.assertEqual(event.question_ids, event_type_1.question_ids)
event_type_1.question_ids = [Command.clear()]
# Check that the questions of the event are not updated when the questions of the event type are removed.
self.assertTrue(event.question_ids, event_type_1_questions)
self.env['event.registration.answer'].create({
'question_id': event_type_1_question.id,
'registration_id': self.env['event.registration'].create({'event_id': event.id}).id,
'value_text_box': 'Value Registration Answer',
})
event.write({'event_type_id': event_type_2.id})
# Check that the questions of the event are updated with those of the new event type of the event
# and that the question with attendee answer is not removed.
self.assertEqual(
event.question_ids,
event_type_1_question + event_type_2_question + event_type_common_question
)
@users('user_eventmanager')
def test_event_configuration_tickets_from_type(self):
""" Test data computation (related to tickets) of event coming from its event.type template.
@ -308,11 +326,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': [
@ -364,6 +380,90 @@ class TestEventData(TestEventInternalsCommon):
set(['Registration Ticket'])
)
@users('user_eventmanager')
def test_event_date_computation(self):
event = self.event_0.with_user(self.env.user)
with freeze_time(self.reference_now):
event.write({
'registration_ids': [(0, 0, {'partner_id': self.event_customer.id, 'name': 'test_reg'})],
'date_begin': datetime(2020, 1, 31, 15, 0, 0),
'date_end': datetime(2020, 4, 5, 18, 0, 0),
})
registration = event.registration_ids[0]
self.assertEqual(registration.event_date_range, 'today')
event.date_begin = datetime(2020, 2, 1, 15, 0, 0)
registration.invalidate_recordset(['event_date_range'])
self.assertEqual(registration.event_date_range, 'tomorrow')
event.date_begin = datetime(2020, 2, 2, 6, 0, 0)
registration.invalidate_recordset(['event_date_range'])
self.assertEqual(registration.event_date_range, 'in 2 days')
event.date_begin = datetime(2020, 2, 20, 17, 0, 0)
registration.invalidate_recordset(['event_date_range'])
self.assertEqual(registration.event_date_range, 'next month')
event.date_begin = datetime(2020, 3, 1, 10, 0, 0)
registration.invalidate_recordset(['event_date_range'])
self.assertEqual(registration.event_date_range, 'on Mar 1, 2020')
# Is actually 8:30 to 20:00 in Mexico
event.write({
'date_begin': datetime(2020, 1, 31, 14, 30, 0),
'date_end': datetime(2020, 2, 1, 2, 0, 0),
'date_tz': 'America/Mexico_City'
})
self.assertTrue(event.is_one_day)
# Checks case when mocked today changes date before event, when event.date_tz considered
with freeze_time(datetime(2020, 6, 20, 20, 0, 0)):
event.write({
'date_begin': datetime(2020, 6, 27, 1, 0, 0),
'date_end': datetime(2020, 7, 8, 2, 0, 0),
'date_tz': 'America/Los_Angeles'
})
# event_date_tz = 2020-06-26 18:00
# today_tz = 2020-06-20 13:00
# event_date_tz.date() - today_tz.date() = 6 days
registration.invalidate_recordset(['event_date_range'])
self.assertEqual(registration.event_date_range, 'in 6 days')
# Checks case when event changes date before mocked today, when event.date_tz considered
with freeze_time(datetime(2020, 6, 20, 13, 0, 0)):
event.write({
'date_begin': datetime(2020, 6, 25, 20, 0, 0),
'date_end': datetime(2020, 7, 8, 2, 0, 0),
'date_tz': 'Australia/Sydney'
})
# event_date_tz = 2020-06-26 06:00
# today_tz = 2020-06-20 23:00
# event_date_tz.date() - today_tz.date() = 6 days
registration.invalidate_recordset(['event_date_range'])
self.assertEqual(registration.event_date_range, 'in 6 days')
@freeze_time('2020-01-31 10:00:00')
@users('user_eventmanager')
def test_event_date_timezone(self):
event = self.event_0.with_user(self.env.user)
# Is actually 8:30 to 20:00 in Mexico
event.write({
'date_begin': datetime(2020, 1, 31, 14, 30, 0),
'date_end': datetime(2020, 2, 1, 2, 0, 0),
'date_tz': 'America/Mexico_City'
})
self.assertTrue(event.is_one_day)
self.assertFalse(event.is_ongoing)
# Should apply default datetimes
with freeze_time(self.reference_now):
default_event = self.env['event.event'].create({
'name': 'Test Default Event',
})
self.assertEqual(default_event.date_begin, self.reference_now)
self.assertEqual(default_event.date_end, self.reference_now + timedelta(days=1))
self.assertEqual(default_event.date_tz, self.user_eventmanager.tz)
@users('user_eventmanager')
def test_event_mail_default_config(self):
event = self.env['event.event'].create({
@ -373,7 +473,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)
@ -405,8 +504,27 @@ class TestEventData(TestEventInternalsCommon):
self.env['mail.template'].create({'model_id': self.env['ir.model']._get('res.partner').id, 'name': 'test template'})
templates = self.env['mail.template'].with_context(filter_template_on_event=True).name_search('test template')
self.assertEqual(len(templates), 1, 'Should return only mail templates related to the event registration model')
templates = self.env['mail.template'].with_context(filter_template_on_event=True).search([('name', '=', 'test template')])
self.assertEqual(len(templates), 1, 'Should also return only mail templates related to the event registration model using search')
@freeze_time('2020-1-31 10:00:00')
@users('user_eventmanager')
def test_event_question_defaults(self):
""" Test that default questions are linked to the new events and shared by all of them. """
event_0, event_1 = self.env['event.event'].create([{
'name': 'TestEvent 0',
'date_begin': self.reference_beg,
'date_end': self.reference_end,
}, {
'name': 'TestEvent 1',
'date_begin': self.reference_beg,
'date_end': self.reference_end,
}])
# Check that event has been linked to the default questions.
self.assertCountEqual(event_0.question_ids.mapped('question_type'), ['name', 'email', 'phone'])
# Check that default questions are shared by events.
self.assertEqual(event_0.question_ids, event_1.question_ids)
@freeze_time('2020-01-31 10:00:00')
@users('user_eventmanager')
def test_event_registrable(self):
"""Test if `_compute_event_registrations_open` works properly."""
@ -440,7 +558,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,
@ -459,7 +576,78 @@ class TestEventData(TestEventInternalsCommon):
self.assertTrue(ticket.is_expired)
self.assertFalse(event.event_registrations_open)
@freeze_time('2020-1-31 10:00:00')
@freeze_time('2020-01-31 10:00:00')
@users('user_eventmanager')
def test_event_multi_slots_registrable(self):
"""Test if `_compute_event_registrations_open` works properly on multi slots events. """
event = self.event_0.with_user(self.env.user)
self.assertTrue(event.event_registrations_open)
event.write({
'date_begin': datetime(2020, 1, 30, 8, 0, 0),
'date_end': datetime(2020, 2, 4, 8, 0, 0),
'is_multi_slots': True,
})
self.assertFalse(event.event_ticket_ids)
self.assertFalse(event.event_slot_ids)
# Should be closed if no slot
self.assertFalse(event.event_registrations_open)
# Should be open with a slot and no tickets
event.write({
'event_slot_ids': [
(0, 0, {
'date': date(2020, 1, 30),
'start_hour': 9,
'end_hour': 12,
}),
(0, 0, {
'date': date(2020, 1, 31),
'start_hour': 14,
'end_hour': 16,
}),
]
})
self.assertTrue(event.event_registrations_open)
# Should be open with a slot, a ticket and slot-ticket availabilities
event.write({
'event_ticket_ids': [
(0, 0, {
'name': 'Better',
'seats_limited': True,
'seats_max': 2,
}),
]
})
self.assertTrue(event.event_registrations_open)
# Should be closed if all slots are sold out (event seats max)
event.write({
'seats_limited': True,
'seats_max': 1,
})
slot1 = event.event_slot_ids[0]
slot2 = event.event_slot_ids[1]
self.assertEqual(slot1.seats_available, 1)
self.assertEqual(slot2.seats_available, 1)
regs = self.env['event.registration'].create([{
'event_id': event.id,
'name': 'reg_open',
'event_slot_id': slot.id,
} for slot in slot1 + slot2])
self.assertTrue(slot1.is_sold_out)
self.assertTrue(slot2.is_sold_out)
self.assertFalse(event.event_registrations_open)
regs.unlink()
# Should be closed if ticket sold out for each slot (ticket seats max)
event.write({'seats_limited': False})
self.assertTrue(event.event_registrations_open)
regs = self.env['event.registration'].create([{
'event_id': event.id,
'name': 'reg_open',
'event_slot_id': slot.id,
'event_ticket_id': event.event_ticket_ids.id,
} for slot in slot1 + slot2 for _ in range(2)])
self.assertFalse(event.event_registrations_open)
@freeze_time('2020-01-31 10:00:00')
@users('user_eventmanager')
def test_event_ongoing(self):
event_1 = self.env['event.event'].create({
@ -468,13 +656,13 @@ class TestEventData(TestEventInternalsCommon):
'date_end': datetime(2020, 2, 1, 18, 0, 0),
})
self.assertTrue(event_1.is_ongoing)
ongoing_event_ids = self.env['event.event']._search([('is_ongoing', '=', True)])
self.assertIn(event_1.id, ongoing_event_ids)
ongoing_events = self.env['event.event'].search([('is_ongoing', '=', True)])
self.assertIn(event_1, ongoing_events)
event_1.update({'date_begin': datetime(2020, 2, 1, 9, 0, 0)})
self.assertFalse(event_1.is_ongoing)
ongoing_event_ids = self.env['event.event']._search([('is_ongoing', '=', True)])
self.assertNotIn(event_1.id, ongoing_event_ids)
ongoing_events = self.env['event.event'].search([('is_ongoing', '=', True)])
self.assertNotIn(event_1, ongoing_events)
event_2 = self.env['event.event'].create({
'name': 'Test Event 2',
@ -482,13 +670,13 @@ class TestEventData(TestEventInternalsCommon):
'date_end': datetime(2020, 1, 28, 8, 0, 0),
})
self.assertFalse(event_2.is_ongoing)
finished_or_upcoming_event_ids = self.env['event.event']._search([('is_ongoing', '=', False)])
self.assertIn(event_2.id, finished_or_upcoming_event_ids)
finished_or_upcoming_events = self.env['event.event'].search([('is_ongoing', '=', False)])
self.assertIn(event_2, finished_or_upcoming_events)
event_2.update({'date_end': datetime(2020, 2, 2, 8, 0, 1)})
self.assertTrue(event_2.is_ongoing)
finished_or_upcoming_event_ids = self.env['event.event']._search([('is_ongoing', '=', False)])
self.assertNotIn(event_2.id, finished_or_upcoming_event_ids)
finished_or_upcoming_events = self.env['event.event'].search([('is_ongoing', '=', False)])
self.assertNotIn(event_2, finished_or_upcoming_events)
@users('user_eventmanager')
def test_event_seats(self):
@ -504,13 +692,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',
@ -529,10 +715,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
@ -542,35 +727,27 @@ 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)
# It is not possible to set a seats_max value below number of current
# confirmed registrations. (4 "reserved" + 1 "used")
with self.assertRaises(exceptions.ValidationError):
event.write({'seats_max': 4})
event.write({'seats_max': 5})
self.assertEqual(event.seats_available, 0)
# It is not possible to unarchive a confirmed seat if the event is fully booked
event.write({'seats_max': 5})
with self.assertRaises(exceptions.ValidationError):
reg_open.action_unarchive()
@ -584,23 +761,59 @@ 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')
class TestEventRegistrationData(TestEventInternalsCommon):
@users('user_eventmanager')
def test_registration_attended_log(self):
"""Test changes in date_closed field when state is changed."""
with self.mock_datetime_and_now('2025-05-03 17:00:00'):
event = self.env['event.event'].create({
'name': 'Test Event',
'date_begin': FieldsDatetime.to_string(datetime.now()),
'date_end': FieldsDatetime.to_string(datetime.now() + timedelta(days=2)),
})
attendee = self.env['event.registration'].create({
'name': 'Test Registration',
'event_id': event.id,
'state': 'done',
})
self.assertEqual(attendee.date_closed, datetime.now())
attendee.action_set_done()
message = '<p>Attended on 5/3/25</p>'
self.assertTrue(message in attendee.message_ids.mapped('body'),
'Expected a "Attended on 5/3/25" message in the chatter.')
self.assertEqual(attendee.message_ids.mapped('body').count(message), 1,
'Logged message when marked as attended.')
attendee.action_set_done()
self.assertEqual(attendee.message_ids.mapped('body').count(message), 2,
'Logged message when marked as attended again.')
with self.mock_datetime_and_now('2025-05-04 17:00:00'):
attendee.action_set_done()
new_message = '<p>Attended on 5/4/25</p>'
self.assertTrue(new_message in attendee.message_ids.mapped('body'),
'Expected a "Attended on 5/4/25" message in the chatter.')
self.assertEqual(attendee.message_ids.mapped('body').count(new_message), 1,
'Logged a new message when marked as attended on a different day.')
@users('user_eventmanager')
def test_registration_partner_sync(self):
""" Test registration computed fields about partner """
@ -721,9 +934,7 @@ class TestEventRegistrationPhone(EventCase):
customer2 = self.event_customer2.with_env(self.env)
event = self.test_event.with_env(self.env)
self.assertFalse(customer.mobile)
self.assertEqual(customer.phone, '0485112233')
self.assertEqual(customer2.mobile, '0456654321')
self.assertEqual(customer2.phone, '0456987654')
self.assertEqual(event.company_id.country_id, self.env.ref("base.be"))
@ -734,12 +945,10 @@ class TestEventRegistrationPhone(EventCase):
""" Test onchange on phone / mobile, should try to format number """
event = self.test_event.with_user(self.env.user)
lead_form = Form(self.env['event.registration'])
lead_form.event_id = event
lead_form.mobile = '7200000011'
lead_form.phone = '7200000000'
self.assertEqual(lead_form.mobile, '+917200000011')
self.assertEqual(lead_form.phone, '+917200000000')
reg_form = Form(self.env['event.registration'])
reg_form.event_id = event
reg_form.phone = '7200000000'
self.assertEqual(reg_form.phone, '+917200000000')
@users('user_eventregistrationdesk')
def test_registration_phone_format(self):
@ -747,73 +956,61 @@ class TestEventRegistrationPhone(EventCase):
(IN numbers) or company (BE numbers). """
event = self.test_event.with_user(self.env.user)
# customer_id, mobile, phone -> based on partner or event country
# customer_id, phone -> based on partner or event country
sources = [
(self.event_customer.id, None, None), # BE local on partner
(self.event_customer2.id, None, None), # BE local on partner
(self.event_customer2.id, '0456001122', None), # BE local + on partner
(False, '0456778899', '+32456778899'), # BE local + BE global
(False, '7200000000', False), # IN local
(False, False, '7200000011'), # IN local
(False, '7200000000', '7200000011'), # IN local
(False, '+917200000088', '+917200000099'), # IN global
(self.event_customer.id, None), # BE local on partner
(self.event_customer2.id, None), # BE local on partner
(self.event_customer2.id, '0456001122'), # BE local + on partner
(False, '0456778899'), # BE local
(False, '7200000000'), # IN local
(False, '+917200000088'), # IN global
]
# mobile, phone
# expected phone
expected = [
(False, '0485112233'), # partner values, no format
('0456654321', '0456987654'), # partner values, no format
('+32456001122', '0456987654'), # BE on partner / partner value, no format
('0456778899', '+32456778899'), # IN on event -> cannot format BE
('+917200000000', False), # IN on event
(False, '+917200000011'), # IN on event
('+917200000000', '+917200000011'), # IN on event
('+917200000088', '+917200000099'), # already formatted
'0485112233', # partner values, no format (phone only)
'0456987654', # partner values, no format (both: phone wins)
'+32456001122', # BE on partner
'0456778899', # IN on event -> cannot format BE
'+917200000000', # IN on event
'+917200000088', # already formatted
]
for (partner_id, mobile, phone), (exp_mobile, exp_phone) in zip(sources, expected):
with self.subTest(partner_id=partner_id, mobile=mobile, phone=phone):
for (partner_id, phone), exp_phone in zip(sources, expected):
with self.subTest(partner_id=partner_id, phone=phone):
create_vals = {
'event_id': event.id,
'partner_id': partner_id,
}
if mobile is not None:
create_vals['mobile'] = mobile
if phone is not None:
create_vals['phone'] = phone
reg = self.env['event.registration'].create(create_vals)
self.assertEqual(reg.mobile, exp_mobile)
self.assertEqual(reg.phone, exp_phone)
# no country on event -> based on partner or event company country
self.test_event.write({'address_id': False})
expected = [
(False, '0485112233'), # partner values, no format
('0456654321', '0456987654'), # partner values, no format
('+32456001122', '0456987654'), # BE on partner / partner value, no format
('+32456778899', '+32456778899'), # BE on company
('7200000000', False), # BE on company -> cannot format IN
(False, '7200000011'), # BE on company -> cannot format IN
('7200000000', '7200000011'), # BE on company -> cannot format IN
('+917200000088', '+917200000099'), # already formatted
'0485112233', # partner values, no format (phone only)
'0456987654', # partner values, no format (both: phone wins)
'+32456001122', # BE on company
'+32456778899', # BE on company
'7200000000', # BE on company -> cannot format IN
'+917200000088', # already formatted
]
for (partner_id, mobile, phone), (exp_mobile, exp_phone) in zip(sources, expected):
with self.subTest(partner_id=partner_id, mobile=mobile, phone=phone):
for (partner_id, phone), exp_phone in zip(sources, expected):
with self.subTest(partner_id=partner_id, phone=phone):
create_vals = {
'event_id': event.id,
'partner_id': partner_id,
}
if mobile is not None:
create_vals['mobile'] = mobile
if phone is not None:
create_vals['phone'] = phone
reg = self.env['event.registration'].create(create_vals)
self.assertEqual(reg.mobile, exp_mobile)
self.assertEqual(reg.phone, exp_phone)
@tagged('event_ticket')
class TestEventTicketData(TestEventInternalsCommon):
@freeze_time('2020-1-31 10:00:00')
@freeze_time('2020-01-31 10:00:00')
@users('user_eventmanager')
def test_event_ticket_fields(self):
""" Test event ticket fields synchronization """
@ -831,7 +1028,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')
@ -897,23 +1093,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)
@ -926,9 +1122,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)
@ -938,15 +1131,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):
first_ticket.write({'seats_max': 1})
reg_open.action_archive()
first_ticket.write({'seats_max': 1})
@ -956,10 +1140,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,

View file

@ -0,0 +1,299 @@
from datetime import date, datetime, timedelta
from odoo.addons.event.tests.common import EventCase
from odoo import exceptions
from odoo.tests import tagged
class TestEventSlotsCommon(EventCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Mock dates to have reproducible computed fields based on time
cls.reference_now = datetime(2025, 4, 15, 10, 0, 0)
cls.reference_beg = datetime(2025, 4, 21, 6, 30, 0)
cls.reference_end = datetime(2025, 8, 21, 17, 45, 0)
with cls.mock_datetime_and_now(cls, cls.reference_now):
cls.test_event = cls.env['event.event'].create({
'date_begin': cls.reference_beg,
'date_end': cls.reference_end,
'date_tz': 'Europe/Brussels',
'event_ticket_ids': [
(0, 0, {
'name': 'Classic',
'seats_limited': False,
'seats_max': 0,
}), (0, 0, {
'name': 'Better',
'seats_limited': True,
'seats_max': 3,
}), (0, 0, {
'name': 'VIP',
'seats_limited': True,
'seats_max': 1,
}),
],
'name': 'Test Event',
'seats_limited': True,
'seats_max': 5,
'event_slot_ids': [
(0, 0, {
'date': date(2025, 4, 21),
'end_hour': 12,
'start_hour': 9,
}),
(0, 0, {
'date': date(2025, 4, 21),
'end_hour': 16,
'start_hour': 13,
}),
],
'user_id': cls.user_eventuser.id,
})
first_slot = cls.test_event.event_slot_ids.filtered(lambda s: s.start_hour == 9)
second_slot = cls.test_event.event_slot_ids.filtered(lambda s: s.start_hour == 13)
first_ticket = cls.test_event.event_ticket_ids.filtered(lambda t: t.name == 'Classic')
second_ticket = cls.test_event.event_ticket_ids.filtered(lambda t: t.name == 'Better')
# already existing registrations
cls.test_reg_slot_1 = cls._create_registrations_for_slot_and_ticket(cls.test_event, first_slot, first_ticket, 3)
cls.test_reg_slot_2 = cls._create_registrations_for_slot_and_ticket(cls.test_event, second_slot, second_ticket, 1)
@tagged('event_slot', 'event_registration')
class TestEventSlotRegistration(TestEventSlotsCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_event_no_slot = cls.env['event.event'].create({
'date_begin': cls.reference_beg,
'date_end': cls.reference_end,
'date_tz': 'Europe/Brussels',
'name': 'Test Event No Slot',
})
cls.test_reg_no_slot = cls.env["event.registration"].create({
"event_id": cls.test_event_no_slot.id,
"name": "Test Registration No Slot",
})
def test_search_event_begin_date(self):
""" Searching on the registration 'event_begin_date' field should correctly search
on the slot start datetime if the registration is linked to a slot
else on the event start date.
"""
for search_from_date, expected in [
(self.reference_beg, self.test_reg_no_slot + self.test_reg_slot_1 + self.test_reg_slot_2),
(self.reference_beg + timedelta(minutes=30), self.test_reg_slot_1 + self.test_reg_slot_2),
(self.reference_beg + timedelta(hours=1), self.test_reg_slot_2),
]:
self.assertEqual(
self.env["event.registration"].search([
('event_id', 'in', [self.test_event_no_slot.id, self.test_event.id]),
('event_begin_date', '>=', search_from_date),
]),
expected,
)
def test_search_event_end_date(self):
""" Searching on the registration 'event_end_date' field should correctly search
on the slot end datetime if the registration is linked to a slot,
else on the event end date.
"""
for search_to_date, expected in [
(self.reference_end, self.test_reg_no_slot + self.test_reg_slot_1 + self.test_reg_slot_2),
(self.reference_beg + timedelta(hours=12), self.test_reg_slot_1 + self.test_reg_slot_2),
(self.reference_beg + timedelta(hours=6), self.test_reg_slot_1),
]:
self.assertEqual(
self.env["event.registration"].search([
('event_id', 'in', [self.test_event_no_slot.id, self.test_event.id]),
('event_end_date', '<=', search_to_date),
]),
expected,
)
@tagged('event_slot', 'event_seats')
class TestEventSlotSeats(TestEventSlotsCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_event_slot_noticket = cls.test_event.copy({'event_ticket_ids': False})
first_slot = cls.test_event_slot_noticket.event_slot_ids.filtered(lambda s: s.start_hour == 9)
second_slot = cls.test_event_slot_noticket.event_slot_ids.filtered(lambda s: s.start_hour == 13)
# already existing registrations: 3 on first slot (1 archived), 1 on second slot (2 archived)
with cls.mock_datetime_and_now(cls, cls.reference_now):
cls._create_registrations_for_slot_and_ticket(cls.test_event_slot_noticket, first_slot, False, 1, state='open')
cls._create_registrations_for_slot_and_ticket(cls.test_event_slot_noticket, first_slot, False, 2, state='done')
cls._create_registrations_for_slot_and_ticket(cls.test_event_slot_noticket, first_slot, False, 1, active=False)
cls._create_registrations_for_slot_and_ticket(cls.test_event_slot_noticket, second_slot, False, 1)
cls._create_registrations_for_slot_and_ticket(cls.test_event_slot_noticket, second_slot, False, 2, active=False)
cls._create_registrations_for_slot_and_ticket(cls.test_event_slot_noticket, second_slot, False, 1, state='cancel')
cls._create_registrations_for_slot_and_ticket(cls.test_event_slot_noticket, second_slot, False, 1, state='draft')
def test_assert_initial_values(self):
""" Check initial values, ensure test conditions """
test_event = self.test_event.with_user(self.user_eventregistrationdesk)
first_slot = test_event.event_slot_ids.filtered(lambda s: s.start_hour == 9)
second_slot = test_event.event_slot_ids.filtered(lambda s: s.start_hour == 13)
self.assertTrue(first_slot)
self.assertTrue(second_slot)
self.assertEqual(first_slot.seats_available, 2)
self.assertEqual(first_slot.seats_reserved, 3)
self.assertEqual(second_slot.seats_available, 4)
self.assertEqual(second_slot.seats_reserved, 1)
test_event_nt = self.test_event_slot_noticket.with_user(self.user_eventregistrationdesk)
first_slot = test_event_nt.event_slot_ids.filtered(lambda s: s.start_hour == 9)
second_slot = test_event_nt.event_slot_ids.filtered(lambda s: s.start_hour == 13)
self.assertTrue(first_slot)
self.assertTrue(second_slot)
self.assertEqual(first_slot.seats_available, 2)
self.assertEqual(first_slot.seats_reserved, 1)
self.assertEqual(second_slot.seats_available, 4)
self.assertEqual(second_slot.seats_reserved, 1)
def test_seats_slots_notickets(self):
""" Test: slots, no tickets -> limits come from event itself """
# self.test_event_slot_noticket.with_user(self.user_eventuser).write({'event_ticket_ids': [(5, 0)]})
test_event = self.test_event_slot_noticket.with_user(self.user_eventregistrationdesk)
first_slot = test_event.event_slot_ids.filtered(lambda s: s.start_hour == 9)
second_slot = test_event.event_slot_ids.filtered(lambda s: s.start_hour == 13)
Registration = self.env['event.registration'].with_user(self.user_eventregistrationdesk)
# check ``_get_seats_availability`` tool, giving availabilities for slot / ticket combinations
res = test_event._get_seats_availability([(first_slot, False), (second_slot, False)])
self.assertEqual(res, [first_slot.seats_available, second_slot.seats_available])
# check constraints at registration creation
for create_input, should_crash in [
# ok for event max seats for both slots
(((first_slot, 2), (second_slot, 2)), False),
# not enough seats on first slot
(((first_slot, 3),), True),
# not enough seats on second slot
(((second_slot, 5),), True),
]:
with self.subTest(create_input=create_input, should_crash=should_crash):
create_values = []
for slot, count in create_input:
create_values += [
{
'email': f'{slot.display_name}.{idx}@test.example.com',
'event_id': test_event.id,
'event_slot_id': slot.id,
'name': f'{slot.display_name} {idx}',
} for idx in range(count)
]
if should_crash:
with self.assertRaises(exceptions.ValidationError):
new = Registration.create(create_values)
else:
new = Registration.create(create_values)
self.assertEqual(len(new), sum(count for _slot, count in create_input))
new.with_user(self.user_eventmanager).unlink()
# check ``_verify_seats_availability`` itself
for check_input, should_crash in [
# ok for event max seats for both slots
(((first_slot, False, 2), (second_slot, False, 4)), False),
# not enough seats on first slot
(((first_slot, False, 3),), True),
# not enough seats on second slot
(((second_slot, False, 5),), True),
]:
with self.subTest(check_input=check_input, should_crash=should_crash):
if should_crash:
with self.assertRaises(exceptions.ValidationError):
test_event._verify_seats_availability(check_input)
else:
test_event._verify_seats_availability(check_input)
# check constraint at write (active change) -> ok, check count
all_slot2 = test_event.with_context(active_test=False).registration_ids.filtered(lambda r: r.event_slot_id == second_slot)
self.assertEqual(len(all_slot2), 5, 'Test setup data: 3 active, 2 inactive')
all_slot2.active = True
self.assertEqual(second_slot.seats_available, 2)
self.assertEqual(second_slot.seats_reserved, 3)
# move them on first slot -> crash as it would be out of limits
with self.assertRaises(exceptions.ValidationError):
all_slot2.event_slot_id = first_slot.id
def test_seats_slots_tickets(self):
""" Test: slots and tickets -> limits come from event (global) and tickets """
test_event = self.test_event.with_user(self.user_eventregistrationdesk)
first_slot = test_event.event_slot_ids.filtered(lambda s: s.start_hour == 9)
second_slot = test_event.event_slot_ids.filtered(lambda s: s.start_hour == 13)
first_ticket = test_event.event_ticket_ids.filtered(lambda t: t.name == 'Classic')
second_ticket = test_event.event_ticket_ids.filtered(lambda t: t.name == 'Better')
third_ticket = test_event.event_ticket_ids.filtered(lambda t: t.name == 'VIP')
Registration = self.env['event.registration'].with_user(self.user_eventregistrationdesk)
# check ``_get_seats_availability`` tool, giving availabilities for slot / ticket combinations
res = test_event._get_seats_availability([
(first_slot, first_ticket), (first_slot, second_ticket), (first_slot, third_ticket),
(second_slot, first_ticket), (second_slot, second_ticket), (second_slot, third_ticket),
])
# first slot: 2 seats available, and VIP ticket has 1 seat anyway
# second slot: 4 seats available, Better has 3 max and 1 taken and VIP 1 max
self.assertEqual(res, [2, 2, 1, 4, 2, 1])
# check constraints at registration creation
for create_input, should_crash in [
(((first_slot, second_ticket, 2),), False),
# not enough seats for first slot
(((first_slot, first_ticket, 5),), True),
# not enough seats on VIP ticket
(((second_slot, third_ticket, 2),), True),
]:
with self.subTest(create_input=create_input, should_crash=should_crash):
create_values = []
for slot, ticket, count in create_input:
create_values += [
{
'email': f'{slot.display_name}.{ticket.name}.{idx}@test.example.com',
'event_id': test_event.id,
'event_slot_id': slot.id,
'event_ticket_id': ticket.id,
'name': f'{slot.display_name} {ticket.name} {idx}',
} for idx in range(count)
]
if should_crash:
with self.assertRaises(exceptions.ValidationError):
new = Registration.create(create_values)
else:
new = Registration.create(create_values)
self.assertEqual(len(new), sum(count for _slot, _ticket, count in create_input))
new.with_user(self.user_eventmanager).unlink()
# check create constraint through embed 2many: 2 VIPs is not possible
with self.assertRaises(exceptions.ValidationError):
test_event.with_user(self.user_eventmanager).write({
'registration_ids': [
(0, 0, {'event_slot_id': second_slot.id, 'event_ticket_id': third_ticket.id}),
(0, 0, {'event_slot_id': second_slot.id, 'event_ticket_id': third_ticket.id}),
],
})
# one of them is archived, ok for limit
test_event.with_user(self.user_eventmanager).write({
'registration_ids': [
(0, 0, {'event_slot_id': second_slot.id, 'event_ticket_id': third_ticket.id, 'active': False}),
(0, 0, {'event_slot_id': second_slot.id, 'event_ticket_id': third_ticket.id}),
],
})
archived_vip = test_event.with_context(active_test=False).registration_ids.filtered(lambda r: r.event_slot_id == second_slot and r.event_ticket_id == third_ticket and not r.active)
self.assertTrue(archived_vip)
# writing on active triggers constraint on VIP
with self.assertRaises(exceptions.ValidationError):
archived_vip.active = True

View file

@ -0,0 +1,167 @@
from datetime import datetime
from odoo.addons.event.tests.common import EventCase
from odoo.addons.mail.tests.common import MockEmail
from odoo.tests import Form, tagged, users
from odoo.tools import formataddr
@tagged("event_mail", "mail_template", "mail_thread", "post_install", "-at_install")
class TestMailing(EventCase, MockEmail):
@classmethod
def setUpClass(cls):
super().setUpClass()
# freeze some datetimes, and ensure more than 1D+1H before event starts
# to ease time-based scheduler check
# Since `now` is used to set the `create_date` of an event and create_date
# has often microseconds, we set it to ensure that the scheduler we still be
# launched if scheduled_date == create_date - microseconds
cls.reference_now = datetime(2024, 7, 20, 14, 30, 15, 123456)
cls.event_date_begin = datetime(2024, 7, 22, 8, 0, 0)
cls.event_date_end = datetime(2024, 7, 24, 18, 0, 0)
with cls.mock_datetime_and_now(cls, cls.reference_now):
cls.test_event = cls.env['event.event'].create({
'date_begin': cls.event_date_begin,
'date_end': cls.event_date_end,
'date_tz': 'Europe/Brussels',
'event_mail_ids': False,
'name': 'TestEvent',
})
cls.registrations = cls.env["event.registration"].create([
{
"event_id": cls.test_event.id,
"partner_id": cls.event_customer.id,
},
{
"event_id": cls.test_event.id,
"partner_id": cls.event_customer2.id,
},
{
"email": "robodoo@example.com",
"event_id": cls.test_event.id,
"name": "Robodoo",
},
{
"email": "another.email@example.com",
"event_id": cls.test_event.id,
"name": "Another Email",
"partner_id": cls.event_customer2.id,
},
])
@users("user_eventuser")
def test_event_mail_attendees(self):
template_form = Form(
self.env["mail.template"].with_context(default_model="event.registration", default_name="Test Template")
)
template = template_form.save()
event = self.test_event.with_user(self.env.user)
event.write({
'event_mail_ids': [
(0, 0, {
'interval_type': 'before_event',
'interval_nbr': 0,
'template_ref': f'mail.template,{template.id}',
})
]
})
for event_values, exp_mail_values in [
(
{"organizer_id": self.event_organizer.id, "user_id": self.user_eventmanager.id},
{"email_from": self.event_organizer.email_formatted},
),
(
{"organizer_id": False},
{"email_from": self.user_eventmanager.company_id.email_formatted},
),
(
{"company_id": False},
{"email_from": self.user_eventuser.email_formatted},
),
]:
with self.subTest(event_values=event_values):
event.write(event_values)
with self.mock_mail_gateway(), self.mock_datetime_and_now(self.event_date_begin):
event.event_mail_ids._send_mail(self.registrations)
# where email == partner.email -> partner is recipient
for customer in self.event_customer + self.event_customer2:
self.assertMailMail(
customer,
"outgoing",
fields_values=exp_mail_values,
)
# other registrations: emails
self.assertMailMailWEmails(
[
formataddr(("Robodoo", "robodoo@example.com")),
formataddr(("Another Email", "another.email@example.com")),
],
"outgoing",
fields_values=exp_mail_values,
)
self.assertEqual(len(self._new_mails), 4)
@users("user_eventmanager")
def test_event_mail_recipients(self):
""" Check default / suggested recipients """
_default_organizer = self.user_eventmanager.company_id.partner_id
for event_values, exp_followers, exp_defaults, exp_suggested in [
(
{"organizer_id": self.event_organizer.id, "user_id": self.user_eventuser.id},
self.user_eventmanager.partner_id + self.user_eventuser.partner_id,
{"email_cc": "", "email_to": "", "partner_ids": []},
[],
),
(
{"organizer_id": False, "user_id": self.user_eventuser.id},
self.user_eventmanager.partner_id + self.user_eventuser.partner_id,
{"email_cc": "", "email_to": "", "partner_ids": []},
[],
),
(
{},
self.user_eventmanager.partner_id,
{"email_cc": "", "email_to": "", "partner_ids": []},
[],
),
]:
with self.subTest(event_values=event_values):
test_event = self.env['event.event'].create({
'date_begin': self.event_date_begin,
'date_end': self.event_date_end,
'date_tz': 'Europe/Brussels',
'event_mail_ids': False,
'name': 'TestEvent',
**event_values,
})
self.assertEqual(test_event.message_partner_ids, exp_followers)
defaults = test_event._message_get_default_recipients()[test_event.id]
self.assertDictEqual(defaults, exp_defaults)
suggested = test_event._message_get_suggested_recipients()
self.assertEqual(suggested, exp_suggested)
@users("user_eventuser")
def test_mail_template_creation(self):
""" Check default values when creating registration templates, should
be correctly configured by default. """
template_form_default = Form(
self.env["mail.template"].with_context(default_model="event.registration")
)
template_form_user = Form(self.env["mail.template"])
template_form_user.model_id = self.env["ir.model"]._get("event.registration")
for template in (template_form_default, template_form_user):
self.assertEqual(
template.email_from,
"{{ (object.event_id.organizer_id.email_formatted or object.event_id.company_id.email_formatted or user.email_formatted or '') }}"
)
self.assertFalse(template.email_to)
self.assertEqual(
template.lang,
"{{ object.event_id.lang or object.partner_id.lang }}",
)
self.assertTrue(template.use_default_to)