From 6b8daa880c97cbd4aaeb85b9ef39a6893cac891e Mon Sep 17 00:00:00 2001 From: Patrick Hoste Date: Thu, 12 Jan 2023 14:36:58 +0000 Subject: [PATCH] [IMP] website_event(_sale): adds new identification questions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes the way the identification questions (name, email, phone) are asked when registering to an event. They aren't hardcoded anymore and can be created per event the same way other questions can be. They can be set as mandatory or not and the order can be changed. One can now also ask for the attendee company name. Task-3056380 closes odoo/odoo#112164 Related: odoo/upgrade#4313 Signed-off-by: Warnon Aurélien (awa) --- addons/event/data/mail_template_data.xml | 12 +-- addons/event/models/event_registration.py | 11 +++ .../event/views/event_registration_views.xml | 2 + .../src/js/tours/wevent_performance_tour.js | 36 +++---- .../src/js/tours/wevent_register_tour.js | 24 ++--- .../test_event_full/tests/test_performance.py | 2 +- addons/website_event/controllers/main.py | 86 ++++++++-------- addons/website_event/data/event_demo.xml | 56 +++++++++++ addons/website_event/models/event_event.py | 6 +- addons/website_event/models/event_question.py | 7 +- .../models/event_registration.py | 2 +- addons/website_event/models/event_type.py | 11 ++- .../static/tests/tours/tickets_questions.js | 13 ++- addons/website_event/tests/common.py | 3 - .../tests/test_event_internals.py | 57 ++++++++--- .../website_event/tests/test_website_event.py | 19 +++- .../website_event/views/event_event_views.xml | 5 +- .../views/event_question_views.xml | 2 +- .../views/event_registration_views.xml | 2 +- .../event_templates_page_registration.xml | 97 ++++++++----------- .../static/tests/tours/website_event_sale.js | 24 ++--- .../tours/website_event_sale_last_ticket.js | 18 ++-- .../tours/website_event_sale_pricelists.js | 12 +-- .../views/website_event_templates.xml | 8 +- 24 files changed, 320 insertions(+), 195 deletions(-) diff --git a/addons/event/data/mail_template_data.xml b/addons/event/data/mail_template_data.xml index e34855e198b..75235a18b76 100644 --- a/addons/event/data/mail_template_data.xml +++ b/addons/event/data/mail_template_data.xml @@ -11,7 +11,7 @@ Sent automatically to someone after they registered to an event
- Dear Oscar Morgan,
+ Dear ,
Thank you for your inquiry.
Here is your badge for the event OpenWood Collection Online Reveal.
If you have any questions, please let us know. @@ -50,7 +50,7 @@ Your registration
- Oscar Morgan + @@ -75,7 +75,7 @@
- Hello Oscar Morgan,
+ Hello ,
We are happy to confirm your registration to the event OpenWood Collection Online Reveal @@ -83,7 +83,7 @@ OpenWood Collection Online Reveal - for attendee Oscar Morgan. + for attendee .

@@ -282,7 +282,7 @@
Your registration
- Oscar Morgan +
- Hello Oscar Morgan,
+ Hello ,
We are excited to remind you that the event OpenWood Collection Online Reveal diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index 22043ef9c5e..15456493a58 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -34,6 +34,8 @@ class EventRegistration(models.Model): email = fields.Char(string='Email', compute='_compute_email', readonly=False, store=True, tracking=11) 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) + company_name = fields.Char( + string='Company Name', compute='_compute_company_name', readonly=False, store=True, tracking=14) # organization date_closed = fields.Datetime( string='Attended Date', compute='_compute_date_closed', @@ -86,6 +88,15 @@ class EventRegistration(models.Model): fnames=['mobile'] ).get('mobile') or False + @api.depends('partner_id') + def _compute_company_name(self): + for registration in self: + if not registration.company_name and registration.partner_id: + registration.company_name = registration._synchronize_partner_values( + registration.partner_id, + fnames=['company_name'] + ).get('company_name') or False + @api.depends('state') def _compute_date_closed(self): for registration in self: diff --git a/addons/event/views/event_registration_views.xml b/addons/event/views/event_registration_views.xml index e813c50e303..d5497e684a9 100644 --- a/addons/event/views/event_registration_views.xml +++ b/addons/event/views/event_registration_views.xml @@ -16,6 +16,7 @@ + @@ -64,6 +65,7 @@ + diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py index 7ae4c40491e..781c02ca756 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -1,20 +1,14 @@ # -*- coding: utf-8 -*- import babel.dates -import pytz import re import werkzeug from ast import literal_eval -from collections import defaultdict -from datetime import datetime, timedelta -from dateutil.parser import parse -from dateutil.relativedelta import relativedelta from werkzeug.datastructures import OrderedMultiDict from werkzeug.exceptions import NotFound from odoo import fields, http, _ -from odoo.addons.http_routing.models.ir_http import slug from odoo.addons.website.controllers.main import QueryURL from odoo.http import request from odoo.osv import expression @@ -270,51 +264,65 @@ class WebsiteEventController(http.Controller): if int(ticket_id) not in event.event_ticket_ids.ids and len(event.event_ticket_ids.ids) > 0: raise UserError(_("This ticket is not available for sale for this event")) registrations = {} - global_values = {} general_answer_ids = [] + general_identification_answers = {} + # as we may have several questions populating the same field (e.g: the phone) + # we use this to hold the fields that have already been handled + # goal is to use the answer to the first question of every 'type' (aka name / phone / email / company name) + already_handled_fields_data = {} for key, value in form_details.items(): - if 'question_answer' in key and value: - dummy, registration_index, question_id = key.split('-') - question_sudo = request.env['event.question'].browse(int(question_id)) - answer_values = None - if question_sudo.question_type == 'simple_choice': - answer_values = { - 'question_id': int(question_id), - 'value_answer_id': int(value) - } - elif question_sudo.question_type == 'text_box': - answer_values = { - 'question_id': int(question_id), - 'value_text_box': value - } + if not value: + continue - if answer_values and not int(registration_index): - general_answer_ids.append((0, 0, answer_values)) - elif answer_values: - registrations.setdefault(registration_index, dict())\ - .setdefault('registration_answer_ids', list()).append((0, 0, answer_values)) - else: - counter, attr_name = key.split('-', 1) - field_name = attr_name.split('-')[0] + key_values = key.split('-') + # Special case for handling event_ticket_id data that holds only 2 values + if len(key_values) == 2: + registration_index, field_name = key_values if field_name not in registration_fields: continue - elif isinstance(registration_fields[field_name], (fields.Many2one, fields.Integer)): - value = int(value) or False # 0 is considered as a void many2one aka False - else: - value = value + registrations.setdefault(registration_index, dict())[field_name] = int(value) + continue - if counter == '0': - global_values[attr_name] = value + registration_index, question_type, question_id = key_values + answer_values = None + if question_type == 'simple_choice': + answer_values = { + 'question_id': int(question_id), + 'value_answer_id': int(value) + } + else: + answer_values = { + 'question_id': int(question_id), + 'value_text_box': value + } + + if answer_values and not int(registration_index): + general_answer_ids.append((0, 0, answer_values)) + elif answer_values: + registrations.setdefault(registration_index, dict())\ + .setdefault('registration_answer_ids', list()).append((0, 0, answer_values)) + + if question_type in ('name', 'email', 'phone', 'company_name')\ + and question_type not in already_handled_fields_data.get(registration_index, []): + if question_type not in registration_fields: + continue + + field_name = question_type + already_handled_fields_data.setdefault(registration_index, list()).append(field_name) + + if not int(registration_index): + general_identification_answers[field_name] = value else: - registrations.setdefault(counter, dict())[attr_name] = value - for key, value in global_values.items(): - for registration in registrations.values(): - registration[key] = value + registrations.setdefault(registration_index, dict())[field_name] = value if general_answer_ids: for registration in registrations.values(): registration.setdefault('registration_answer_ids', list()).extend(general_answer_ids) + if general_identification_answers: + for registration in registrations.values(): + registration.update(general_identification_answers) + return list(registrations.values()) def _create_attendees_from_registration_post(self, event, registration_data): diff --git a/addons/website_event/data/event_demo.xml b/addons/website_event/data/event_demo.xml index 5468167445b..0f12a338c79 100644 --- a/addons/website_event/data/event_demo.xml +++ b/addons/website_event/data/event_demo.xml @@ -1,5 +1,29 @@ + + + + + + + + + + + + + + + Get Inspired • Stay Connected • Have Fun @@ -36,6 +60,10 @@
+ @@ -57,6 +85,10 @@ + @@ -99,6 +131,10 @@ + @@ -117,12 +153,20 @@ + Discover how to grow a sustainable business with our experts. {"background-image": "url('/website_event/static/src/img/event_cover_4.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"} + @@ -142,11 +186,19 @@ + {"background-image": "none", "background-color": "secondary", "opacity": ""} + @@ -169,6 +221,10 @@ + diff --git a/addons/website_event/models/event_event.py b/addons/website_event/models/event_event.py index b7b42da0509..15462b1c3a5 100644 --- a/addons/website_event/models/event_event.py +++ b/addons/website_event/models/event_event.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from ast import literal_eval -from collections import defaultdict from dateutil.relativedelta import relativedelta import json import werkzeug.urls @@ -36,6 +35,9 @@ class Event(models.Model): }) return res + def _default_question_ids(self): + return self.env['event.type']._default_question_ids() + # description subtitle = fields.Char('Event Subtitle', translate=True) # registration @@ -219,7 +221,7 @@ class Event(models.Model): questions_tokeep_ids = [] for event in self: if not event.event_type_id and not event.question_ids: - event.question_ids = False + event.question_ids = self._default_question_ids() continue if questions_tokeep_ids: diff --git a/addons/website_event/models/event_question.py b/addons/website_event/models/event_question.py index b005731cd17..6b7d1f87264 100644 --- a/addons/website_event/models/event_question.py +++ b/addons/website_event/models/event_question.py @@ -14,7 +14,12 @@ class EventQuestion(models.Model): title = fields.Char(required=True, translate=True) question_type = fields.Selection([ ('simple_choice', 'Selection'), - ('text_box', 'Text Input')], default='simple_choice', string="Question Type", required=True) + ('text_box', 'Text Input'), + ('name', 'Name'), + ('email', 'Email'), + ('phone', 'Phone'), + ('company_name', 'Company'), + ], default='simple_choice', string="Question Type", required=True) event_type_id = fields.Many2one('event.type', 'Event Type', ondelete='cascade') event_id = fields.Many2one('event.event', 'Event', ondelete='cascade') answer_ids = fields.One2many('event.question.answer', 'question_id', "Answers", copy=True) diff --git a/addons/website_event/models/event_registration.py b/addons/website_event/models/event_registration.py index 6fea1061f26..4bf084649f8 100644 --- a/addons/website_event/models/event_registration.py +++ b/addons/website_event/models/event_registration.py @@ -12,4 +12,4 @@ class EventRegistration(models.Model): registration_answer_ids = fields.One2many('event.registration.answer', 'registration_id', string='Attendee Answers') def _get_website_registration_allowed_fields(self): - return {'name', 'phone', 'email', 'mobile', 'event_id', 'partner_id', 'event_ticket_id'} + return {'name', 'phone', 'email', 'mobile', 'company_name', 'event_id', 'partner_id', 'event_ticket_id'} diff --git a/addons/website_event/models/event_type.py b/addons/website_event/models/event_type.py index 57e963f8bff..03052a4ba1e 100644 --- a/addons/website_event/models/event_type.py +++ b/addons/website_event/models/event_type.py @@ -1,13 +1,20 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ class EventType(models.Model): _name = 'event.type' _inherit = ['event.type'] + def _default_question_ids(self): + return [ + (0, 0, {'title': _('Name'), 'question_type': 'name', 'is_mandatory_answer': True}), + (0, 0, {'title': _('Email'), 'question_type': 'email', 'is_mandatory_answer': True}), + (0, 0, {'title': _('Phone'), 'question_type': 'phone'}), + ] + website_menu = fields.Boolean('Display a dedicated menu on Website') community_menu = fields.Boolean( "Community Menu", compute="_compute_community_menu", @@ -17,7 +24,7 @@ class EventType(models.Model): 'Extra Register Button', compute='_compute_menu_register_cta', readonly=False, store=True) question_ids = fields.One2many( - 'event.question', 'event_type_id', + 'event.question', 'event_type_id', default=_default_question_ids, string='Questions', copy=True) @api.depends('website_menu') diff --git a/addons/website_event/static/tests/tours/tickets_questions.js b/addons/website_event/static/tests/tours/tickets_questions.js index a54b5a3409b..c6d014df220 100644 --- a/addons/website_event/static/tests/tours/tickets_questions.js +++ b/addons/website_event/static/tests/tours/tickets_questions.js @@ -25,10 +25,10 @@ registry.category("web_tour.tours").add('test_tickets_questions', { trigger: 'div:contains("Ticket #1").modal-body input[name*="phone"]', run: 'text +32499123456' }, { - trigger: 'div:contains("Ticket #1").modal-body select[name*="question_answer"]', + trigger: 'div:contains("Ticket #1").modal-body select[name*="1-simple_choice"]', run: 'text Vegetarian' }, { - trigger: 'div:contains("Ticket #1").modal-body textarea[name*="question_answer"]', + trigger: 'div:contains("Ticket #1").modal-body textarea[name*="1-text_box"]', run: 'text Fish and Nuts' }, { trigger: 'div:contains("Ticket #2").modal-body input[name*="name"]', @@ -37,13 +37,16 @@ registry.category("web_tour.tours").add('test_tickets_questions', { trigger: 'div:contains("Ticket #2").modal-body input[name*="email"]', run: 'text attendee-b@gmail.com' }, { - trigger: 'div:contains("Ticket #2").modal-body select[name*="question_answer"]', + trigger: 'div:contains("Ticket #2").modal-body input[name*="company_name"]', + run: 'text My Company' +}, { + trigger: 'div:contains("Ticket #2").modal-body select[name*="2-simple_choice"]', run: 'text Pastafarian' }, { - trigger: 'div.o_wevent_registration_question_global select[name*="question_answer"]', + trigger: 'div.o_wevent_registration_question_global select[name*="0-simple_choice"]', run: 'text A friend' }, { - trigger: 'button:contains("Continue")', + trigger: 'button[type=submit]', run: 'click' }, { // The tour stops too early and the registration fails if we don't wait the confirmation. diff --git a/addons/website_event/tests/common.py b/addons/website_event/tests/common.py index bf19adcb309..d798bee5173 100644 --- a/addons/website_event/tests/common.py +++ b/addons/website_event/tests/common.py @@ -2,12 +2,9 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import datetime, timedelta, time -from unittest.mock import patch from odoo.addons.event.tests.common import EventCase from odoo.addons.mail.tests.common import mail_new_test_user -from odoo.fields import Datetime as FieldsDatetime, Date as FieldsDate -from odoo.tests.common import TransactionCase class OnlineEventCase(EventCase): diff --git a/addons/website_event/tests/test_event_internals.py b/addons/website_event/tests/test_event_internals.py index ca5f364b5aa..4fe636115e5 100644 --- a/addons/website_event/tests/test_event_internals.py +++ b/addons/website_event/tests/test_event_internals.py @@ -25,10 +25,15 @@ class TestEventData(TestEventQuestionCommon): self.assertEqual( event.question_ids.mapped('question_type'), - ['simple_choice', 'simple_choice', 'text_box']) - self.assertEqual(event.specific_question_ids.title, 'Question1') + ['name', 'email', '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.mapped('answer_ids.name')), + set(event.specific_question_ids.filtered( + lambda q: q.question_type in ['simple_choice', 'text_box']).mapped('answer_ids.name')), set(['Q1-Answer1', 'Q1-Answer2'])) self.assertEqual(len(event.general_question_ids), 2) self.assertEqual(event.general_question_ids[0].title, 'Question2') @@ -55,19 +60,34 @@ class TestEventData(TestEventQuestionCommon): 'seats_max': 200, }]) + [name_question, email_question, phone_question] = event.question_ids.filtered( + lambda q: q.question_type in ('name', 'email', 'phone', 'company_name')) + + [second_phone_question, company_name_question] = self.env['event.question'].create([{ + 'title': 'Second Phone', + 'question_type': 'phone', + 'event_id': event.id, + }, { + 'title': 'Company Name', + 'question_type': 'company_name', + 'event_id': event.id, + }]) + form_details = { - '1-name': 'Pixis', - '1-email': 'pixis@gmail.com', - '1-phone': '+32444444444', + '1-name-%s' % name_question.id: 'Pixis', + '1-email-%s' % email_question.id: 'pixis@gmail.com', + '1-phone-%s' % phone_question.id: '+32444444444', + '1-phone-%s' % second_phone_question.id: '+32555555555', '1-event_ticket_id': ticket_id_1.id, - '2-name': 'Geluchat', - '2-email': 'geluchat@gmail.com', - '2-phone': '+32777777777', + '2-name-%s' % name_question.id: 'Geluchat', + '2-email-%s' % email_question.id: 'geluchat@gmail.com', + '2-phone-%s' % phone_question.id: '+32777777777', + '2-company_name-%s' % company_name_question.id: 'My Company', '2-event_ticket_id': ticket_id_2.id, - 'question_answer-1-%s' % self.event_question_1.id: '5', - 'question_answer-2-%s' % self.event_question_1.id: '9', - 'question_answer-0-%s' % self.event_question_2.id: '7', - 'question_answer-0-%s' % self.event_question_3.id: 'Free Text', + '1-simple_choice-%s' % self.event_question_1.id: '5', + '2-simple_choice-%s' % self.event_question_1.id: '9', + '0-simple_choice-%s' % self.event_question_2.id: '7', + '0-text_box-%s' % self.event_question_3.id: 'Free Text', } with MockRequest(self.env): @@ -76,11 +96,20 @@ class TestEventData(TestEventQuestionCommon): self.assertEqual(registrations, [ {'name': 'Pixis', 'email': 'pixis@gmail.com', 'phone': '+32444444444', 'event_ticket_id': ticket_id_1.id, 'registration_answer_ids': [ + (0, 0, {'question_id': name_question.id, 'value_text_box': 'Pixis'}), + (0, 0, {'question_id': email_question.id, 'value_text_box': 'pixis@gmail.com'}), + (0, 0, {'question_id': phone_question.id, 'value_text_box': '+32444444444'}), + (0, 0, {'question_id': second_phone_question.id, 'value_text_box': '+32555555555'}), (0, 0, {'question_id': self.event_question_1.id, 'value_answer_id': 5}), (0, 0, {'question_id': self.event_question_2.id, 'value_answer_id': 7}), (0, 0, {'question_id': self.event_question_3.id, 'value_text_box': 'Free Text'})]}, - {'name': 'Geluchat', 'email': 'geluchat@gmail.com', 'phone': '+32777777777', 'event_ticket_id': ticket_id_2.id, + {'name': 'Geluchat', 'email': 'geluchat@gmail.com', 'phone': '+32777777777', 'company_name': 'My Company', + 'event_ticket_id': ticket_id_2.id, 'registration_answer_ids': [ + (0, 0, {'question_id': name_question.id, 'value_text_box': 'Geluchat'}), + (0, 0, {'question_id': email_question.id, 'value_text_box': 'geluchat@gmail.com'}), + (0, 0, {'question_id': phone_question.id, 'value_text_box': '+32777777777'}), + (0, 0, {'question_id': company_name_question.id, 'value_text_box': 'My Company'}), (0, 0, {'question_id': self.event_question_1.id, 'value_answer_id': 9}), (0, 0, {'question_id': self.event_question_2.id, 'value_answer_id': 7}), (0, 0, {'question_id': self.event_question_3.id, 'value_text_box': 'Free Text'})]} diff --git a/addons/website_event/tests/test_website_event.py b/addons/website_event/tests/test_website_event.py index ab694d3cdb4..8a615845c72 100644 --- a/addons/website_event/tests/test_website_event.py +++ b/addons/website_event/tests/test_website_event.py @@ -48,6 +48,18 @@ class TestUi(HttpCaseWithUserDemo): })], 'website_published': True, 'question_ids': [(0, 0, { + 'title': 'Name', + 'question_type': 'name', + }), (0, 0, { + 'title': 'Email', + 'question_type': 'email', + }), (0, 0, { + 'title': 'Phone', + 'question_type': 'phone', + }), (0, 0, { + 'title': 'Company Name', + 'question_type': 'company_name', + }), (0, 0, { 'title': 'Meal Type', 'question_type': 'simple_choice', 'answer_ids': [ @@ -81,12 +93,13 @@ class TestUi(HttpCaseWithUserDemo): self.assertEqual(first_registration.name, 'Attendee A') self.assertEqual(first_registration.phone, '+32499123456') self.assertEqual(second_registration.name, 'Attendee B') + self.assertEqual(second_registration.company_name, 'My Company') event_questions = registrations.mapped('event_id.question_ids') - self.assertEqual(len(event_questions), 3) + self.assertEqual(len(event_questions), 7) first_registration_answers = first_registration.registration_answer_ids - self.assertEqual(len(first_registration_answers), 3) + self.assertEqual(len(first_registration_answers), 6) self.assertEqual(first_registration_answers.filtered( lambda answer: answer.question_id.title == 'Meal Type' @@ -101,7 +114,7 @@ class TestUi(HttpCaseWithUserDemo): ).value_answer_id.name, 'A friend') second_registration_answers = second_registration.registration_answer_ids - self.assertEqual(len(second_registration_answers), 2) + self.assertEqual(len(second_registration_answers), 5) self.assertEqual(second_registration_answers.filtered( lambda answer: answer.question_id.title == 'Meal Type' diff --git a/addons/website_event/views/event_event_views.xml b/addons/website_event/views/event_event_views.xml index c5547adc2d5..56fe49c5a3c 100644 --- a/addons/website_event/views/event_event_views.xml +++ b/addons/website_event/views/event_event_views.xml @@ -39,7 +39,8 @@ -