[MERGE] event(_*): improve attendees views and reporting
PURPOSE To improve Event attendees views and reporting for better usability. SPECIFICATIONS The goal of this commit is to improve attendees views and reporting in Event, the changes done are listed below. - When opening attendees from an event (stat button or kanban button), it will display the list view (before the kanban one) and group by create_date (week). - Removed the date_open field from the model as it is a duplicate of the create_date. - On the attendees tree view, displayed the registration_answer_ids field with a many2many_tags widget (optional default hide), before the Status. - Displayed the "X Cancel" button of the attendees tree view in red. - Moved the partner_id field next to the ticket and kept it editable in attendees form view, also moved visitor field below the Mobile one so that even in debug mode, Attendee Name is the "primary" field. - Added a badge widget on the `state` field. - Added next activity widget to the field `event_ticket_id`. While coming to Attendee reporting section we have changed the default view as graph view instead of stern pivot view and added default groupby `state`, `event_id`, `create_date` and created a new filter `Last 30 Days` and made it default. Apart from that we have made `summary` field default hide in Meeting Room tree view. LINKS PR #79166 Task 2670765 Related: odoo/enterprise#21976 Related: odoo/upgrade#3066 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -288,8 +288,6 @@ class EventMailRegistration(models.Model):
|
||||
def _compute_scheduled_date(self):
|
||||
for mail in self:
|
||||
if mail.registration_id:
|
||||
date_open = mail.registration_id.date_open
|
||||
date_open_datetime = date_open or fields.Datetime.now()
|
||||
mail.scheduled_date = date_open_datetime + _INTERVALS[mail.scheduler_id.interval_unit](mail.scheduler_id.interval_nbr)
|
||||
mail.scheduled_date = mail.registration_id.create_date + _INTERVALS[mail.scheduler_id.interval_unit](mail.scheduler_id.interval_nbr)
|
||||
else:
|
||||
mail.scheduled_date = False
|
||||
|
||||
@@ -27,9 +27,7 @@ class EventRegistration(models.Model):
|
||||
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
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner', string='Booked by',
|
||||
states={'done': [('readonly', True)]})
|
||||
partner_id = fields.Many2one('res.partner', string='Booked by', tracking=1)
|
||||
name = fields.Char(
|
||||
string='Attendee Name', index=True,
|
||||
compute='_compute_name', readonly=False, store=True, tracking=10)
|
||||
@@ -37,7 +35,6 @@ class EventRegistration(models.Model):
|
||||
phone = fields.Char(string='Phone', compute='_compute_phone', readonly=False, store=True, tracking=12)
|
||||
mobile = fields.Char(string='Mobile', compute='_compute_mobile', readonly=False, store=True, tracking=13)
|
||||
# organization
|
||||
date_open = fields.Datetime(string='Registration Date', readonly=True, default=lambda self: fields.Datetime.now()) # weird crash is directly now
|
||||
date_closed = fields.Datetime(
|
||||
string='Attended Date', compute='_compute_date_closed',
|
||||
readonly=False, store=True)
|
||||
@@ -51,26 +48,6 @@ class EventRegistration(models.Model):
|
||||
('open', 'Confirmed'), ('done', 'Attended')],
|
||||
string='Status', default='draft', readonly=True, copy=False, tracking=True)
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner_id(self):
|
||||
""" Keep an explicit onchange on partner_id. Rationale : if user explicitly
|
||||
changes the partner in interface, he want to update the whole customer
|
||||
information. If partner_id is updated in code (e.g. updating your personal
|
||||
information after having registered in website_event_sale) fields with a
|
||||
value should not be reset as we don't know which one is the right one.
|
||||
|
||||
In other words
|
||||
* computed fields based on partner_id should only update missing
|
||||
information. Indeed automated code cannot decide which information
|
||||
is more accurate;
|
||||
* interface should allow to update all customer related information
|
||||
at once. We consider event users really want to update all fields
|
||||
related to the partner;
|
||||
"""
|
||||
for registration in self:
|
||||
if registration.partner_id:
|
||||
registration.update(registration._synchronize_partner_values(registration.partner_id))
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_name(self):
|
||||
for registration in self:
|
||||
|
||||
@@ -24,3 +24,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_event_registration_view_tree {
|
||||
.o_list_button > .o_btn_cancel_registration {
|
||||
color: $danger;
|
||||
&:hover {
|
||||
color: darken($danger, $emphasized-link-hover-darken-percentage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,9 @@ class TestEventCommon(common.TransactionCase):
|
||||
@classmethod
|
||||
def _create_registrations(cls, event, reg_count):
|
||||
# create some registrations
|
||||
create_date = fields.Datetime.now()
|
||||
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,
|
||||
|
||||
@@ -7,62 +7,9 @@ from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo.addons.event.tests.common import TestEventCommon
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import Form
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestEventUI(TestEventCommon):
|
||||
|
||||
def test_event_registration_partner_sync(self):
|
||||
""" Ensure onchange on partner_id is kept for interface, not for computed
|
||||
fields. """
|
||||
registration_form = Form(self.env['event.registration'].with_context(
|
||||
default_name='WrongName',
|
||||
default_event_id=self.event_0.id
|
||||
))
|
||||
self.assertEqual(registration_form.event_id, self.event_0)
|
||||
self.assertEqual(registration_form.name, 'WrongName')
|
||||
self.assertFalse(registration_form.email)
|
||||
self.assertFalse(registration_form.phone)
|
||||
self.assertFalse(registration_form.mobile)
|
||||
|
||||
# trigger onchange
|
||||
registration_form.partner_id = self.event_customer
|
||||
self.assertEqual(registration_form.name, self.event_customer.name)
|
||||
self.assertEqual(registration_form.email, self.event_customer.email)
|
||||
self.assertEqual(registration_form.phone, self.event_customer.phone)
|
||||
self.assertEqual(registration_form.mobile, self.event_customer.mobile)
|
||||
|
||||
# save, check record matches Form values
|
||||
registration = registration_form.save()
|
||||
self.assertEqual(registration.partner_id, self.event_customer)
|
||||
self.assertEqual(registration.name, self.event_customer.name)
|
||||
self.assertEqual(registration.email, self.event_customer.email)
|
||||
self.assertEqual(registration.phone, self.event_customer.phone)
|
||||
self.assertEqual(registration.mobile, self.event_customer.mobile)
|
||||
|
||||
# allow writing on some fields independently from customer config
|
||||
registration.write({'phone': False, 'mobile': False})
|
||||
self.assertFalse(registration.phone)
|
||||
self.assertFalse(registration.mobile)
|
||||
|
||||
# reset partner should not reset other fields
|
||||
registration.write({'partner_id': False})
|
||||
self.assertEqual(registration.partner_id, self.env['res.partner'])
|
||||
self.assertEqual(registration.name, self.event_customer.name)
|
||||
self.assertEqual(registration.email, self.event_customer.email)
|
||||
self.assertFalse(registration.phone)
|
||||
self.assertFalse(registration.mobile)
|
||||
|
||||
# update to a new partner not through UI -> update only void feilds
|
||||
registration.write({'partner_id': self.event_customer2.id})
|
||||
self.assertEqual(registration.partner_id, self.event_customer2)
|
||||
self.assertEqual(registration.name, self.event_customer.name)
|
||||
self.assertEqual(registration.email, self.event_customer.email)
|
||||
self.assertEqual(registration.phone, self.event_customer2.phone)
|
||||
self.assertEqual(registration.mobile, self.event_customer2.mobile)
|
||||
|
||||
|
||||
class TestEventFlow(TestEventCommon):
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
|
||||
|
||||
@@ -89,12 +89,14 @@ class TestMailSchedule(TestEventCommon, MockEmail):
|
||||
|
||||
# create some registrations
|
||||
with freeze_time(now), self.mock_mail_gateway():
|
||||
reg1 = self.env['event.registration'].with_user(self.user_eventuser).create({
|
||||
reg1 = self.env['event.registration'].create({
|
||||
'create_date': now,
|
||||
'event_id': test_event.id,
|
||||
'name': 'Reg1',
|
||||
'email': 'reg1@example.com',
|
||||
})
|
||||
reg2 = self.env['event.registration'].with_user(self.user_eventuser).create({
|
||||
reg2 = self.env['event.registration'].create({
|
||||
'create_date': now,
|
||||
'event_id': test_event.id,
|
||||
'name': 'Reg2',
|
||||
'email': 'reg2@example.com',
|
||||
@@ -105,7 +107,7 @@ class TestMailSchedule(TestEventCommon, MockEmail):
|
||||
|
||||
# check registration state
|
||||
self.assertTrue(all(reg.state == 'open' for reg in reg1 + reg2), 'Registrations: should be auto-confirmed')
|
||||
self.assertTrue(all(reg.date_open == now for reg in reg1 + reg2), 'Registrations: should have open date set to confirm date')
|
||||
self.assertTrue(all(reg.create_date == now for reg in reg1 + reg2), 'Registrations: should have open date set to confirm date')
|
||||
|
||||
# verify that subscription scheduler was auto-executed after each registration
|
||||
self.assertEqual(len(after_sub_scheduler.mail_registration_ids), 2, 'event: should have 2 scheduled communication (1 / registration)')
|
||||
@@ -204,7 +206,8 @@ class TestMailSchedule(TestEventCommon, MockEmail):
|
||||
|
||||
test_event.write({'auto_confirm': False})
|
||||
with freeze_time(now_start), self.mock_mail_gateway():
|
||||
reg3 = self.env['event.registration'].with_user(self.user_eventuser).create({
|
||||
reg3 = self.env['event.registration'].create({
|
||||
'create_date': now_start,
|
||||
'event_id': test_event.id,
|
||||
'name': 'Reg3',
|
||||
'email': 'reg3@example.com',
|
||||
@@ -220,7 +223,7 @@ class TestMailSchedule(TestEventCommon, MockEmail):
|
||||
self.assertTrue(after_sub_scheduler_2.mail_done, 'event: scheduler on registration not updated next to draft registration')
|
||||
|
||||
# confirm registration -> should trigger registration schedulers
|
||||
# NOTE: currently all schedulers are based on date_open which equals create_date
|
||||
# NOTE: currently all schedulers are based on create_date
|
||||
# meaning several communications may be sent in the time time
|
||||
with freeze_time(now_start + relativedelta(hours=1)), self.mock_mail_gateway():
|
||||
reg3.action_confirm()
|
||||
|
||||
@@ -121,6 +121,23 @@
|
||||
|
||||
<!-- EVENT.REGISTRATION ACTIONS -->
|
||||
<record id="act_event_registration_from_event" model="ir.actions.act_window">
|
||||
<field name="res_model">event.registration</field>
|
||||
<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}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No Attendees yet!
|
||||
</p><p>
|
||||
Wait until Attendees register to your Event or create their registrations manually.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- We need kanban view to be displayed first while coming from registration desk, so we have created
|
||||
new action and changed the view sequence. -->
|
||||
<record id="event_registration_action_kanban" model="ir.actions.act_window">
|
||||
<field name="res_model">event.registration</field>
|
||||
<field name="name">Attendees</field>
|
||||
<field name="view_mode">kanban,tree,form,calendar,graph</field>
|
||||
@@ -181,7 +198,8 @@
|
||||
<div class="oe_button_box" name="button_box" groups="base.group_user">
|
||||
<button name="%(event.act_event_registration_from_event)d"
|
||||
type="action"
|
||||
context="{'search_default_expected': True}"
|
||||
context="{'search_default_expected': True,
|
||||
'search_default_group_by_create_date_week': True}"
|
||||
class="oe_stat_button"
|
||||
icon="fa-users"
|
||||
help="Total Registrations for this Event">
|
||||
@@ -364,7 +382,10 @@
|
||||
<div t-if="record.address_id.value"><i class="fa fa-map-marker" title="Location"/> <span class="o_text_overflow o_event_kanban_location" t-esc="record.address_id.value"/></div>
|
||||
</div>
|
||||
<h5 class="o_event_fontsize_11 p-0">
|
||||
<a name="%(act_event_registration_from_event)d" type="action" context="{'search_default_expected': True}">
|
||||
<a name="%(act_event_registration_from_event)d"
|
||||
type="action"
|
||||
context="{'search_default_expected': True,
|
||||
'search_default_group_by_create_date_week': 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"/>
|
||||
@@ -495,9 +516,10 @@
|
||||
<field name="name">event.registration.tree</field>
|
||||
<field name="model">event.registration</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Registration" multi_edit="1" sample="1">
|
||||
<tree string="Registration" multi_edit="1" sample="1"
|
||||
expand="1" default_order="create_date desc"
|
||||
class="o_event_registration_view_tree">
|
||||
<field name="create_date" optional="show" string="Registration Date"/>
|
||||
<field name="date_open" optional="hide"/>
|
||||
<field name="name"/>
|
||||
<field name="partner_id" optional="hide"/>
|
||||
<field name="email" optional="show"/>
|
||||
@@ -505,12 +527,17 @@
|
||||
<field name="mobile" optional="hide"/>
|
||||
<field name="event_id" invisible="context.get('default_event_id')"/>
|
||||
<field name="event_ticket_id" domain="[('event_id', '=', event_id)]"/>
|
||||
<field name="state" readonly="0"/>
|
||||
<field name="activity_ids" widget="list_activity"/>
|
||||
<field name="state" decoration-info="state in ('draft', 'open')"
|
||||
decoration-success="state == 'done'"
|
||||
decoration-muted="state == 'cancel'" widget="badge"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
|
||||
<field name="message_needaction" invisible="1"/>
|
||||
<button name="action_confirm" string="Confirm" states="draft" type="object" icon="fa-check"/>
|
||||
<button name="action_set_done" string="Mark as Attending" states="open" type="object" icon="fa-level-down"/>
|
||||
<button name="action_cancel" string="Cancel" states="draft,open" type="object" icon="fa-times"/>
|
||||
<button name="action_cancel" string="Cancel"
|
||||
states="draft,open" type="object"
|
||||
class="o_btn_cancel_registration" icon="fa-times"/>
|
||||
<field name="activity_exception_decoration" widget="activity_exception"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -533,7 +560,6 @@
|
||||
<div class="oe_button_box" name="button_box"/>
|
||||
<group>
|
||||
<group string="Attendee" name="attendee">
|
||||
<field name="partner_id" attrs="{'readonly':[('state', '!=', 'draft')]}"/>
|
||||
<field class="o_text_overflow" name="name"/>
|
||||
<field name="email"/>
|
||||
<field name="phone" class="o_force_ltr"/>
|
||||
@@ -548,7 +574,8 @@
|
||||
]"
|
||||
attrs="{'invisible': [('event_id', '=', False)]}"
|
||||
options="{'no_open': True, 'no_create': True}"/>
|
||||
<field name="date_open" groups="base.group_no_one"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="create_date" string="Registration Date" groups="base.group_no_one"/>
|
||||
<field name="date_closed" groups="base.group_no_one"/>
|
||||
</group>
|
||||
<group string="Marketing" name="utm_link" groups="base.group_no_one">
|
||||
@@ -686,7 +713,7 @@
|
||||
<filter string="Confirmed" name="confirmed" domain="[('state', '=', 'open')]"/>
|
||||
<filter string="Attended" name="attended" domain="[('state', '=', 'done')]"/>
|
||||
<separator/>
|
||||
<filter string="Registration Date" name="filter_date_open" date="date_open"/>
|
||||
<filter string="Registration Date" name="filter_create_date" date="create_date"/>
|
||||
<filter string="Event Start Date" name="filter_event_begin_date" date="event_begin_date"/>
|
||||
<filter string="Attended Date" name="filter_date_closed" date="date_closed"/>
|
||||
<field name="event_id"/>
|
||||
@@ -699,12 +726,16 @@
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Last 30 days" name="filter_last_month_creation" domain="[('create_date','>', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Partner" name="partner" domain="[]" context="{'group_by':'partner_id'}"/>
|
||||
<filter string="Event" name="group_event" domain="[]" context="{'group_by':'event_id'}"/>
|
||||
<filter string="Ticket Type" name ="group_event_ticket_id" domain="[]" context="{'group_by': 'event_ticket_id'}"/>
|
||||
<filter string="Status" name="status" domain="[]" context="{'group_by':'state'}"/>
|
||||
<filter string="Registration Date" name="createmonth" domain="[]" context="{'group_by': 'create_date:month'}"/>
|
||||
<filter string="Registration Date" name="group_by_create_date_week" domain="[]" context="{'group_by': 'create_date:week'}"
|
||||
invisible="context.get('registration_view_hide_group_by_create_date_week')"/>
|
||||
<filter string="Registration Date" name="group_by_create_date_day" domain="[]" context="{'group_by': 'create_date:day'}"
|
||||
invisible="not context.get('registration_view_hide_group_by_create_date_week')"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
@@ -714,8 +745,15 @@
|
||||
<field name="name">Attendees</field>
|
||||
<field name="res_model">event.registration</field>
|
||||
<field name="domain"></field>
|
||||
<field name="view_mode">pivot,graph,kanban,tree,form</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="view_mode">graph,pivot,kanban,tree,form</field>
|
||||
<field name="context">{
|
||||
'search_default_filter_last_month_creation': 1,
|
||||
'search_default_status': 1,
|
||||
'search_default_group_by_create_date_day': 1,
|
||||
'search_default_group_event': 1,
|
||||
'registration_view_hide_group_by_create_date_week': 1,
|
||||
}
|
||||
</field>
|
||||
<field name="search_view_id" ref="view_registration_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<field name="model">event.registration</field>
|
||||
<field name="inherit_id" ref="event.view_event_registration_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<xpath expr="//field[@name='mobile']" position="after">
|
||||
<field name="visitor_id" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -68,12 +68,12 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Meeting Room" multi_edit="1" sample="1">
|
||||
<field name="name"/>
|
||||
<field name="summary"/>
|
||||
<field name="summary" optional="hide"/>
|
||||
<field name="target_audience"/>
|
||||
<field name="is_published"/>
|
||||
<field name="is_pinned"/>
|
||||
<field name="room_is_full" readonly="1"/>
|
||||
<field name="room_participant_count" readonly="1"/>
|
||||
<field name="room_participant_count" readonly="1" sum="Total Participant Count" />
|
||||
<field name="room_max_capacity"/>
|
||||
<field name="room_lang_id"/>
|
||||
</tree>
|
||||
|
||||
@@ -28,3 +28,12 @@ class EventRegistrationAnswer(models.Model):
|
||||
_sql_constraints = [
|
||||
('value_check', "CHECK(value_answer_id IS NOT NULL OR COALESCE(value_text_box, '') <> '')", "There must be a suggested value or a text value.")
|
||||
]
|
||||
|
||||
# for displaying selected answers by attendees in attendees list view
|
||||
def name_get(self):
|
||||
return [
|
||||
(reg.id,
|
||||
reg.value_answer_id.name if reg.question_type == "simple_choice" else reg.value_text_box
|
||||
)
|
||||
for reg in self
|
||||
]
|
||||
|
||||
@@ -24,4 +24,15 @@
|
||||
</sheet>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_registration_view_tree" model="ir.ui.view">
|
||||
<field name="name">event.registration.view.tree.inherit.website.event.questions</field>
|
||||
<field name="model">event.registration</field>
|
||||
<field name="inherit_id" ref="event.view_event_registration_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="state" position="before">
|
||||
<field name="registration_answer_ids" string="Selected Answers" widget="many2many_tags" optional="hide"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user