diff --git a/addons/event/__manifest__.py b/addons/event/__manifest__.py index c7da5440836..d8916054b3d 100644 --- a/addons/event/__manifest__.py +++ b/addons/event/__manifest__.py @@ -23,14 +23,13 @@ Key Features 'security/ir.model.access.csv', 'wizard/event_confirm_view.xml', 'views/event_views.xml', - 'report/report_event_registration_view.xml', 'report/event_event_templates.xml', 'report/event_event_reports.xml', + 'data/email_template_data.xml', 'data/event_data.xml', 'views/event_config_settings_views.xml', 'views/event_templates.xml', 'views/res_partner_views.xml', - 'data/email_template_data.xml', ], 'demo': [ 'data/event_demo.xml', diff --git a/addons/event/data/event_data.xml b/addons/event/data/event_data.xml index 54acbe204a3..9d34a70822c 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 @@ -24,5 +21,18 @@ + + Online + + + + + + + Physical Event + + + + diff --git a/addons/event/models/event.py b/addons/event/models/event.py index b07ec585b9f..b25f41b783a 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -11,51 +11,11 @@ from dateutil.relativedelta import relativedelta class EventType(models.Model): - """ Event Type """ _name = 'event.type' - _description = 'Event Type' - - name = fields.Char('Event Type', 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' - - name = fields.Char( - string='Event Name', translate=True, required=True, - readonly=False, states={'done': [('readonly', True)]}) - active = fields.Boolean(default=True, track_visibility="onchange") - user_id = fields.Many2one( - 'res.users', string='Responsible', - default=lambda self: self.env.user, - readonly=False, states={'done': [('readonly', True)]}) - company_id = fields.Many2one( - 'res.company', string='Company', change_default=True, - default=lambda self: self.env['res.company']._company_default_get('event.event'), - required=False, readonly=False, states={'done': [('readonly', True)]}) - organizer_id = fields.Many2one( - 'res.partner', string='Organizer', - default=lambda self: self.env.user.company_id.partner_id) - event_type_id = fields.Many2one( - 'event.type', string='Category', - 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=lambda self: self._default_event_mail_ids(), copy=True) + _description = 'Event Category' @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', @@ -72,6 +32,81 @@ 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)]}) + active = fields.Boolean(default=True) + user_id = fields.Many2one( + 'res.users', string='Responsible', + default=lambda self: self.env.user, + track_visibility="onchange", + readonly=False, states={'done': [('readonly', True)]}) + company_id = fields.Many2one( + 'res.company', string='Company', change_default=True, + default=lambda self: self.env['res.company']._company_default_get('event.event'), + required=False, readonly=False, states={'done': [('readonly', True)]}) + organizer_id = fields.Many2one( + 'res.partner', string='Organizer', + track_visibility="onchange", + default=lambda self: self.env.user.company_id.partner_id) + event_type_id = fields.Many2one( + 'event.type', string='Category', + 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', copy=True) + # Seats and computation seats_max = fields.Integer( string='Maximum Attendees Number', oldname='register_max', @@ -99,6 +134,48 @@ class EventEvent(models.Model): string='Number of Expected Attendees', readonly=True, compute='_compute_seats') + # Registration fields + registration_ids = fields.One2many( + 'event.registration', 'event_id', string='Attendees', + readonly=False, states={'done': [('readonly', True)]}) + # Date fields + date_tz = fields.Selection('_tz_get', string='Timezone', required=True, default=lambda self: self.env.user.tz) + date_begin = fields.Datetime( + string='Start Date', required=True, + track_visibility='onchange', states={'done': [('readonly', True)]}) + date_end = fields.Datetime( + string='End Date', required=True, + track_visibility='onchange', states={'done': [('readonly', True)]}) + date_begin_located = fields.Char(string='Start Date Located', compute='_compute_date_begin_tz') + date_end_located = fields.Char(string='End Date Located', compute='_compute_date_end_tz') + + state = fields.Selection([ + ('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), + ('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') + 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)]}) + # badge fields + badge_front = fields.Html(string='Badge Front') + badge_back = fields.Html(string='Badge Back') + badge_innerleft = fields.Html(string='Badge Inner Left') + badge_innerright = fields.Html(string='Badge Inner Right') + event_logo = fields.Html(string='Event Logo') + @api.multi @api.depends('seats_max', 'registration_ids.state') def _compute_seats(self): @@ -128,21 +205,6 @@ class EventEvent(models.Model): event.seats_available = event.seats_max - (event.seats_reserved + event.seats_used) event.seats_expected = event.seats_unconfirmed + event.seats_reserved + event.seats_used - # Registration fields - registration_ids = fields.One2many( - 'event.registration', 'event_id', string='Attendees', - readonly=False, states={'done': [('readonly', True)]}) - # Date fields - date_tz = fields.Selection('_tz_get', string='Timezone', required=True, default=lambda self: self.env.user.tz) - date_begin = fields.Datetime( - string='Start Date', required=True, - track_visibility='onchange', states={'done': [('readonly', True)]}) - date_end = fields.Datetime( - string='End Date', required=True, - track_visibility='onchange', states={'done': [('readonly', True)]}) - date_begin_located = fields.Char(string='Start Date Located', compute='_compute_date_begin_tz') - date_end_located = fields.Char(string='End Date Located', compute='_compute_date_end_tz') - @api.model def _tz_get(self): return [(x, x) for x in pytz.all_timezones] @@ -163,33 +225,51 @@ class EventEvent(models.Model): else: self.date_end_located = False - state = fields.Selection([ - ('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), - ('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='Confirmation not required', compute='_compute_auto_confirm') + @api.onchange('event_type_id') + def _onchange_type(self): + if self.event_type_id: + self.seats_min = self.event_type_id.default_registration_min + self.seats_max = self.event_type_id.default_registration_max + if self.event_type_id.default_registration_max: + self.seats_availability = 'limited' + + 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): + if any(event.seats_availability == 'limited' and event.seats_min > event.seats_max for event in self): + raise ValidationError(_('Maximum attendees number should be greater than minimum attendees number.')) + + @api.constrains('seats_max', 'seats_available') + def _check_seats_limit(self): + if any(event.seats_availability == 'limited' and event.seats_max and event.seats_available < 0 for event in self): + raise ValidationError(_('No more available seats.')) @api.one - def _compute_auto_confirm(self): - self.auto_confirm = self.env['ir.values'].get_default('event.config.settings', 'default_auto_confirmation') - - 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.") - address_id = fields.Many2one( - 'res.partner', string='Location', default=lambda self: self.env.user.company_id.partner_id, - readonly=False, states={'done': [('readonly', True)]}) - country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True) - description = fields.Html( - string='Description', oldname='note', translate=html_translate, sanitize_attributes=False, - readonly=False, states={'done': [('readonly', True)]}) - # badge fields - badge_front = fields.Html(string='Badge Front') - badge_back = fields.Html(string='Badge Back') - badge_innerleft = fields.Html(string='Badge Inner Left') - badge_innerright = fields.Html(string='Badge Inner Right') - event_logo = fields.Html(string='Event Logo') + @api.constrains('date_begin', 'date_end') + def _check_closing_date(self): + if self.date_end < self.date_begin: + raise ValidationError(_('Closing Date cannot be set before Beginning Date.')) @api.multi @api.depends('name', 'date_begin', 'date_end') @@ -203,18 +283,6 @@ class EventEvent(models.Model): result.append((event.id, '%s (%s)' % (event.name, ' - '.join(dates)))) return result - @api.one - @api.constrains('seats_max', 'seats_available') - def _check_seats_limit(self): - if self.seats_availability == 'limited' and self.seats_max and self.seats_available < 0: - raise ValidationError(_('No more available seats.')) - - @api.one - @api.constrains('date_begin', 'date_end') - def _check_closing_date(self): - if self.date_end < self.date_begin: - raise ValidationError(_('Closing Date cannot be set before Beginning Date.')) - @api.model def create(self, vals): res = super(EventEvent, self).create(vals) @@ -251,22 +319,6 @@ class EventEvent(models.Model): def button_confirm(self): self.state = 'confirm' - @api.onchange('event_type_id') - def _onchange_type(self): - if self.event_type_id: - self.seats_min = self.event_type_id.default_registration_min - self.seats_max = self.event_type_id.default_registration_max - self.reply_to = self.event_type_id.default_reply_to - - @api.multi - def action_event_registration_report(self): - res = self.env['ir.actions.act_window'].for_xml_id('event', 'action_report_event_registration') - res['context'] = { - "search_default_event_id": self.id, - "group_by": ['create_date:day'], - } - return res - @api.one def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: True): for attendee in self.registration_ids.filtered(filter_func): @@ -278,6 +330,7 @@ class EventRegistration(models.Model): _description = 'Attendee' _inherit = ['mail.thread'] _order = 'name, create_date desc' + _mail_mass_mailing = _('Event Attendees') origin = fields.Char( string='Source Document', readonly=True, @@ -361,10 +414,12 @@ class EventRegistration(models.Model): def button_reg_close(self): """ Close Registration """ today = fields.Datetime.now() - if self.event_id.date_begin <= today: + if self.event_id.date_begin <= today and self.event_id.state == 'confirm': self.write({'state': 'done', 'date_closed': today}) + elif self.event_id.state == 'draft': + raise UserError(_("You must wait the event confirmation before doing this action.")) else: - raise UserError(_("You must wait for the starting day of the event to do this action.")) + raise UserError(_("You must wait the event starting day before doing this action.")) @api.one def button_reg_cancel(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 6a5edd12854..9c462c9abe7 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -15,11 +15,39 @@ _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 that periodically checks for mailing to run. """ _name = 'event.mail' + _rec_name = 'event_id' + _description = 'Event Automated Mailing' event_id = fields.Many2one('event.event', string='Event', required=True, ondelete='cascade') sequence = fields.Integer('Display order') @@ -33,9 +61,9 @@ class EventMailScheduler(models.Model): ('after_sub', 'After each registration'), ('before_event', 'Before the event'), ('after_event', 'After the event')], - string='When to Run ', default="before_event", required=True) + 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/report/__init__.py b/addons/event/report/__init__.py index 42c58d25bd9..67dee8c60db 100644 --- a/addons/event/report/__init__.py +++ b/addons/event/report/__init__.py @@ -1,4 +1,2 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - -import report_event_registration diff --git a/addons/event/report/report_event_registration.py b/addons/event/report/report_event_registration.py deleted file mode 100644 index 14f8f33a95a..00000000000 --- a/addons/event/report/report_event_registration.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, models, fields -from odoo import tools - - -class ReportEventRegistration(models.Model): - """Events Analysis""" - _name = "report.event.registration" - _order = 'event_date desc' - _auto = False - - create_date = fields.Datetime('Creation Date', readonly=True) - event_date = fields.Datetime('Event Date', readonly=True) - event_id = fields.Many2one('event.event', 'Event', required=True) - draft_state = fields.Integer(' # No of Draft Registrations') - cancel_state = fields.Integer(' # No of Cancelled Registrations') - confirm_state = fields.Integer(' # No of Confirmed Registrations') - seats_max = fields.Integer('Max Seats') - nbevent = fields.Integer('Number of Events') - nbregistration = fields.Integer('Number of Registrations') - event_type_id = fields.Many2one('event.type', 'Event Type') - registration_state = fields.Selection([('draft', 'Draft'), ('confirm', 'Confirmed'), ('done', 'Attended'), ('cancel', 'Cancelled')], 'Registration State', readonly=True, required=True) - event_state = fields.Selection([('draft', 'Draft'), ('confirm', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Event State', readonly=True, required=True) - user_id = fields.Many2one('res.users', 'Event Responsible', readonly=True) - name_registration = fields.Char('Participant / Contact Name', readonly=True) - company_id = fields.Many2one('res.company', 'Company', readonly=True) - - @api.model_cr - def init(self): - """Initialize the sql view for the event registration """ - tools.drop_view_if_exists(self.env.cr, 'report_event_registration') - - # TOFIX this request won't select events that have no registration - self.env.cr.execute(""" CREATE VIEW report_event_registration AS ( - SELECT - e.id::varchar || '/' || coalesce(r.id::varchar,'') AS id, - e.id AS event_id, - e.user_id AS user_id, - r.name AS name_registration, - r.create_date AS create_date, - e.company_id AS company_id, - e.date_begin AS event_date, - count(r.id) AS nbevent, - count(r.event_id) AS nbregistration, - CASE WHEN r.state IN ('draft') THEN count(r.event_id) ELSE 0 END AS draft_state, - CASE WHEN r.state IN ('open','done') THEN count(r.event_id) ELSE 0 END AS confirm_state, - CASE WHEN r.state IN ('cancel') THEN count(r.event_id) ELSE 0 END AS cancel_state, - e.event_type_id AS event_type_id, - e.seats_max AS seats_max, - e.state AS event_state, - r.state AS registration_state - FROM - event_event e - LEFT JOIN event_registration r ON (e.id=r.event_id) - - GROUP BY - event_id, - r.id, - registration_state, - event_type_id, - e.id, - e.date_begin, - e.user_id, - event_state, - e.company_id, - e.seats_max, - name_registration - ) - """) diff --git a/addons/event/report/report_event_registration_view.xml b/addons/event/report/report_event_registration_view.xml deleted file mode 100644 index f5579c1b0e7..00000000000 --- a/addons/event/report/report_event_registration_view.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - report.event.registration.pivot - report.event.registration - - - - - - - - - - - report.event.registration.graph - report.event.registration - - - - - - - - - - - - report.event.registration.search - report.event.registration - - - - - - - - - - - - - - - - - - - - - - - - - - - Events Analysis - report.event.registration - form - pivot,graph - - {} - - - - - - diff --git a/addons/event/security/event_security.xml b/addons/event/security/event_security.xml index 6605989b3fc..110262928b6 100644 --- a/addons/event/security/event_security.xml +++ b/addons/event/security/event_security.xml @@ -41,16 +41,5 @@ ] - - Event/Report Registration: multi-company - - - ['|', - ('company_id', '=', False), - ('company_id', 'child_of', [user.company_id.id]), - ] - - - diff --git a/addons/event/security/ir.model.access.csv b/addons/event/security/ir.model.access.csv index ac13c583b55..49d859ad183 100644 --- a/addons/event/security/ir.model.access.csv +++ b/addons/event/security/ir.model.access.csv @@ -10,4 +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_report_event_registration,report.event.registration,model_report_event_registration,event.group_event_user,1,1,1,1 \ No newline at end of file +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 6b18bb9d71d..aba1ec2f607 100644 --- a/addons/event/views/event_config_settings_views.xml +++ b/addons/event/views/event_config_settings_views.xml @@ -24,14 +24,14 @@ -
+
-
@@ -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 70a9f8049e6..66208b3da65 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -11,14 +11,135 @@ event.type.form event.type -
- - - - - - - + + +
+
+

Location

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

Communication

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

Visibility

+
+
+
+ +
+
+
+
+
+

Attendees

+
+
+
+ +
+
+
+
+
@@ -28,14 +149,14 @@ event.type.tree event.type - + - Event Types + Event Categories event.type form @@ -54,6 +175,13 @@ + + Events Analysis + event.event + form + pivot,graph + + event.event.form event.event @@ -69,14 +197,16 @@
- @@ -84,44 +214,44 @@
- + - + + - + + + + + - + + + + + - - - - - - - - - - - - - - + @@ -211,8 +341,9 @@ Expected attendees
- Confirmed attendees - (stats) + + Confirmed attendees +
@@ -264,12 +395,23 @@
+ + event.event.view.pivot + event.event + + + + + + + + Events ir.actions.act_window event.event form - kanban,calendar,tree,form + kanban,calendar,tree,form,pivot {"search_default_upcoming":1}

@@ -291,6 +433,12 @@ view_type="form"/> + @@ -328,10 +476,10 @@ + - @@ -423,28 +571,35 @@ event.mail

- - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + +
diff --git a/addons/event_sale/data/event_sale_data.xml b/addons/event_sale/data/event_sale_data.xml index 97cabee1921..106f1cb0299 100644 --- a/addons/event_sale/data/event_sale_data.xml +++ b/addons/event_sale/data/event_sale_data.xml @@ -1,11 +1,6 @@ - - - Generic Events - - 10.0 @@ -17,6 +12,12 @@ service + + Ticketing + + + + diff --git a/addons/event_sale/models/__init__.py b/addons/event_sale/models/__init__.py index 988d9523f9b..a55c87dd5a6 100644 --- a/addons/event_sale/models/__init__.py +++ b/addons/event_sale/models/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import account_invoice import sale_order import product import event diff --git a/addons/event_sale/models/account_invoice.py b/addons/event_sale/models/account_invoice.py new file mode 100644 index 00000000000..b8f4e309bb4 --- /dev/null +++ b/addons/event_sale/models/account_invoice.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + @api.multi + def action_invoice_paid(self): + """ When an invoice linked to a sales order selling registrations is + paid confirm attendees. Attendees should indeed not be confirmed before + full payment. """ + res = super(AccountInvoice, self).action_invoice_paid() + self.mapped('invoice_line_ids.sale_line_ids')._update_registrations(confirm=True) + return res diff --git a/addons/event_sale/models/event.py b/addons/event_sale/models/event.py index 5eba5703918..6fadf6ce756 100644 --- a/addons/event_sale/models/event.py +++ b/addons/event_sale/models/event.py @@ -2,31 +2,59 @@ # 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 Event(models.Model): +class EventType(models.Model): + _inherit = 'event.type' - _inherit = 'event.event' - - def _default_tickets(self): + @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 self.env['event.event.ticket'] - return [{ + return False + return [(0, 0, { 'name': _('Registration'), 'product_id': product.id, 'price': 0, - }] + })] - event_ticket_ids = fields.One2many('event.event.ticket', 'event_id', string='Event Ticket', - default=lambda self: self._default_tickets(), copy=True) + 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' + + event_ticket_ids = fields.One2many( + 'event.event.ticket', 'event_id', string='Event Ticket', + 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): - _name = 'event.event.ticket' _description = 'Event Ticket' @@ -34,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) @@ -115,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/models/sale_order.py b/addons/event_sale/models/sale_order.py index d917254fb34..ec9bbe2ffae 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -10,7 +10,7 @@ class SaleOrder(models.Model): def action_confirm(self): self.ensure_one() res = super(SaleOrder, self).action_confirm() - self.order_line._update_registrations(confirm=True) + self.order_line._update_registrations(confirm=False, cancel_to_draft=False) if any(self.order_line.filtered(lambda line: line.event_id)): return self.env['ir.actions.act_window'].with_context(default_sale_order_id=self.id).for_xml_id('event_sale', 'action_sale_order_event_registration') return res @@ -35,7 +35,7 @@ class SaleOrderLine(models.Model): return res @api.multi - def _update_registrations(self, confirm=True, registration_data=None): + def _update_registrations(self, confirm=True, cancel_to_draft=False, registration_data=None): """ Create or update registrations linked to a sales order line. A sale order line has a product_uom_qty attribute that will be the number of registrations linked to this line. This method update existing registrations @@ -46,7 +46,7 @@ class SaleOrderLine(models.Model): existing_registrations = registrations.filtered(lambda self: self.sale_order_line_id.id == so_line.id) if confirm: existing_registrations.filtered(lambda self: self.state != 'open').confirm_registration() - else: + if cancel_to_draft: existing_registrations.filtered(lambda self: self.state == 'cancel').do_draft() for count in range(int(so_line.product_uom_qty) - len(existing_registrations)): diff --git a/addons/event_sale/views/event_views.xml b/addons/event_sale/views/event_views.xml index 64e2ec9caa4..32027519042 100644 --- a/addons/event_sale/views/event_views.xml +++ b/addons/event_sale/views/event_views.xml @@ -41,34 +41,55 @@ + + event.type.view.form.inherit.sale + event.type + + +
+
+
+ +
+
+
+
+
+
+
+ event.form.inherit event.event - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/addons/event_sale/wizard/event_edit_registration.py b/addons/event_sale/wizard/event_edit_registration.py index 0ae579bc693..f03623d91ad 100644 --- a/addons/event_sale/wizard/event_edit_registration.py +++ b/addons/event_sale/wizard/event_edit_registration.py @@ -55,7 +55,7 @@ class RegistrationEditor(models.TransientModel): self.env['event.registration'].create(values) if self.env.context.get('active_model') == 'sale.order': for order in self.env['sale.order'].browse(self.env.context.get('active_ids', [])): - order.order_line._update_registrations(confirm=True) + order.order_line._update_registrations(confirm=False) return {'type': 'ir.actions.act_window_close'} diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 06918b75551..54921dbb88c 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -734,6 +734,8 @@ class MailThread(models.AbstractModel): recipient_ids.add(record.partner_id.id) elif 'email_from' in self._fields and record.email_from: email_to = record.email_from + elif 'partner_email' in self._fields and record.partner_email: + email_to = record.partner_email elif 'email' in self._fields: email_to = record.email res[record.id] = {'partner_ids': list(recipient_ids), 'email_to': email_to, 'email_cc': email_cc} diff --git a/addons/website_event/README.md b/addons/website_event/README.md index 862c44a685c..e4691d2619b 100644 --- a/addons/website_event/README.md +++ b/addons/website_event/README.md @@ -10,7 +10,7 @@ Create Awesome Event Pages ### Get rid of old WYSIWYG editors -Create beautiful event pages by drag & droping well designed *'Building Blocks'*. Publish event photos, speakers, agenda, etc. +Create beautiful event pages by drag & droping well designed *'Building Blocks'*. Publish event photos, speakers, schedule, etc. Odoo's unique *'edit inline'* approach makes website creation surprisingly easy. "Want to introduce a speaker? to change the price of a ticket? to update a banner? promote sponsors?" just click and change. diff --git a/addons/website_event/__manifest__.py b/addons/website_event/__manifest__.py index 13d42d483be..6811d586e16 100644 --- a/addons/website_event/__manifest__.py +++ b/addons/website_event/__manifest__.py @@ -4,22 +4,22 @@ 'name': 'Online Events', 'category': 'Marketing', 'sequence': 135, - 'summary': 'Schedule, Promote and Sell Events', + 'summary': 'Publish Events and Manage Online Registrations on your Website', 'website': 'https://www.odoo.com/page/website-builder', 'description': """ Online Events """, 'depends': ['website', 'website_partner', 'website_mail', 'event'], 'data': [ - 'data/website_event_data.xml', + 'data/event_data.xml', 'views/event_config_settings_views.xml', - 'views/website_event_templates.xml', - 'views/website_event_views.xml', + 'views/event_templates.xml', + 'views/event_views.xml', 'security/ir.model.access.csv', - 'security/website_event_security.xml', + 'security/event_security.xml', ], 'demo': [ - 'data/website_event_demo.xml' + 'data/event_demo.xml' ], 'application': True, } diff --git a/addons/website_event/data/website_event_data.xml b/addons/website_event/data/event_data.xml similarity index 100% rename from addons/website_event/data/website_event_data.xml rename to addons/website_event/data/event_data.xml diff --git a/addons/website_event/data/website_event_demo.xml b/addons/website_event/data/event_demo.xml similarity index 100% rename from addons/website_event/data/website_event_demo.xml rename to addons/website_event/data/event_demo.xml diff --git a/addons/website_event/models/event.py b/addons/website_event/models/event.py index 6c1e1ddf809..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', @@ -25,9 +29,10 @@ class Event(models.Model): ) is_participating = fields.Boolean("Is Participating", compute="_compute_is_participating") - show_menu = fields.Boolean('Dedicated Menu', compute='_get_show_menu', inverse='_set_show_menu', - help="Creates menus Introduction, Location and Register on the page " - " of the event on the website.", store=True) + website_menu = fields.Boolean( + 'Dedicated Menu', compute='_compute_website_menu', inverse='_set_website_menu', + help="Creates menus Introduction, Location and Register on the page " + " of the event on the website.", store=True) menu_id = fields.Many2one('website.menu', 'Event Menu') def _compute_is_participating(self): @@ -46,46 +51,47 @@ 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.multi - def _get_new_menu_pages(self): - """ Retuns a list of tuple ('Page name', 'relative page url') for the 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() - todo = [ - (_('Introduction'), 'website_event.template_intro'), - (_('Location'), 'website_event.template_location') + return [ + (_('Introduction'), False, 'website_event.template_intro'), + (_('Location'), False, 'website_event.template_location'), + (_('Register'), '/event/%s/register' % slug(self), False), ] - result = [] - for name, path in todo: - complete_name = name + ' ' + self.name - newpath = self.env['website'].new_page(complete_name, path, ispage=False) - url = "/event/" + slug(self) + "/page/" + newpath - result.append((name, url)) - result.append((_('Register'), '/event/%s/register' % slug(self))) - return result @api.multi - def _set_show_menu(self): + def _set_website_menu(self): for event in self: - if event.menu_id and not event.show_menu: + if event.menu_id and not event.website_menu: event.menu_id.unlink() - elif event.show_menu and not event.menu_id: - root_menu = self.env['website.menu'].create({'name': event.name}) - to_create_menus = event._get_new_menu_pages() - seq = 0 - for name, url in to_create_menus: - self.env['website.menu'].create({ - 'name': name, - 'url': url, - 'parent_id': root_menu.id, - 'sequence': seq, - }) - seq += 1 - event.menu_id = root_menu + elif event.website_menu: + if not event.menu_id: + root_menu = self.env['website.menu'].create({'name': event.name}) + event.menu_id = root_menu + + for sequence, (name, url, xml_id) in enumerate(self._get_menu_entries()): + existing_pages = event.menu_id.child_id.mapped('name') + if name not in existing_pages: + if not url: + newpath = self.env['website'].new_page(name + ' ' + self.name, xml_id, ispage=False) + url = "/event/" + slug(self) + "/page/" + newpath + self.env['website.menu'].create({ + 'name': name, + 'url': url, + 'parent_id': event.menu_id.id, + 'sequence': sequence, + }) @api.multi - def _get_show_menu(self): + def _compute_website_menu(self): for event in self: - event.show_menu = bool(event.menu_id) + event.website_menu = bool(event.menu_id) @api.multi def google_map_img(self, zoom=8, width=298, height=298): diff --git a/addons/website_event/security/website_event_security.xml b/addons/website_event/security/event_security.xml similarity index 100% rename from addons/website_event/security/website_event_security.xml rename to addons/website_event/security/event_security.xml diff --git a/addons/website_event/static/description/index.html b/addons/website_event/static/description/index.html index 8ef92f0377c..5648a814172 100644 --- a/addons/website_event/static/description/index.html +++ b/addons/website_event/static/description/index.html @@ -1,7 +1,7 @@

Create your own events

-

Organize, publish, promote and sell.

+

Organize, publish and promote.

@@ -15,7 +15,7 @@

Host events that resonates with your audience

Easily create live or online events

- One platform to manage it all. Odoo Events covers all aspects of an event manager's work: organization, visibility, promotion and sale. Use it with any kind of event, including conferences, meetings, seminars, training and webinars. + One platform to manage it all. Odoo Events covers all aspects of an event manager's work: organization, visibility, promotion. Use it with any kind of event, including conferences, meetings, seminars, training and webinars.

@@ -51,7 +51,7 @@
-

Automatically display your event's agenda in a clear, classy design on your website.

+

Automatically display your event's schedule in a clear, classy design on your website.

@@ -69,9 +69,9 @@

Get sponsors to come to you

-

Sell sponsorship, promote your sponsors

+

Promote your sponsors

- Easily manage existing and new sponsors with Odoo Events. Add sponsors to your events and publish sponsors per level (e.g., gold) on the bottom of every page of the event. Sell sponsorship packages online through Odoo eCommerce.

+ Easily manage existing and new sponsors with Odoo Events. Add sponsors to your events and publish sponsors per level (e.g., gold) on the bottom of every page of the event.

@@ -83,22 +83,16 @@
-
+
+

Total promotion tools

+

Promote events efficiently on all channels

- -

Total promotion tools

-

Promote events efficiently on all channels

-

- Segment your audience, leverage social media and email marketing to promote your events to the right audience. Odoo Events' smooth integration with countless Odoo apps simplifies promotion to the extreme. Setup automated campaigns for attendees, or automatically send timely emails to registrants. Optimize conversions with ad-hoc landing pages for your different marketing campaigns. -

+
- -

Sell tickets online

-

Automate the registration and payment process

- Manage registrations and sell tickets online to your events. Choose whether it is a free event or whether attendees need to register for a fee. Pick the paying method -pay online with a credit card or with the invoice. Define special conditions such as early-bird prices, member benefits or extra services with multiple tickets. All you need to boost your event attendance. + Segment your audience, leverage social media and email marketing to promote your events to the right audience. Odoo Events' smooth integration with countless Odoo apps simplifies promotion to the extreme. Setup automated campaigns for attendees, or automatically send timely emails to registrants. Optimize conversions with ad-hoc landing pages for your different marketing campaigns.

diff --git a/addons/website_event/static/src/js/website_event.js b/addons/website_event/static/src/js/website_event.js index cb531d0035a..2df7642c1a3 100644 --- a/addons/website_event/static/src/js/website_event.js +++ b/addons/website_event/static/src/js/website_event.js @@ -26,13 +26,21 @@ var EventRegistrationForm = Widget.extend({ $("#registration_form select").each(function() { post[$(this).attr('name')] = $(this).val(); }); - return ajax.jsonRpc($form.attr('action'), 'call', post).then(function (modal) { - var $modal = $(modal); - $modal.appendTo($form).modal(); - $modal.on('click', '.js_goto_event', function () { - $modal.modal('hide'); + var tickets_ordered = _.some(_.map(post, function(value, key) { return parseInt(value) })); + if (! tickets_ordered) { + return $('#registration_form table').after( + '
Please select at least one ticket.
' + ); + } + else { + return ajax.jsonRpc($form.attr('action'), 'call', post).then(function (modal) { + var $modal = $(modal); + $modal.appendTo($form).modal(); + $modal.on('click', '.js_goto_event', function () { + $modal.modal('hide'); + }); }); - }); + } }, }); 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/website_event_templates.xml b/addons/website_event/views/event_templates.xml similarity index 99% rename from addons/website_event/views/website_event_templates.xml rename to addons/website_event/views/event_templates.xml index ea714ef45b5..a06e81e5ccb 100644 --- a/addons/website_event/views/website_event_templates.xml +++ b/addons/website_event/views/event_templates.xml @@ -438,7 +438,12 @@ Sold Out diff --git a/addons/website_event/views/event_views.xml b/addons/website_event/views/event_views.xml new file mode 100644 index 00000000000..a59b27ffa5d --- /dev/null +++ b/addons/website_event/views/event_views.xml @@ -0,0 +1,60 @@ + + + + + event.type.view.form.inherit.website + event.type + + +
+
+
+ +
+
+
+
+
+
+
+ + + event.event.view.form.inherit.website + event.event + + + + +