From 91617bfc78e3da94be2276fe1c82c4ce47eeb896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 9 Feb 2017 10:08:01 +0100 Subject: [PATCH] [IMP] event: ease event configuration and management through categories This commit improve event.type (Event Categories) model and views in order to ease event configuration through more detailed categories. The purpose is to be able to define categories holding default data for website, tickets, attendee mailing, ... Choosing a category on a new event takes those default values to help users creating finely-tuned events. Main configuration on event.type is * auto confirmation, replacing the old system-wide auto confirmation parameter * seats limitation * location: online events, timezone * communication: reply-to email address, twitter hashtag, automated mailing of attendees * ticketing * website parameters: display on website, display tracks, allow track proposal * question to attendees --- addons/event/data/event_data.xml | 3 - addons/event/models/event.py | 103 ++++++++++--- addons/event/models/event_config_settings.py | 1 - addons/event/models/event_mail.py | 28 +++- addons/event/security/ir.model.access.csv | 2 + addons/event/tests/test_event_flow.py | 10 +- addons/event/tests/test_mail_schedule.py | 2 +- .../views/event_config_settings_views.xml | 13 +- addons/event/views/event_views.xml | 143 +++++++++++++++++- addons/event_sale/models/event.py | 62 ++++++-- addons/event_sale/views/event_views.xml | 29 ++++ addons/website_event/models/event.py | 18 ++- .../views/event_config_settings_views.xml | 6 +- addons/website_event/views/event_views.xml | 35 ++++- .../website_event_questions/models/event.py | 37 ++++- .../views/event_views.xml | 27 +++- addons/website_event_track/models/event.py | 14 ++ .../website_event_track/views/event_views.xml | 35 +++-- 18 files changed, 468 insertions(+), 100 deletions(-) diff --git a/addons/event/data/event_data.xml b/addons/event/data/event_data.xml index 54acbe204a3..0986e7d2942 100644 --- a/addons/event/data/event_data.xml +++ b/addons/event/data/event_data.xml @@ -8,9 +8,6 @@ Module Events has been installed - - - Event: Mail Scheduler diff --git a/addons/event/models/event.py b/addons/event/models/event.py index 7a4a2496f0b..b25f41b783a 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -14,25 +14,8 @@ class EventType(models.Model): _name = 'event.type' _description = 'Event Category' - name = fields.Char('Event Category', required=True, translate=True) - default_reply_to = fields.Char('Reply To') - default_registration_min = fields.Integer( - 'Default Minimum Registration', default=0, - help="It will select this default minimum value when you choose this event") - default_registration_max = fields.Integer( - 'Default Maximum Registration', default=0, - help="It will select this default maximum value when you choose this event") - - -class EventEvent(models.Model): - """Event""" - _name = 'event.event' - _description = 'Event' - _inherit = ['mail.thread'] - _order = 'date_begin' - @api.model - def _default_event_mail_ids(self): + def _get_default_event_type_mail_ids(self): return [(0, 0, { 'interval_unit': 'now', 'interval_type': 'after_sub', @@ -49,6 +32,57 @@ class EventEvent(models.Model): 'template_id': self.env.ref('event.event_reminder') })] + name = fields.Char('Event Category', required=True, translate=True) + # registration + has_seats_limitation = fields.Boolean( + 'Limited Seats', default=False) + default_registration_min = fields.Integer( + 'Minimum Registrations', default=0, + help="It will select this default minimum value when you choose this event") + default_registration_max = fields.Integer( + 'Maximum Registrations', default=0, + help="It will select this default maximum value when you choose this event") + auto_confirm = fields.Boolean( + 'Automatically Confirm Registrations', default=True, + help="Events and registrations will automatically be confirmed" + "upon creation, easing the flow for simple events.") + # location + is_online = fields.Boolean( + 'Online Event', help='Online events like webinars do not require a specific location and are hosted online.') + use_timezone = fields.Boolean('Use Default Timezone') + default_timezone = fields.Selection( + '_tz_get', string='Timezone', + default=lambda self: self.env.user.tz) + # communication + use_reply_to = fields.Boolean('Use Default Reply-To') + default_reply_to = fields.Char('Reply To') + use_hashtag = fields.Boolean('Use Default Hashtag') + default_hashtag = fields.Char('Twitter Hashtag') + use_mail_schedule = fields.Boolean( + 'Automatically Send Emails', default=True) + event_type_mail_ids = fields.One2many( + 'event.type.mail', 'event_type_id', string='Mail Schedule', + copy=False, + default=_get_default_event_type_mail_ids) + + @api.onchange('has_seats_limitation') + def _onchange_has_seats_limitation(self): + if not self.has_seats_limitation: + self.default_registration_min = 0 + self.default_registration_max = 0 + + @api.model + def _tz_get(self): + return [(x, x) for x in pytz.all_timezones] + + +class EventEvent(models.Model): + """Event""" + _name = 'event.event' + _description = 'Event' + _inherit = ['mail.thread'] + _order = 'date_begin' + name = fields.Char( string='Event Name', translate=True, required=True, readonly=False, states={'done': [('readonly', True)]}) @@ -71,7 +105,7 @@ class EventEvent(models.Model): readonly=False, states={'done': [('readonly', True)]}, oldname='type') color = fields.Integer('Kanban Color Index') - event_mail_ids = fields.One2many('event.mail', 'event_id', string='Mail Schedule', default=_default_event_mail_ids, copy=True) + event_mail_ids = fields.One2many('event.mail', 'event_id', string='Mail Schedule', copy=True) # Seats and computation seats_max = fields.Integer( @@ -120,16 +154,18 @@ class EventEvent(models.Model): ('confirm', 'Confirmed'), ('done', 'Done')], string='Status', default='draft', readonly=True, required=True, copy=False, help="If event is created, the status is 'Draft'. If event is confirmed for the particular dates the status is set to 'Confirmed'. If the event is over, the status is set to 'Done'. If event is cancelled the status is set to 'Cancelled'.") - auto_confirm = fields.Boolean(string='Autoconfirm Registrations', compute='_compute_auto_confirm') + auto_confirm = fields.Boolean(string='Autoconfirm Registrations') reply_to = fields.Char( 'Reply-To Email', readonly=False, states={'done': [('readonly', True)]}, help="The email address of the organizer is likely to be put here, with the effect to be in the 'Reply-To' of the mails sent automatically at event or registrations confirmation. You can also put the email address of your mail gateway if you use one.") + is_online = fields.Boolean('Online Event') address_id = fields.Many2one( 'res.partner', string='Location', default=lambda self: self.env.user.company_id.partner_id, readonly=False, states={'done': [('readonly', True)]}, track_visibility="onchange") country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True) + twitter_hashtag = fields.Char('Twitter Hashtag') description = fields.Html( string='Description', oldname='note', translate=html_translate, sanitize_attributes=False, readonly=False, states={'done': [('readonly', True)]}) @@ -189,10 +225,6 @@ class EventEvent(models.Model): else: self.date_end_located = False - @api.one - def _compute_auto_confirm(self): - self.auto_confirm = self.env['ir.values'].get_default('event.config.settings', 'default_auto_confirmation') - @api.onchange('event_type_id') def _onchange_type(self): if self.event_type_id: @@ -200,7 +232,28 @@ class EventEvent(models.Model): self.seats_max = self.event_type_id.default_registration_max if self.event_type_id.default_registration_max: self.seats_availability = 'limited' - self.reply_to = self.event_type_id.default_reply_to + + if self.event_type_id.auto_confirm: + self.auto_confirm = self.event_type_id.auto_confirm + + if self.event_type_id.use_reply_to: + self.reply_to = self.event_type_id.default_reply_to + + if self.event_type_id.use_reply_to: + self.twitter_hashtag = self.event_type_id.default_hashtag + + if self.event_type_id.use_timezone: + self.date_tz = self.event_type_id.default_timezone + + self.is_online = self.event_type_id.is_online + + if self.event_type_id.event_type_mail_ids: + self.event_mail_ids = [(5, 0, 0)] + [{ + 'template_id': line.template_id, + 'interval_nbr': line.interval_nbr, + 'interval_unit': line.interval_unit, + 'interval_type': line.interval_type} + for line in self.event_type_id.event_type_mail_ids] @api.constrains('seats_min', 'seats_max', 'seats_availability') def _check_seats_min_max(self): diff --git a/addons/event/models/event_config_settings.py b/addons/event/models/event_config_settings.py index 66207c594e6..f268fcf0f6b 100644 --- a/addons/event/models/event_config_settings.py +++ b/addons/event/models/event_config_settings.py @@ -9,7 +9,6 @@ class event_config_settings(models.TransientModel): module_event_sale = fields.Boolean("Tickets") module_website_event_track = fields.Boolean("Tracks and Agenda") module_website_event_questions = fields.Boolean("Registration Survey") - default_auto_confirmation = fields.Boolean("Auto Confirmation", default_model='event.config.settings') module_event_barcode = fields.Boolean("Barcode") module_website_event = fields.Boolean("Online Events") module_website_event_sale = fields.Boolean("Online Ticketing") diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py index c8ba8f8b40d..9c462c9abe7 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -15,6 +15,32 @@ _INTERVALS = { } +class EventTypeMail(models.Model): + """ Template of event.mail to attach to event.type. Those will be copied + upon all events created in that type to ease event creation. """ + _name = 'event.type.mail' + _description = 'Mail Scheduling on Event Type' + + event_type_id = fields.Many2one( + 'event.type', string='Event Type', + ondelete='cascade', required=True) + interval_nbr = fields.Integer('Interval', default=1) + interval_unit = fields.Selection([ + ('now', 'Immediately'), + ('hours', 'Hour(s)'), ('days', 'Day(s)'), + ('weeks', 'Week(s)'), ('months', 'Month(s)')], + string='Unit', default='hours', required=True) + interval_type = fields.Selection([ + ('after_sub', 'After each registration'), + ('before_event', 'Before the event'), + ('after_event', 'After the event')], + string='Trigger', default="before_event", required=True) + template_id = fields.Many2one( + 'mail.template', string='Email Template', + domain=[('model', '=', 'event.registration')], required=True, ondelete='restrict', + help='This field contains the template of the mail that will be automatically sent') + + class EventMailScheduler(models.Model): """ Event automated mailing. This model replaces all existing fields and configuration allowing to send emails on events since Odoo 9. A cron exists @@ -37,7 +63,7 @@ class EventMailScheduler(models.Model): ('after_event', 'After the event')], string='Trigger ', default="before_event", required=True) template_id = fields.Many2one( - 'mail.template', string='Email to Send', + 'mail.template', string='Email Template', domain=[('model', '=', 'event.registration')], required=True, ondelete='restrict', help='This field contains the template of the mail that will be automatically sent') scheduled_date = fields.Datetime('Scheduled Sent Mail', compute='_compute_scheduled_date', store=True) diff --git a/addons/event/security/ir.model.access.csv b/addons/event/security/ir.model.access.csv index d17116d26e9..49d859ad183 100644 --- a/addons/event/security/ir.model.access.csv +++ b/addons/event/security/ir.model.access.csv @@ -10,3 +10,5 @@ access_event_mail,event.mail,model_event_mail,event.group_event_user,1,0,0,0 access_event_mail_manager,event.mail manager,model_event_mail,event.group_event_manager,1,1,1,1 access_event_mail_registration,event.mail.registration,model_event_mail_registration,event.group_event_user,1,0,0,0 access_event_mail_registration_manager,event.mail.registration.manager,model_event_mail_registration,event.group_event_manager,1,1,1,1 +access_event_type_mail_event_user,event.type.mail.event.user,model_event_type_mail,event.group_event_user,1,0,0,0 +access_event_type_mail_event_manager,event.type.mail.event.manager,model_event_type_mail,event.group_event_manager,1,1,1,1 diff --git a/addons/event/tests/test_event_flow.py b/addons/event/tests/test_event_flow.py index 12086101a16..a351fa584a9 100644 --- a/addons/event/tests/test_event_flow.py +++ b/addons/event/tests/test_event_flow.py @@ -15,14 +15,10 @@ class TestEventFlow(TestEventCommon): @mute_logger('odoo.addons.base.ir.ir_model', 'odoo.models') def test_00_basic_event_auto_confirm(self): """ Basic event management with auto confirmation """ - event_config = self.env['event.config.settings'].sudo(self.user_eventmanager).create({ - 'default_auto_confirmation': 1 - }) - event_config.execute() - # EventUser creates a new event: ok test_event = self.Event.sudo(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, @@ -66,13 +62,10 @@ class TestEventFlow(TestEventCommon): with self.assertRaises(UserError): test_event.button_cancel() - @mute_logger('odoo.addons.base.ir.ir_model', 'odoo.models') def test_10_advanced_event_flow(self): """ Avanced event flow: no auto confirmation, manage minimum / maximum seats, ... """ - self.env['ir.values'].set_default('event.config.settings', 'default_auto_confirmation', False) - # EventUser creates a new event: ok test_event = self.Event.sudo(self.user_eventmanager).create({ 'name': 'TestEvent', @@ -114,7 +107,6 @@ class TestEventFlow(TestEventCommon): ]}) with self.assertRaises(AccessError): event_config = self.env['event.config.settings'].sudo(self.user_eventmanager).create({ - 'default_auto_confirmation': 1 }) event_config.execute() diff --git a/addons/event/tests/test_mail_schedule.py b/addons/event/tests/test_mail_schedule.py index 6cbeeded7f2..7ada77218f3 100644 --- a/addons/event/tests/test_mail_schedule.py +++ b/addons/event/tests/test_mail_schedule.py @@ -13,13 +13,13 @@ class TestMailSchedule(TestEventCommon): @mute_logger('odoo.addons.base.ir.ir_model', 'odoo.models') def test_00_event_mail_schedule(self): """ Test mail scheduling for events """ - self.env['ir.values'].set_default('event.config.settings', 'default_auto_confirmation', True) now = fields.datetime.now() event_date_begin = now + relativedelta(days=1) event_date_end = now + relativedelta(days=3) test_event = self.Event.sudo(self.user_eventmanager).create({ 'name': 'TestEventMail', + 'auto_confirm': True, 'date_begin': event_date_begin, 'date_end': event_date_end, 'seats_max': 10, diff --git a/addons/event/views/event_config_settings_views.xml b/addons/event/views/event_config_settings_views.xml index 67601b1274d..aba1ec2f607 100644 --- a/addons/event/views/event_config_settings_views.xml +++ b/addons/event/views/event_config_settings_views.xml @@ -49,7 +49,7 @@ -
+
@@ -60,17 +60,6 @@
-
-
- -
-
-
-

Attendance

diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index b7ae32f62ad..66208b3da65 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -12,13 +12,134 @@ event.type
- - - - - - - + +
+
+

Location

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+

Communication

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+

Visibility

+
+
+
+ +
+
+
+
+
+

Attendees

+
+
+
+ +
+
+
+
+
@@ -93,17 +214,23 @@
- + + diff --git a/addons/event_sale/models/event.py b/addons/event_sale/models/event.py index fb2d843c349..6fadf6ce756 100644 --- a/addons/event_sale/models/event.py +++ b/addons/event_sale/models/event.py @@ -2,26 +2,56 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError, UserError import odoo.addons.decimal_precision as dp +class EventType(models.Model): + _inherit = 'event.type' + + @api.model + def _get_default_event_ticket_ids(self): + product = self.env.ref('event_sale.product_product_event', raise_if_not_found=False) + if not product: + return False + return [(0, 0, { + 'name': _('Registration'), + 'product_id': product.id, + 'price': 0, + })] + + use_ticketing = fields.Boolean('Ticketing') + event_ticket_ids = fields.One2many( + 'event.event.ticket', 'event_type_id', + string='Tickets', default=_get_default_event_ticket_ids) + + @api.onchange('name') + def _onchange_name(self): + if self.name: + self.event_ticket_ids.filtered(lambda ticket: ticket.name == _('Registration')).update({ + 'name': _('Registration for %s') % self.name + }) + + class Event(models.Model): _inherit = 'event.event' - def _default_tickets(self): - product = self.env.ref('event_sale.product_product_event', raise_if_not_found=False) - if not product: - return self.env['event.event.ticket'] - return [{ - 'name': _('Registration'), - 'product_id': product.id, - 'price': 0, - }] + event_ticket_ids = fields.One2many( + 'event.event.ticket', 'event_id', string='Event Ticket', + copy=True) - event_ticket_ids = fields.One2many('event.event.ticket', 'event_id', string='Event Ticket', - default=lambda self: self._default_tickets(), copy=True) + @api.onchange('event_type_id') + def _onchange_type(self): + super(Event, self)._onchange_type() + if self.event_type_id.use_ticketing: + self.event_ticket_ids = [(5, 0, 0)] + [ + (0, 0, { + 'name': self.name and _('Registration for %s') % self.name or ticket.name, + 'product_id': ticket.product_id.id, + 'price': ticket.price, + }) + for ticket in self.event_type_id.event_ticket_ids] class EventTicket(models.Model): @@ -32,7 +62,8 @@ class EventTicket(models.Model): return self.env.ref('event_sale.product_product_event', raise_if_not_found=False) name = fields.Char(string='Name', required=True, translate=True) - event_id = fields.Many2one('event.event', string="Event", required=True, ondelete='cascade') + event_type_id = fields.Many2one('event.type', string='Event Category', ondelete='cascade') + event_id = fields.Many2one('event.event', string="Event", ondelete='cascade') product_id = fields.Many2one('product.product', string='Product', required=True, domain=[("event_ok", "=", True)], default=_default_product_id) @@ -113,6 +144,11 @@ class EventTicket(models.Model): if record.seats_max and record.seats_available < 0: raise ValidationError(_('No more available seats for the ticket')) + @api.constrains('event_type_id', 'event_id') + def _constrains_event(self): + if any(ticket.event_type_id and ticket.event_id for ticket in self): + raise UserError(_('Ticket should belong to either event category or event but not both')) + @api.onchange('product_id') def _onchange_product_id(self): self.price = self.product_id.list_price or 0 diff --git a/addons/event_sale/views/event_views.xml b/addons/event_sale/views/event_views.xml index f706ee317e6..32027519042 100644 --- a/addons/event_sale/views/event_views.xml +++ b/addons/event_sale/views/event_views.xml @@ -41,6 +41,35 @@ + + event.type.view.form.inherit.sale + event.type + + +
+
+
+ +
+
+
+
+
+
+
+ event.form.inherit event.event diff --git a/addons/website_event/models/event.py b/addons/website_event/models/event.py index b6596223e3b..640eb9a3121 100644 --- a/addons/website_event/models/event.py +++ b/addons/website_event/models/event.py @@ -6,14 +6,18 @@ from odoo import api, fields, models, _ from odoo.addons.website.models.website import slug +class EventType(models.Model): + _name = 'event.type' + _inherit = ['event.type'] + + website_menu = fields.Boolean( + 'Display a dedicated menu on Website') + + class Event(models.Model): _name = 'event.event' _inherit = ['event.event', 'website.seo.metadata', 'website.published.mixin'] - def _default_hashtag(self): - return re.sub("[- \\.\\(\\)\\@\\#\\&]+", "", self.env.user.company_id.name).lower() - - twitter_hashtag = fields.Char('Twitter Hashtag', default=_default_hashtag) website_published = fields.Boolean(track_visibility='onchange') website_message_ids = fields.One2many( 'mail.message', 'res_id', @@ -47,6 +51,12 @@ class Event(models.Model): if event.id: # avoid to perform a slug on a not yet saved record in case of an onchange. event.website_url = '/event/%s' % slug(event) + @api.onchange('event_type_id') + def _onchange_type(self): + super(Event, self)._onchange_type() + if self.event_type_id: + self.website_menu = self.event_type_id.website_menu + def _get_menu_entries(self): self.ensure_one() return [ diff --git a/addons/website_event/views/event_config_settings_views.xml b/addons/website_event/views/event_config_settings_views.xml index aa713ad4b68..48da1930637 100644 --- a/addons/website_event/views/event_config_settings_views.xml +++ b/addons/website_event/views/event_config_settings_views.xml @@ -1,12 +1,12 @@ - - event.config.settings.form.inherit.website.event + + event.config.settings.view.form.inherit.website event.config.settings -
+
diff --git a/addons/website_event/views/event_views.xml b/addons/website_event/views/event_views.xml index 61666898621..a59b27ffa5d 100644 --- a/addons/website_event/views/event_views.xml +++ b/addons/website_event/views/event_views.xml @@ -1,8 +1,32 @@ - - event.event.website.form + + event.type.view.form.inherit.website + event.type + + +
+
+
+ +
+
+
+
+
+
+
+ + + event.event.view.form.inherit.website event.event @@ -15,9 +39,10 @@ - - - +