From 22351763477969b415c7403fca260a43a68baac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 20 Feb 2020 09:38:46 +0000 Subject: [PATCH 1/8] [MOV] website_event(_track): rename some odd methods and files, remove dead code In order to keep things organized, let us move some models in their own file and rename some test files. Some odd methods are relocated to better follow guidelines. Dead code is removed because we do not like dead code. LINKS Task ID 2089156 Community PR odoo/odoo#42911 --- addons/website_event/models/event_event.py | 36 ++++++++----------- addons/website_event_sale/tests/__init__.py | 2 +- ...est_ui.py => test_frontend_buy_tickets.py} | 1 + addons/website_event_track/models/__init__.py | 1 + .../website_event_track/models/event_event.py | 23 ++++-------- .../models/website_event_menu.py | 13 +++++++ 6 files changed, 38 insertions(+), 38 deletions(-) rename addons/website_event_sale/tests/{test_ui.py => test_frontend_buy_tickets.py} (99%) create mode 100644 addons/website_event_track/models/website_event_menu.py diff --git a/addons/website_event/models/event_event.py b/addons/website_event/models/event_event.py index 491b8b60b17..472a60f802a 100644 --- a/addons/website_event/models/event_event.py +++ b/addons/website_event/models/event_event.py @@ -21,6 +21,11 @@ class Event(models.Model): _name = 'event.event' _inherit = ['event.event', 'website.seo.metadata', 'website.published.multi.mixin', 'website.cover_properties.mixin'] + def _default_cover_properties(self): + res = super()._default_cover_properties() + res['opacity'] = '0.4' + return res + # description subtitle = fields.Char('Event Subtitle', translate=True) # registration @@ -56,9 +61,15 @@ class Event(models.Model): if self.event_type_id: self.website_menu = self.event_type_id.website_menu - def _default_cover_properties(self): - res = super()._default_cover_properties() - res['opacity'] = '0.4' + @api.model + def create(self, vals): + res = super(Event, self).create(vals) + res._update_website_menus(vals) + return res + + def write(self, vals): + res = super(Event, self).write(vals) + self._update_website_menus(vals) return res def _get_menu_entries(self): @@ -71,7 +82,7 @@ class Event(models.Model): (_('Register'), '/event/%s/register' % slug(self), False), ] - def _toggle_create_website_menus(self, vals): + def _update_website_menus(self, vals): for event in self: if 'website_menu' in vals: if event.menu_id and not event.website_menu: @@ -83,17 +94,6 @@ class Event(models.Model): for sequence, (name, url, xml_id) in enumerate(event._get_menu_entries()): event._create_menu(sequence, name, url, xml_id) - @api.model - def create(self, vals): - res = super(Event, self).create(vals) - res._toggle_create_website_menus(vals) - return res - - def write(self, vals): - res = super(Event, self).write(vals) - self._toggle_create_website_menus(vals) - return res - def _create_menu(self, sequence, name, url, xml_id): if not url: newpath = self.env['website'].new_page(name + ' ' + self.name, template=xml_id, ispage=False)['url'] @@ -107,12 +107,6 @@ class Event(models.Model): }) return menu - def google_map_img(self, zoom=8, width=298, height=298): - self.ensure_one() - if self.address_id: - return self.sudo().address_id.google_map_img(zoom=zoom, width=width, height=height) - return None - def google_map_link(self, zoom=8): self.ensure_one() if self.address_id: diff --git a/addons/website_event_sale/tests/__init__.py b/addons/website_event_sale/tests/__init__.py index 2235dde4188..9f8e0cdceb8 100644 --- a/addons/website_event_sale/tests/__init__.py +++ b/addons/website_event_sale/tests/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import test_ui +from . import test_frontend_buy_tickets diff --git a/addons/website_event_sale/tests/test_ui.py b/addons/website_event_sale/tests/test_frontend_buy_tickets.py similarity index 99% rename from addons/website_event_sale/tests/test_ui.py rename to addons/website_event_sale/tests/test_frontend_buy_tickets.py index 60092d1bf09..e4698895587 100644 --- a/addons/website_event_sale/tests/test_ui.py +++ b/addons/website_event_sale/tests/test_frontend_buy_tickets.py @@ -7,6 +7,7 @@ from datetime import timedelta from odoo.addons.base.tests.common import HttpCaseWithUserDemo from odoo.fields import Datetime + @odoo.tests.common.tagged('post_install', '-at_install') class TestUi(HttpCaseWithUserDemo): diff --git a/addons/website_event_track/models/__init__.py b/addons/website_event_track/models/__init__.py index 1a9f0aa1fe0..38501492c72 100644 --- a/addons/website_event_track/models/__init__.py +++ b/addons/website_event_track/models/__init__.py @@ -5,3 +5,4 @@ from . import event_event from . import event_sponsor from . import event_track from . import event_track_stage +from . import website_event_menu diff --git a/addons/website_event_track/models/event_event.py b/addons/website_event_track/models/event_event.py index 1c75e41f857..c98815edaad 100644 --- a/addons/website_event_track/models/event_event.py +++ b/addons/website_event_track/models/event_event.py @@ -18,15 +18,6 @@ class EventType(models.Model): self.website_track_proposal = False -class EventMenu(models.Model): - _name = "website.event.menu" - _description = "Website Event Menu" - - menu_id = fields.Many2one('website.menu', string='Menu', ondelete='cascade') - event_id = fields.Many2one('event.event', string='Event', ondelete='cascade') - menu_type = fields.Selection([('track', 'Event Tracks Menus'), ('track_proposal', 'Event Proposals Menus')]) - - class Event(models.Model): _inherit = "event.event" @@ -59,8 +50,13 @@ class Event(models.Model): for event in self: event.sponsor_count = result.get(event.id, 0) - def _toggle_create_website_menus(self, vals): - super(Event, self)._toggle_create_website_menus(vals) + @api.depends('track_ids.tag_ids', 'track_ids.tag_ids.color') + def _compute_tracks_tag_ids(self): + for event in self: + event.tracks_tag_ids = event.track_ids.mapped('tag_ids').filtered(lambda tag: tag.color != 0).ids + + def _update_website_menus(self, vals): + super(Event, self)._update_website_menus(vals) for event in self: if 'website_track' in vals: if vals['website_track']: @@ -97,11 +93,6 @@ class Event(models.Model): res = [(_('Talk Proposals'), '/event/%s/track_proposal' % slug(self), False, 'track_proposal')] return res - @api.depends('track_ids.tag_ids', 'track_ids.tag_ids.color') - def _compute_tracks_tag_ids(self): - for event in self: - event.tracks_tag_ids = event.track_ids.mapped('tag_ids').filtered(lambda tag: tag.color != 0).ids - @api.onchange('event_type_id') def _onchange_type(self): super(Event, self)._onchange_type() diff --git a/addons/website_event_track/models/website_event_menu.py b/addons/website_event_track/models/website_event_menu.py new file mode 100644 index 00000000000..89ddc9984d1 --- /dev/null +++ b/addons/website_event_track/models/website_event_menu.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class EventMenu(models.Model): + _name = "website.event.menu" + _description = "Website Event Menu" + + menu_id = fields.Many2one('website.menu', string='Menu', ondelete='cascade') + event_id = fields.Many2one('event.event', string='Event', ondelete='cascade') + menu_type = fields.Selection([('track', 'Event Tracks Menus'), ('track_proposal', 'Event Proposals Menus')]) From 5e4efef3ca3f2e9f02ec40c746c0d43aa3e97b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 19 Feb 2020 14:40:37 +0000 Subject: [PATCH 2/8] [REF] event : move from onchange / default to stored editable computed fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PURPOSE Try to move from onchange / default_get to stored editable computed fields. Behavior should be the same (computed or set by user), with support of create / write / onchange field update without additional code. SPECIFICATIONS: GLOBAL RULES Update classic fields updated in some cases by onchange and/or default methods by fields with store=True, readonly=False. It means their value comes either from manual user input, either from trigger based computation. Remove onchange and default_get when possible, leading to an unique computation method and clearing fields definition. Also clean some fields definition inconsistencies, notably required fields that should instead be correctly computed or default that have no real meaning. SPECIFICATIONS: REQUIRED FIELDS As computed fields are computed after create required attribute cannot be respected without computing them beforehand. That is why we have some custom code to compute required fields if not given at create and update the creation values accordingly. SPECIFICATIONS: MAIL SCHEDULING Mail scheduling on event type is modified in this commit. Previously checking the use_mail_schedule radio button had no effect on event_type_mail_ids field. It is now reset if unchecked. It is therefore coherent with use_ticket and event_type_ticket_ids field behavior. LINKS Task ID 2089156 Community PR odoo/odoo#42911 Co-Authored-By: Thibault Delavallée Co-Authored-By: Michaël Mattiello --- addons/event/models/event_event.py | 197 ++++++++++++------ addons/event/models/event_registration.py | 64 +++--- addons/event/tests/common.py | 2 +- addons/event/tests/test_event_flow.py | 5 +- addons/event/tests/test_event_internals.py | 27 +-- .../event/tests/test_event_mail_schedule.py | 1 - addons/event/tests/test_event_security.py | 6 +- .../event_sale/tests/test_event_internals.py | 3 +- addons/website_event/models/event_event.py | 21 +- .../models/event_event.py | 29 +-- .../tests/test_event_internals.py | 2 +- .../tests/test_frontend_buy_tickets.py | 8 +- 12 files changed, 218 insertions(+), 147 deletions(-) diff --git a/addons/event/models/event_event.py b/addons/event/models/event_event.py index e5f89ecf884..a1fd7ae8df7 100644 --- a/addons/event/models/event_event.py +++ b/addons/event/models/event_event.py @@ -24,21 +24,6 @@ class EventType(models.Model): _description = 'Event Category' _order = 'sequence, id' - @api.model - def _get_default_event_type_mail_ids(self): - return [(0, 0, { - 'notification_type': 'mail', - 'interval_unit': 'now', - 'interval_type': 'after_sub', - 'template_id': self.env.ref('event.event_subscription').id, - }), (0, 0, { - 'notification_type': 'mail', - 'interval_nbr': 10, - 'interval_unit': 'days', - 'interval_type': 'before_event', - 'template_id': self.env.ref('event.event_reminder').id, - })] - name = fields.Char('Event Category', required=True, translate=True) sequence = fields.Integer() # tickets @@ -50,7 +35,8 @@ class EventType(models.Model): # registration has_seats_limitation = fields.Boolean('Limited Seats') default_registration_max = fields.Integer( - 'Maximum Registrations', default=0, + 'Maximum Registrations', compute='_compute_default_registration', + copy=True, readonly=False, store=True, help="It will select this default maximum value when you choose this event") auto_confirm = fields.Boolean( 'Automatically Confirm Registrations', default=True, @@ -66,9 +52,28 @@ class EventType(models.Model): 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=lambda self: self._get_default_event_type_mail_ids()) + 'event.type.mail', 'event_type_id', + string='Mail Schedule', compute='_compute_event_type_mail_ids', + readonly=False, store=True) + + @api.depends('use_mail_schedule') + def _compute_event_type_mail_ids(self): + for template in self: + if not template.use_mail_schedule: + template.event_type_mail_ids = [(5, 0)] + elif not template.event_type_mail_ids: + template.event_type_mail_ids = [(0, 0, { + 'notification_type': 'mail', + 'interval_unit': 'now', + 'interval_type': 'after_sub', + 'template_id': self.env.ref('event.event_subscription').id, + }), (0, 0, { + 'notification_type': 'mail', + 'interval_nbr': 10, + 'interval_unit': 'days', + 'interval_type': 'before_event', + 'template_id': self.env.ref('event.event_reminder').id, + })] @api.depends('use_ticket') def _compute_event_type_ticket_ids(self): @@ -80,10 +85,11 @@ class EventType(models.Model): 'name': _('Registration'), })] - @api.onchange('has_seats_limitation') - def _onchange_has_seats_limitation(self): - if not self.has_seats_limitation: - self.default_registration_max = 0 + @api.depends('has_seats_limitation') + def _compute_default_registration(self): + for template in self: + if not template.has_seats_limitation: + template.default_registration_max = 0 class EventEvent(models.Model): @@ -114,10 +120,14 @@ class EventEvent(models.Model): domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") event_type_id = fields.Many2one('event.type', string='Category', ondelete='set null') color = fields.Integer('Kanban Color Index') - event_mail_ids = fields.One2many('event.mail', 'event_id', string='Mail Schedule', copy=True) + event_mail_ids = fields.One2many( + 'event.mail', 'event_id', string='Mail Schedule', copy=True, + compute='_compute_from_event_type', readonly=False, store=True) # Kanban fields kanban_state = fields.Selection([('normal', 'In Progress'), ('done', 'Done'), ('blocked', 'Blocked')], default='normal') - kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State Label', tracking=True, store=True) + kanban_state_label = fields.Char( + string='Kanban State Label', compute='_compute_kanban_state_label', + store=True, tracking=True) stage_id = fields.Many2one( 'event.stage', ondelete='restrict', default=_get_default_stage_id, group_expand='_read_group_stage_ids', tracking=True) @@ -127,10 +137,12 @@ class EventEvent(models.Model): # Seats and computation seats_max = fields.Integer( string='Maximum Attendees Number', + compute='_compute_from_event_type', copy=True, readonly=False, store=True, help="For each event you can define a maximum registration of seats(number of attendees), above this numbers the registrations are not accepted.") seats_availability = fields.Selection( [('unlimited', 'Unlimited'), ('limited', 'Limited')], - 'Maximum Attendees', required=True, default='unlimited') + string='Maximum Attendees', required=True, + compute='_compute_seats_availability', copy=True, readonly=False, store=True) seats_reserved = fields.Integer( string='Reserved Seats', store=True, readonly=True, compute='_compute_seats') @@ -147,16 +159,18 @@ class EventEvent(models.Model): string='Number of Expected Attendees', compute_sudo=True, readonly=True, compute='_compute_seats') # Registration fields - auto_confirm = fields.Boolean(string='Autoconfirm Registrations') + auto_confirm = fields.Boolean( + string='Autoconfirm Registrations', + compute='_compute_from_event_type', copy=True, readonly=False, store=True) registration_ids = fields.One2many('event.registration', 'event_id', string='Attendees') event_registrations_open = fields.Boolean('Registration open', compute='_compute_event_registrations_open') event_ticket_ids = fields.One2many( - 'event.event.ticket', 'event_id', string='Event Ticket', - copy=True) + 'event.event.ticket', 'event_id', string='Event Ticket', copy=True, + compute='_compute_from_event_type', readonly=False, store=True) # Date fields date_tz = fields.Selection( _tz_get, string='Timezone', required=True, - default=lambda self: self.env.user.tz or 'UTC') + compute='_compute_date_tz', copy=True, readonly=False, store=True) date_begin = fields.Datetime(string='Start Date', required=True, tracking=True) date_end = fields.Datetime(string='End Date', required=True, tracking=True) date_begin_located = fields.Char(string='Start Date Located', compute='_compute_date_begin_tz') @@ -165,12 +179,16 @@ class EventEvent(models.Model): is_one_day = fields.Boolean(compute='_compute_field_is_one_day') start_sale_date = fields.Date('Start sale date', compute='_compute_start_sale_date') # Location and communication - is_online = fields.Boolean('Online Event') + is_online = fields.Boolean( + string='Online Event', compute='_compute_from_event_type', + copy=True, readonly=False, store=True) address_id = fields.Many2one( - 'res.partner', string='Venue', tracking=True, - default=lambda self: self.env.company.partner_id, + 'res.partner', string='Venue', compute='_compute_address_id', + copy=True, readonly=False, store=True, tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") - country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True, readonly=False) + country_id = fields.Many2one( + 'res.country', 'Country', related='address_id.country_id', + copy=True, readonly=False, store=True) # badge fields badge_front = fields.Html(string='Badge Front') badge_back = fields.Html(string='Badge Back') @@ -282,47 +300,86 @@ class EventEvent(models.Model): start_dates = [ticket.start_sale_date for ticket in event.event_ticket_ids if ticket.start_sale_date] event.start_sale_date = min(start_dates) if start_dates else False - @api.onchange('is_online') - def _onchange_is_online(self): - if self.is_online: - self.address_id = False + @api.depends('is_online') + def _compute_address_id(self): + for event in self: + if event.is_online: + event.address_id = False + elif not event.address_id: + event.address_id = self.env.company.partner_id.id - @api.onchange('event_type_id') - def _onchange_type(self): - if self.event_type_id: - self.seats_max = self.event_type_id.default_registration_max - if self.event_type_id.default_registration_max: - self.seats_availability = 'limited' + @api.depends('event_type_id') + def _compute_date_tz(self): + for event in self: + if event.event_type_id.use_timezone: + event.date_tz = event.event_type_id.default_timezone + if not event.date_tz: + event.date_tz = self.env.user.tz or 'UTC' - if self.event_type_id.auto_confirm: - self.auto_confirm = self.event_type_id.auto_confirm + @api.depends('event_type_id') + def _compute_seats_availability(self): + """ Make it separate from ``_compute_from_event_type`` because otherwise + a value given at create (see create override) would protect all other fields + depending on event type id from being computed as compute method will be + blacklisted during create (see ``_field_computed`` attribute used in create + to compute protected field from re-computation) """ + for event in self: + if event.event_type_id.default_registration_max: + event.seats_availability = 'limited' + if not event.seats_availability: + event.seats_availability = 'unlimited' - if self.event_type_id.use_timezone: - self.date_tz = self.event_type_id.default_timezone + @api.depends('event_type_id') + def _compute_from_event_type(self): + """ Update event configuration from its event type. Depends are set only + on event_type_id itself, not its sub fields. Indeed purpose is to emulate + an onchange: if event type is changed, update event configuration. Changing + event type content itself should not trigger this method. - self.is_online = self.event_type_id.is_online + Updated by this method + * seats_max -> triggers _compute_seats (all seats computation) + * auto_confirm + * is_online -> triggers _compute_address_id (address_id computation) + * event_mail_ids + * event_ticket_ids -> triggers _compute_start_sale_date (start_sale_date computation) + """ + for event in self: + if not event.event_type_id: + if not event.seats_max: + event.seats_max = 0 + if not event.is_online: + event.is_online = False + if not event.event_ticket_ids: + event.event_ticket_ids = False + continue - if self.event_type_id.use_mail_schedule and self.event_type_id.event_type_mail_ids: - self.event_mail_ids = [(5, 0, 0)] + [ + if event.event_type_id.default_registration_max: + event.seats_max = event.event_type_id.default_registration_max + + if event.event_type_id.auto_confirm: + event.auto_confirm = event.event_type_id.auto_confirm + + event.is_online = event.event_type_id.is_online + + # compute mailing information (force only if activated and mailing defined) + if event.event_type_id.use_mail_schedule and event.event_type_id.event_type_mail_ids: + event.event_mail_ids = [(5, 0, 0)] + [ (0, 0, { attribute_name: line[attribute_name] if not isinstance(line[attribute_name], models.BaseModel) else line[attribute_name].id for attribute_name in self.env['event.type.mail']._get_event_mail_fields_whitelist() }) - for line in self.event_type_id.event_type_mail_ids] + for line in event.event_type_id.event_type_mail_ids] - # compute tickets information - if self.event_type_id.use_ticket: - all_ticket_values = [] - for ticket in self.event_type_id.event_type_ticket_ids: - ticket_vals = dict( - (attribute_name, ticket[attribute_name] if not isinstance(ticket[attribute_name], models.BaseModel) else ticket[attribute_name].id) + # compute tickets information (force only if activated and tickets defined) + if event.event_type_id.use_ticket and event.event_type_id.event_type_ticket_ids: + event.event_ticket_ids = [(5, 0, 0)] + [ + (0, 0, { + attribute_name: line[attribute_name] if not isinstance(line[attribute_name], models.BaseModel) else line[attribute_name].id for attribute_name in self.env['event.type.ticket']._get_event_ticket_fields_whitelist() - ) - all_ticket_values.append(ticket_vals) + }) + for line in event.event_type_id.event_type_ticket_ids] - self.event_ticket_ids = [(5, 0, 0)] + [(0, 0, item) for item in all_ticket_values] - - @api.constrains('seats_max', 'seats_available') + @api.constrains('seats_max', 'seats_available', 'seats_availability') 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.')) @@ -350,6 +407,9 @@ class EventEvent(models.Model): @api.model def create(self, vals): + # Temporary fix for ``seats_availability`` and ``date_tz`` required fields (see ``_compute_from_event_type`` + vals.update(self._sync_required_computed(vals)) + res = super(EventEvent, self).create(vals) if res.organizer_id: res.message_subscribe([res.organizer_id.id]) @@ -367,6 +427,17 @@ class EventEvent(models.Model): default = dict(default or {}, name=_("%s (copy)") % (self.name)) return super(EventEvent, self).copy(default) + def _sync_required_computed(self, values): + """ Call compute fields in cache to find missing values for required fields + (seats_availability and date_tz) in case they are not given in values """ + missing_fields = list(set(['seats_availability', 'date_tz']).difference(set(values.keys()))) + if missing_fields and values: + cache_event = self.new(values) + cache_event._compute_from_event_type() + return dict((fname, cache_event[fname]) for fname in missing_fields) + else: + return {} + def action_set_done(self): """ Action which will move the events diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index 5f6c32a620b..b9d88dab020 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -29,13 +29,17 @@ class EventRegistration(models.Model): partner_id = fields.Many2one( 'res.partner', string='Contact', states={'done': [('readonly', True)]}) - name = fields.Char(string='Attendee Name', index=True, required=True, tracking=True) - email = fields.Char(string='Email') - phone = fields.Char(string='Phone') - mobile = fields.Char(string='Mobile') + name = fields.Char( + string='Attendee Name', index=True, + compute='_compute_contact_info', copy=True, readonly=False, store=True, tracking=True) + email = fields.Char(string='Email', compute='_compute_contact_info', copy=True, readonly=False, store=True) + phone = fields.Char(string='Phone', compute='_compute_contact_info', copy=True, readonly=False, store=True) + mobile = fields.Char(string='Mobile', compute='_compute_contact_info', copy=True, readonly=False, store=True) # organization date_open = fields.Datetime(string='Registration Date', readonly=True, default=lambda self: fields.Datetime.now()) # weird crash is directly now - date_closed = fields.Datetime(string='Attended Date', readonly=True) + date_closed = fields.Datetime( + string='Attended Date', compute='_compute_date_closed', + copy=True, readonly=False, store=True) event_begin_date = fields.Datetime(string="Event Start Date", related='event_id.date_begin', readonly=True) event_end_date = fields.Datetime(string="Event End Date", related='event_id.date_end', readonly=True) company_id = fields.Many2one( @@ -46,11 +50,23 @@ class EventRegistration(models.Model): ('open', 'Confirmed'), ('done', 'Attended')], string='Status', default='draft', readonly=True, copy=False, tracking=True) - @api.onchange('event_id') - def _onchange_event_id(self): - # We reset the ticket when keeping it would lead to an inconstitent state. - if self.event_ticket_id and (not self.event_id or self.event_id != self.event_ticket_id.event_id): - self.event_ticket_id = None + @api.depends('partner_id') + def _compute_contact_info(self): + for registration in self: + if registration.partner_id: + partner_vals = self._synchronize_partner_values(registration.partner_id) + registration.update( + dict((fname, fvalue) + for fname, fvalue in partner_vals.items() + if fvalue and not (registration[fname] or registration._origin[fname]) + ) + ) + + @api.depends('state') + def _compute_date_closed(self): + for registration in self: + if registration.state == 'done' and not registration.date_closed: + registration.date_closed = fields.Datetime.now() @api.constrains('event_id', 'state') def _check_seats_limit(self): @@ -64,10 +80,10 @@ class EventRegistration(models.Model): if record.event_ticket_id.seats_max and record.event_ticket_id.seats_available < 0: raise ValidationError(_('No more available seats for this ticket')) - @api.onchange('partner_id') - def _onchange_partner(self): - if self.partner_id: - self.update(self._synchronize_partner_values(self.partner_id)) + @api.constrains('event_id', 'event_ticket_id') + def _check_event_ticket(self): + if any(registration.event_id != registration.event_ticket_id.event_id for registration in self if registration.event_ticket_id): + raise ValidationError(_('Invalid event / ticket choice')) # ------------------------------------------------------------ # CRUD @@ -75,13 +91,6 @@ class EventRegistration(models.Model): @api.model def create(self, vals): - # update missing pieces of information from partner - if vals.get('partner_id'): - partner_vals = self._synchronize_partner_values( - self.env['res.partner'].browse(vals['partner_id']) - ) - vals = dict(partner_vals, **vals) - registration = super(EventRegistration, self).create(vals) if registration._check_auto_confirmation(): registration.sudo().action_confirm() @@ -89,21 +98,8 @@ class EventRegistration(models.Model): return registration def write(self, vals): - if vals.get('state') == 'done' and 'date_closed' not in vals: - vals['date_closed'] = fields.Datetime.now() - ret = super(EventRegistration, self).write(vals) - # update missing pieces of information from partner - if vals.get('partner_id'): - partner_vals = self._synchronize_partner_values( - self.env['res.partner'].browse(vals['partner_id']) - ) - for registration in self: - partner_info = dict((key, val) for key, val in partner_vals.items() if not registration[key]) - if partner_info: - registration.write(partner_info) - if vals.get('state') == 'open': # auto-trigger after_sub (on subscribe) mail schedulers, if needed onsubscribe_schedulers = self.mapped('event_id.event_mail_ids').filtered(lambda s: s.interval_type == 'after_sub') diff --git a/addons/event/tests/common.py b/addons/event/tests/common.py index f58722efcd1..8c32e558185 100644 --- a/addons/event/tests/common.py +++ b/addons/event/tests/common.py @@ -58,7 +58,7 @@ class TestEventCommon(common.SavepointCase): 'default_timezone': 'Europe/Paris', 'use_ticket': True, 'event_type_ticket_ids': [(0, 0, { - 'name': 'First Ticket', + 'name': 'First Ticket', }), (0, 0, { 'name': 'Second Ticket', }) diff --git a/addons/event/tests/test_event_flow.py b/addons/event/tests/test_event_flow.py index 572e64a8231..1ec9208e1b3 100644 --- a/addons/event/tests/test_event_flow.py +++ b/addons/event/tests/test_event_flow.py @@ -24,6 +24,7 @@ class TestEventFlow(TestEventCommon): 'seats_max': 2, 'seats_availability': 'limited', }) + self.assertTrue(test_event.auto_confirm) # EventUser create registrations for this event test_reg1 = self.env['event.registration'].with_user(self.user_eventuser).create({ @@ -56,15 +57,17 @@ class TestEventFlow(TestEventCommon): @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') def test_event_flow(self): - """ Avanced event flow: no auto confirmation, manage minimum / maximum + """ Advanced event flow: no auto confirmation, manage minimum / maximum seats, ... """ # EventUser creates a new event: ok test_event = self.env['event.event'].with_user(self.user_eventmanager).create({ 'name': 'TestEvent', 'date_begin': datetime.datetime.now() + relativedelta(days=-1), 'date_end': datetime.datetime.now() + relativedelta(days=1), + 'seats_availability': 'limited', 'seats_max': 10, }) + self.assertFalse(test_event.auto_confirm) # EventUser create registrations for this event -> no auto confirmation test_reg1 = self.env['event.registration'].with_user(self.user_eventuser).create({ diff --git a/addons/event/tests/test_event_internals.py b/addons/event/tests/test_event_internals.py index 95e935972b3..b0ab686fa76 100644 --- a/addons/event/tests/test_event_internals.py +++ b/addons/event/tests/test_event_internals.py @@ -58,7 +58,7 @@ class TestEventData(TestEventCommon): 'date_begin': FieldsDatetime.to_string(datetime.today() + timedelta(days=1)), 'date_end': FieldsDatetime.to_string(datetime.today() + timedelta(days=15)), }) - event._onchange_type() + self.assertFalse(event.is_online) self.assertEqual(event.address_id, self.env.user.company_id.partner_id) # seats: coming from event type configuration @@ -71,7 +71,6 @@ class TestEventData(TestEventCommon): # set is_online: should reset the address_id field event.update({'is_online': True}) - event._onchange_is_online() self.assertTrue(event.is_online) self.assertFalse(event.address_id) @@ -112,14 +111,12 @@ class TestEventData(TestEventCommon): event_type.write({ 'use_mail_schedule': False, }) - # Event type does not use mail schedule but data is kept for compatibility and avoid recreating them - self.assertEqual(len(event_type.event_type_mail_ids), 2) + self.assertEqual(event_type.event_type_mail_ids, self.env['event.type.mail']) event = self.env['event.event'].create({ 'name': 'Event Update Type', 'date_begin': FieldsDatetime.to_string(datetime.today() + timedelta(days=1)), 'date_end': FieldsDatetime.to_string(datetime.today() + timedelta(days=15)), - 'auto_confirm': False, 'is_online': True, }) self.assertEqual(event.date_tz, self.env.user.tz) @@ -128,22 +125,18 @@ class TestEventData(TestEventCommon): self.assertTrue(event.is_online) self.assertEqual(event.event_mail_ids, self.env['event.mail']) - event.update({'event_type_id': event_type.id}) - event._onchange_type() - self.assertEqual(event.date_tz, 'Europe/Paris') - self.assertEqual(event.seats_availability, 'limited') - self.assertEqual(event.seats_max, event_type.default_registration_max) - self.assertTrue(event.auto_confirm) - self.assertFalse(event.is_online) - self.assertEqual(event.event_mail_ids, self.env['event.mail']) - event_type.write({ 'use_mail_schedule': True, 'event_type_mail_ids': [(5, 0), (0, 0, { 'interval_nbr': 1, 'interval_unit': 'days', 'interval_type': 'before_event', 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_reminder')})] }) - event._onchange_type() + event.write({'event_type_id': event_type.id}) + self.assertEqual(event.date_tz, 'Europe/Paris') + self.assertEqual(event.seats_availability, 'limited') + self.assertEqual(event.seats_max, event_type.default_registration_max) + self.assertTrue(event.auto_confirm) + self.assertFalse(event.is_online) self.assertEqual(event.event_mail_ids.interval_nbr, 1) self.assertEqual(event.event_mail_ids.interval_unit, 'days') self.assertEqual(event.event_mail_ids.interval_type, 'before_event') @@ -194,7 +187,7 @@ class TestEventData(TestEventCommon): self.assertFalse(event.event_registrations_open) @users('user_eventmanager') - def test_ongoing_events(self): + def test_event_ongoing(self): self.patcher = patch('odoo.addons.event.models.event_event.fields.Datetime', wraps=FieldsDatetime) self.mock_datetime = self.patcher.start() self.mock_datetime.now.return_value = datetime(2020, 1, 10, 8, 0, 0) @@ -297,14 +290,12 @@ class TestEventTypeData(TestEventCommon): 'default_registration_max': 30, 'use_ticket': True, }) - event_type._onchange_has_seats_limitation() self.assertTrue(event_type.has_seats_limitation) self.assertEqual(event_type.default_registration_max, 30) self.assertEqual(event_type.event_type_ticket_ids.mapped('name'), ['Registration']) # reset seats limitation event_type.write({'has_seats_limitation': False}) - event_type._onchange_has_seats_limitation() self.assertFalse(event_type.has_seats_limitation) self.assertEqual(event_type.default_registration_max, 0) diff --git a/addons/event/tests/test_event_mail_schedule.py b/addons/event/tests/test_event_mail_schedule.py index ba1223ccc0f..42e1d18017d 100644 --- a/addons/event/tests/test_event_mail_schedule.py +++ b/addons/event/tests/test_event_mail_schedule.py @@ -22,7 +22,6 @@ class TestMailSchedule(TestEventCommon): 'auto_confirm': True, 'date_begin': event_date_begin, 'date_end': event_date_end, - 'seats_max': 10, 'event_mail_ids': [ (0, 0, { # right at subscription 'interval_unit': 'now', diff --git a/addons/event/tests/test_event_security.py b/addons/event/tests/test_event_security.py index d2ac6d275a3..50cf81a84ec 100644 --- a/addons/event/tests/test_event_security.py +++ b/addons/event/tests/test_event_security.py @@ -26,6 +26,7 @@ class TestEventSecurity(TestEventCommon): 'name': 'TestEvent', 'date_begin': datetime.now() + relativedelta(days=-1), 'date_end': datetime.now() + relativedelta(days=1), + 'seats_availability': 'limited', 'seats_max': 10, }) @@ -50,7 +51,6 @@ class TestEventSecurity(TestEventCommon): 'name': 'TestEvent', 'date_begin': datetime.now() + relativedelta(days=-1), 'date_end': datetime.now() + relativedelta(days=1), - 'seats_max': 10, }) with self.assertRaises(AccessError): @@ -76,12 +76,10 @@ class TestEventSecurity(TestEventCommon): }) event = self.env['event.event'].create({ 'name': 'ManagerEvent', + 'event_type_id': event_type.id, 'date_begin': datetime.now() + relativedelta(days=-1), 'date_end': datetime.now() + relativedelta(days=1), }) - event.update({'event_type_id': event_type.id}) - event._onchange_type() - event.flush() registration = self.env['event.registration'].create({'event_id': event.id, 'name': 'Myself'}) registration.write({'name': 'Myself2'}) diff --git a/addons/event_sale/tests/test_event_internals.py b/addons/event_sale/tests/test_event_internals.py index d70911b1524..0f3a7ae4d08 100644 --- a/addons/event_sale/tests/test_event_internals.py +++ b/addons/event_sale/tests/test_event_internals.py @@ -28,7 +28,6 @@ class TestEventData(TestEventSaleCommon): 'date_begin': FieldsDatetime.to_string(datetime.today() + timedelta(days=1)), 'date_end': FieldsDatetime.to_string(datetime.today() + timedelta(days=15)), }) - event._onchange_type() self.assertEqual(event.event_ticket_ids, self.env['event.event.ticket']) event_type.write({ @@ -43,7 +42,7 @@ class TestEventData(TestEventSaleCommon): self.assertEqual(event_type.event_type_ticket_ids.description, self.event_product.description_sale) # synchronize event - event._onchange_type() + event.write({'event_type_id': event_type.id}) self.assertEqual(event.event_ticket_ids.name, event.event_type_id.event_type_ticket_ids.name) self.assertEqual(event.event_ticket_ids.seats_availability, 'limited') self.assertEqual(event.event_ticket_ids.seats_max, 5) diff --git a/addons/website_event/models/event_event.py b/addons/website_event/models/event_event.py index 472a60f802a..e67d6da0ce6 100644 --- a/addons/website_event/models/event_event.py +++ b/addons/website_event/models/event_event.py @@ -33,9 +33,10 @@ class Event(models.Model): # website website_published = fields.Boolean(tracking=True) website_menu = fields.Boolean( - 'Dedicated Menu', copy=False, + string='Dedicated Menu', copy=False, + compute='_compute_from_event_type', readonly=False, store=True, help="Creates menus Introduction, Location and Register on the page " - " of the event on the website.") + "of the event on the website.") menu_id = fields.Many2one('website.menu', 'Event Menu', copy=False) def _compute_is_participating(self): @@ -55,11 +56,17 @@ 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 + @api.depends('event_type_id') + def _compute_from_event_type(self): + """ Also ensure a value for website_menu as it is a trigger notably for + track related menus. """ + super(Event, self)._compute_from_event_type() + for event in self: + if not event.event_type_id: + if not event.website_menu: + event.website_menu = False + elif event.event_type_id: + event.website_menu = event.event_type_id.website_menu @api.model def create(self, vals): diff --git a/addons/website_event_questions/models/event_event.py b/addons/website_event_questions/models/event_event.py index 53e1cfc0533..3a7984a30a0 100644 --- a/addons/website_event_questions/models/event_event.py +++ b/addons/website_event_questions/models/event_event.py @@ -17,21 +17,24 @@ class EventEvent(models.Model): """ Override Event model to add optional questions when buying tickets. """ _inherit = 'event.event' - question_ids = fields.One2many('event.question', 'event_id', 'Questions', copy=True) + question_ids = fields.One2many( + 'event.question', 'event_id', 'Questions', copy=True, + compute='_compute_from_event_type', readonly=False, store=True) general_question_ids = fields.One2many('event.question', 'event_id', 'General Questions', domain=[('once_per_order', '=', True)]) specific_question_ids = fields.One2many('event.question', 'event_id', 'Specific Questions', domain=[('once_per_order', '=', False)]) - @api.onchange('event_type_id') - def _onchange_type(self): - super(EventEvent, self)._onchange_type() - if self.event_type_id.use_questions and self.event_type_id.question_ids: - self.question_ids = [(5, 0, 0)] + [ - (0, 0, { - 'title': question.title, - 'sequence': question.sequence, - 'once_per_order': question.once_per_order, - }) - for question in self.event_type_id.question_ids - ] + @api.depends('event_type_id') + def _compute_from_event_type(self): + super(EventEvent, self)._compute_from_event_type() + for event in self: + if event.event_type_id.use_questions and event.event_type_id.question_ids: + event.question_ids = [(5, 0, 0)] + [ + (0, 0, { + 'title': question.title, + 'sequence': question.sequence, + 'once_per_order': question.once_per_order, + }) + for question in event.event_type_id.question_ids + ] diff --git a/addons/website_event_questions/tests/test_event_internals.py b/addons/website_event_questions/tests/test_event_internals.py index aedaf148fc9..d3494cae49c 100644 --- a/addons/website_event_questions/tests/test_event_internals.py +++ b/addons/website_event_questions/tests/test_event_internals.py @@ -22,7 +22,7 @@ class TestEventData(TestEventQuestionCommon): 'date_begin': FieldsDatetime.to_string(datetime.today() + timedelta(days=1)), 'date_end': FieldsDatetime.to_string(datetime.today() + timedelta(days=15)), }) - event._onchange_type() + self.assertEqual(event.specific_question_ids.title, 'Question1') self.assertEqual( set(event.specific_question_ids.mapped('answer_ids.name')), diff --git a/addons/website_event_sale/tests/test_frontend_buy_tickets.py b/addons/website_event_sale/tests/test_frontend_buy_tickets.py index e4698895587..a64dca5d4ad 100644 --- a/addons/website_event_sale/tests/test_frontend_buy_tickets.py +++ b/addons/website_event_sale/tests/test_frontend_buy_tickets.py @@ -4,6 +4,7 @@ import odoo.tests from datetime import timedelta + from odoo.addons.base.tests.common import HttpCaseWithUserDemo from odoo.fields import Datetime @@ -16,8 +17,8 @@ class TestUi(HttpCaseWithUserDemo): self.event_2 = self.env['event.event'].create({ 'name': 'Conference for Architects TEST', 'user_id': self.env.ref('base.user_admin').id, - 'date_begin': (Datetime.today()+ timedelta(days=5)).strftime('%Y-%m-%d 07:00:00'), - 'date_end': (Datetime.today()+ timedelta(days=5)).strftime('%Y-%m-%d 16:30:00'), + 'date_begin': (Datetime.today() + timedelta(days=5)).strftime('%Y-%m-%d 07:00:00'), + 'date_end': (Datetime.today() + timedelta(days=5)).strftime('%Y-%m-%d 16:30:00'), }) self.env['event.event.ticket'].create([{ @@ -35,6 +36,9 @@ class TestUi(HttpCaseWithUserDemo): 'price': 1500.0, }]) + # flush event to ensure having tickets available in the tests + self.event_2.flush() + (self.env.ref('base.partner_admin') + self.partner_demo).write({ 'street': '215 Vine St', 'city': 'Scranton', From c6003af73409e2cd124f641fb7714fefa9dc8b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 27 Feb 2020 10:15:25 +0000 Subject: [PATCH 3/8] [IMP] event: set an explicit onchange on partner_id of registration RATIONALE Manual onchange is necessary because you spot an issue (or customer complains). Automatic update through computed feild is only there to try to add missing pieces of information but cannot decide which is the correct field value to keep. SPECIFICATIONS Keep an explicit onchange on partner_id. Rationale : if user explicitly changes the partner in interface, he wants to update the whole customer information. If partner_id is updated in code (e.g. updating your personal information after registeration in website_event_sale) fields with a value should not be reset as we do not know which one is the correct one. How it should behave as following * computed fields based on partner_id should only update missing information. Indeed automated code cannot decide which information is more accurate; * interface should allow to update all customer related information at once. We consider event users really want to update all fields Tests are added to ensure behavior is not modified without notice. LINKS Task ID 2089156 Community PR odoo/odoo#42911 --- addons/event/models/event_registration.py | 28 +++++++++-- addons/event/tests/test_event_flow.py | 60 +++++++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index b9d88dab020..e22283172e2 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -31,10 +31,10 @@ class EventRegistration(models.Model): states={'done': [('readonly', True)]}) name = fields.Char( string='Attendee Name', index=True, - compute='_compute_contact_info', copy=True, readonly=False, store=True, tracking=True) - email = fields.Char(string='Email', compute='_compute_contact_info', copy=True, readonly=False, store=True) - phone = fields.Char(string='Phone', compute='_compute_contact_info', copy=True, readonly=False, store=True) - mobile = fields.Char(string='Mobile', compute='_compute_contact_info', copy=True, readonly=False, store=True) + compute='_compute_contact_info', copy=True, readonly=False, store=True, tracking=10) + email = fields.Char(string='Email', compute='_compute_contact_info', copy=True, readonly=False, store=True, tracking=11) + phone = fields.Char(string='Phone', compute='_compute_contact_info', copy=True, readonly=False, store=True, tracking=12) + mobile = fields.Char(string='Mobile', compute='_compute_contact_info', copy=True, readonly=False, store=True, tracking=13) # organization date_open = fields.Datetime(string='Registration Date', readonly=True, default=lambda self: fields.Datetime.now()) # weird crash is directly now date_closed = fields.Datetime( @@ -50,6 +50,26 @@ class EventRegistration(models.Model): ('open', 'Confirmed'), ('done', 'Attended')], string='Status', default='draft', readonly=True, copy=False, tracking=True) + @api.onchange('partner_id') + def _onchange_partner_id(self): + """ Keep an explicit onchange on partner_id. Rationale : if user explicitly + changes the partner in interface, he want to update the whole customer + information. If partner_id is updated in code (e.g. updating your personal + information after having registered in website_event_sale) fields with a + value should not be reset as we don't know which one is the right one. + + In other words + * computed fields based on partner_id should only update missing + information. Indeed automated code cannot decide which information + is more accurate; + * interface should allow to update all customer related information + at once. We consider event users really want to update all fields + related to the partner; + """ + for registration in self: + if registration.partner_id: + registration.update(registration._synchronize_partner_values(registration.partner_id)) + @api.depends('partner_id') def _compute_contact_info(self): for registration in self: diff --git a/addons/event/tests/test_event_flow.py b/addons/event/tests/test_event_flow.py index 1ec9208e1b3..ff114bb4196 100644 --- a/addons/event/tests/test_event_flow.py +++ b/addons/event/tests/test_event_flow.py @@ -7,9 +7,69 @@ from dateutil.relativedelta import relativedelta from odoo.addons.event.tests.common import TestEventCommon from odoo.exceptions import ValidationError +from odoo.tests.common import Form from odoo.tools import mute_logger +class TestEventUI(TestEventCommon): + + def test_event_registration_partner_sync(self): + """ Ensure onchange on partner_id is kept for interface, not for computed + fields. """ + registration_form = Form(self.env['event.registration'].with_context( + default_name='WrongName', + default_event_id=self.event_0.id + )) + self.assertEqual(registration_form.event_id, self.event_0) + self.assertEqual(registration_form.name, 'WrongName') + self.assertFalse(registration_form.email) + self.assertFalse(registration_form.phone) + self.assertFalse(registration_form.mobile) + + # trigger onchange + registration_form.partner_id = self.customer + self.assertEqual(registration_form.name, self.customer.name) + self.assertEqual(registration_form.email, self.customer.email) + self.assertEqual(registration_form.phone, self.customer.phone) + self.assertEqual(registration_form.mobile, self.customer.mobile) + + # save, check record matches Form values + registration = registration_form.save() + self.assertEqual(registration.partner_id, self.customer) + self.assertEqual(registration.name, self.customer.name) + self.assertEqual(registration.email, self.customer.email) + self.assertEqual(registration.phone, self.customer.phone) + self.assertEqual(registration.mobile, self.customer.mobile) + + # allow writing on some fields independently from customer config + registration.write({'phone': False, 'mobile': False}) + self.assertFalse(registration.phone) + self.assertFalse(registration.mobile) + + # reset partner should not reset other fields + registration.write({'partner_id': False}) + self.assertEqual(registration.partner_id, self.env['res.partner']) + self.assertEqual(registration.name, self.customer.name) + self.assertEqual(registration.email, self.customer.email) + self.assertFalse(registration.phone) + self.assertFalse(registration.mobile) + + # update to a new partner not through UI -> update only void feilds + customer2 = self.env['res.partner'].create({ + 'name': 'Constantin Customer 2', + 'email': 'constantin2@test.example.com', + 'country_id': self.env.ref('base.be').id, + 'phone': '0456987654', + 'mobile': '0456654321', + }) + registration.write({'partner_id': customer2.id}) + self.assertEqual(registration.partner_id, customer2) + self.assertEqual(registration.name, self.customer.name) + self.assertEqual(registration.email, self.customer.email) + self.assertEqual(registration.phone, customer2.phone) + self.assertEqual(registration.mobile, customer2.mobile) + + class TestEventFlow(TestEventCommon): @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') From 7666f97693645ac3c6139089ed01ac411347af97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 19 Feb 2020 15:15:53 +0000 Subject: [PATCH 4/8] [REF] event_sale: move from onchange / default to stored editable computed fields PURPOSE Try to move from onchange / default_get to stored editable computed fields. Behavior should be the same (computed or set by user), with support of create / write / onchange field update without additional code. SPECIFICATIONS: GLOBAL RULES Update classic fields updated in some cases by onchange and/or default methods by fields with store=True, readonly=False. It means their value comes either from manual user input, either from trigger based computation. Remove onchange and default_get when possible, leading to an unique computation method and clearing fields definition. Also clean some fields definition inconsistencies, notably required fields that should instead be correctly computed or default that have no real meaning. LINKS Task ID 2089156 Community PR odoo/odoo#42911 --- .../event_sale/models/event_registration.py | 13 +++--- addons/event_sale/models/event_ticket.py | 44 +++++++++++++------ .../event_sale/tests/test_event_internals.py | 3 -- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/addons/event_sale/models/event_registration.py b/addons/event_sale/models/event_registration.py index 3c8b4c780a9..84144973885 100644 --- a/addons/event_sale/models/event_registration.py +++ b/addons/event_sale/models/event_registration.py @@ -73,13 +73,12 @@ class EventRegistration(models.Model): 'old_ticket_name': registration.event_ticket_id.name, 'new_ticket_name': new_event_ticket.name } - user_id = registration.event_id.user_id.id or \ - registration.sale_order_id.user_id.id or \ - fallback_user_id - registration.sale_order_id.activity_schedule_with_view('mail.mail_activity_data_warning', - user_id=user_id, - views_or_xmlid='event_sale.event_ticket_id_change_exception', - render_context=render_context) + user_id = registration.event_id.user_id.id or registration.sale_order_id.user_id.id or fallback_user_id + registration.sale_order_id.activity_schedule_with_view( + 'mail.mail_activity_data_warning', + user_id=user_id, + views_or_xmlid='event_sale.event_ticket_id_change_exception', + render_context=render_context) def _action_set_paid(self): self.write({'is_paid': True}) diff --git a/addons/event_sale/models/event_ticket.py b/addons/event_sale/models/event_ticket.py index e3e666d5db3..d88ea5f4a24 100644 --- a/addons/event_sale/models/event_ticket.py +++ b/addons/event_sale/models/event_ticket.py @@ -14,25 +14,41 @@ class EventTemplateTicket(models.Model): def _default_product_id(self): return self.env.ref('event_sale.product_product_event', raise_if_not_found=False) + description = fields.Text(compute='_compute_description', copy=True, readonly=False, store=True) # product product_id = fields.Many2one( - 'product.product', string='Product', required=True, domain=[("event_ok", "=", True)], - default=_default_product_id) - price = fields.Float(string='Price', digits='Product Price') - price_reduce = fields.Float(string="Price Reduce", compute="_compute_price_reduce", digits='Product Price') + 'product.product', string='Product', required=True, + domain=[("event_ok", "=", True)], default=_default_product_id) + price = fields.Float( + string='Price', compute='_compute_price', + digits='Product Price', copy=True, readonly=False, store=True) + price_reduce = fields.Float( + string="Price Reduce", compute="_compute_price_reduce", + digits='Product Price') @api.depends('product_id') - def _compute_price_reduce(self): - for record in self: - product = record.product_id - discount = product.lst_price and (product.lst_price - product.price) / product.lst_price or 0.0 - record.price_reduce = (1.0 - discount) * record.price + def _compute_price(self): + for ticket in self: + if ticket.product_id and ticket.product_id.lst_price: + ticket.price = ticket.product_id.lst_price or 0 + elif not ticket.price: + ticket.price = 0 - @api.onchange('product_id') - def _onchange_product_id(self): - self.price = self.product_id.list_price or 0 - if not self.description and self.product_id.description_sale: - self.description = self.product_id.description_sale + @api.depends('product_id') + def _compute_description(self): + for ticket in self: + if ticket.product_id and ticket.product_id.description_sale: + ticket.description = ticket.product_id.description_sale + # initialize, i.e for embedded tree views + if not ticket.description: + ticket.description = False + + @api.depends('product_id', 'price') + def _compute_price_reduce(self): + for ticket in self: + product = ticket.product_id + discount = product.lst_price and (product.lst_price - product.price) / product.lst_price or 0.0 + ticket.price_reduce = (1.0 - discount) * ticket.price def _init_column(self, column_name): if column_name != "product_id": diff --git a/addons/event_sale/tests/test_event_internals.py b/addons/event_sale/tests/test_event_internals.py index 0f3a7ae4d08..fc6341b51a5 100644 --- a/addons/event_sale/tests/test_event_internals.py +++ b/addons/event_sale/tests/test_event_internals.py @@ -38,7 +38,6 @@ class TestEventData(TestEventSaleCommon): 'seats_max': 5, })] }) - event_type.event_type_ticket_ids._onchange_product_id() self.assertEqual(event_type.event_type_ticket_ids.description, self.event_product.description_sale) # synchronize event @@ -112,9 +111,7 @@ class TestEventTicketData(TestEventSaleCommon): ], }) first_ticket = event.event_ticket_ids.filtered(lambda t: t.name == 'First Ticket') - first_ticket._onchange_product_id() second_ticket = event.event_ticket_ids.filtered(lambda t: t.name == 'Second Ticket') - second_ticket._onchange_product_id() # force second ticket price, after calling the onchange second_ticket.write({'price': 8.0}) From 968ef0223f43f454946613e8bee27a4275615d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 19 Feb 2020 15:16:19 +0000 Subject: [PATCH 5/8] [REF] website_event(_track/_question) : move from onchange / default to stored editable computed fields PURPOSE Try to move from onchange / default_get to stored editable computed fields. Behavior should be the same (computed or set by user), with support of create / write / onchange field update without additional code. SPECIFICATIONS: GLOBAL RULES Update classic fields updated in some cases by onchange and/or default methods by fields with store=True, readonly=False. It means their value comes either from manual user input, either from trigger based computation. Remove onchange and default_get when possible, leading to an unique computation method and clearing fields definition. Also clean some fields definition inconsistencies, notably required fields that should instead be correctly computed or default that have no real meaning. SPECIFICATIONS: WEBSITE_TRACK(_PROPOSAL) Keep an explicit onchange for tick / untick of website_track_proposal. Indeed otherwise you have a loop of dependencies between website_track and website_track_proposal * untick website_track: website_track_proposal = False (done in _compute_website_track_proposal) * tick website_track: no effect * untick website_track_proposal: no effect * tick website_track_proposa: website_track = True It would be complicated to write in computed fields, as they depend on each other, on cache and current values, ... It is therefore simpler to keep an onchange: when ticking website_track_proposal set website_track as True in interface. LINKS Task ID 2089156 Community PR odoo/odoo#42911 --- addons/website_event/models/event_event.py | 5 +- .../website_event_track/models/event_event.py | 94 ++++++++++++------- .../website_event_track/models/event_track.py | 27 ++++-- 3 files changed, 77 insertions(+), 49 deletions(-) diff --git a/addons/website_event/models/event_event.py b/addons/website_event/models/event_event.py index e67d6da0ce6..ec7e6073c85 100644 --- a/addons/website_event/models/event_event.py +++ b/addons/website_event/models/event_event.py @@ -62,9 +62,8 @@ class Event(models.Model): track related menus. """ super(Event, self)._compute_from_event_type() for event in self: - if not event.event_type_id: - if not event.website_menu: - event.website_menu = False + if not event.event_type_id and not event.website_menu: + event.website_menu = False elif event.event_type_id: event.website_menu = event.event_type_id.website_menu diff --git a/addons/website_event_track/models/event_event.py b/addons/website_event_track/models/event_event.py index c98815edaad..20ef5f43c97 100644 --- a/addons/website_event_track/models/event_event.py +++ b/addons/website_event_track/models/event_event.py @@ -8,14 +8,19 @@ from odoo.addons.http_routing.models.ir_http import slug class EventType(models.Model): _inherit = 'event.type' - website_track = fields.Boolean('Tracks on Website') - website_track_proposal = fields.Boolean('Tracks Proposals on Website') + website_track = fields.Boolean( + string='Tracks on Website', compute='_compute_website_menu_data', + copy=True, readonly=False, store=True) + website_track_proposal = fields.Boolean( + string='Tracks Proposals on Website', compute='_compute_website_menu_data', + copy=True, readonly=False, store=True) - @api.onchange('website_menu') - def _onchange_website_menu(self): - if not self.website_menu: - self.website_track = False - self.website_track_proposal = False + @api.depends('website_menu') + def _compute_website_menu_data(self): + for event_type in self: + if not event_type.website_menu: + event_type.website_track = False + event_type.website_track_proposal = False class Event(models.Model): @@ -23,16 +28,16 @@ class Event(models.Model): track_ids = fields.One2many('event.track', 'event_id', 'Tracks') track_count = fields.Integer('Track Count', compute='_compute_track_count') - sponsor_ids = fields.One2many('event.sponsor', 'event_id', 'Sponsors') sponsor_count = fields.Integer('Sponsor Count', compute='_compute_sponsor_count') - - website_track = fields.Boolean('Tracks on Website') - website_track_proposal = fields.Boolean('Proposals on Website') - + website_track = fields.Boolean( + 'Tracks on Website', compute='_compute_website_track', + copy=True, readonly=False, store=True) + website_track_proposal = fields.Boolean( + 'Proposals on Website', compute='_compute_website_track_proposal', + copy=True, readonly=False, store=True) track_menu_ids = fields.One2many('website.event.menu', 'event_id', string='Event Tracks Menus', domain=[('menu_type', '=', 'track')]) track_proposal_menu_ids = fields.One2many('website.event.menu', 'event_id', string='Event Proposals Menus', domain=[('menu_type', '=', 'track_proposal')]) - allowed_track_tag_ids = fields.Many2many('event.track.tag', relation='event_allowed_track_tags_rel', string='Available Track Tags') tracks_tag_ids = fields.Many2many( 'event.track.tag', relation='event_track_tags_rel', string='Track Tags', @@ -50,6 +55,46 @@ class Event(models.Model): for event in self: event.sponsor_count = result.get(event.id, 0) + @api.depends('event_type_id', 'website_menu') + def _compute_website_track(self): + """ Explicitly checks that event_type has changed before copying its value + on the event itself. Changing website_menu trigger should not mess with the + behavior of event_type. """ + for event in self: + if event.event_type_id and event.event_type_id != event._origin.event_type_id: + event.website_track = event.event_type_id.website_track + elif not event.website_menu or not event.website_track: + event.website_track = False + + @api.depends('event_type_id', 'website_track') + def _compute_website_track_proposal(self): + for event in self: + if event.event_type_id and event.event_type_id != event._origin.event_type_id: + event.website_track_proposal = event.event_type_id.website_track_proposal + elif not event.website_track: + event.website_track_proposal = False + + @api.onchange('website_track_proposal') + def _onchange_website_track_proposal(self): + """ Keep an explicit onchange for tick / untick of website_track_proposal. + Indeed otherwise you have a loop of dependencies between website_track and + website_track_proposal + + * untick website_track: website_track_proposal = False (done in _compute_website_track_proposal) + * tick website_track: no effect + * untick website_track_proposal: no effect + * tick website_track_proposa: website_track = True + + It would be complicated to write in computed fields, as they depend on + each other, on cache and current values, ... + + It is therefore simpler to keep an onchange: when ticking website_track_proposal + set website_track as True in interface. + """ + for event in self: + if event.website_track_proposal and not event.website_track: + event.website_track = True + @api.depends('track_ids.tag_ids', 'track_ids.tag_ids.color') def _compute_tracks_tag_ids(self): for event in self: @@ -92,26 +137,3 @@ class Event(models.Model): self.ensure_one() res = [(_('Talk Proposals'), '/event/%s/track_proposal' % slug(self), False, 'track_proposal')] return res - - @api.onchange('event_type_id') - def _onchange_type(self): - super(Event, self)._onchange_type() - if self.event_type_id and self.website_menu: - self.website_track = self.event_type_id.website_track - self.website_track_proposal = self.event_type_id.website_track_proposal - - @api.onchange('website_menu') - def _onchange_website_menu(self): - if not self.website_menu: - self.website_track = False - self.website_track_proposal = False - - @api.onchange('website_track') - def _onchange_website_track(self): - if not self.website_track: - self.website_track_proposal = False - - @api.onchange('website_track_proposal') - def _onchange_website_track_proposal(self): - if self.website_track_proposal: - self.website_track = True diff --git a/addons/website_event_track/models/event_track.py b/addons/website_event_track/models/event_track.py index bb2ab91999b..80b16d75e16 100644 --- a/addons/website_event_track/models/event_track.py +++ b/addons/website_event_track/models/event_track.py @@ -43,10 +43,16 @@ class Track(models.Model): user_id = fields.Many2one('res.users', 'Responsible', tracking=True, default=lambda self: self.env.user) company_id = fields.Many2one('res.company', related='event_id.company_id') partner_id = fields.Many2one('res.partner', 'Speaker') - partner_name = fields.Char('Name', tracking=10) - partner_email = fields.Char('Email', tracking=20) - partner_phone = fields.Char('Phone', tracking=30) - partner_biography = fields.Html('Biography') + partner_name = fields.Char( + string='Name', compute='_compute_partner_info', + copy=True, readonly=False, store=True, tracking=10) + partner_email = fields.Char( + string='Email', compute='_compute_partner_info', + copy=True, readonly=False, store=True, tracking=20) + partner_phone = fields.Char( + string='Phone', compute='_compute_partner_info', + copy=True, readonly=False, store=True, tracking=30) + partner_biography = fields.Html(string='Biography') tag_ids = fields.Many2many('event.track.tag', string='Tags') stage_id = fields.Many2one( 'event.track.stage', string='Stage', ondelete='restrict', @@ -82,12 +88,13 @@ class Track(models.Model): if track.id: track.website_url = '/event/%s/track/%s' % (slug(track.event_id), slug(track)) - @api.onchange('partner_id') - def _onchange_partner_id(self): - if self.partner_id: - self.partner_name = self.partner_id.name - self.partner_email = self.partner_id.email - self.partner_phone = self.partner_id.phone + @api.depends('partner_id') + def _compute_partner_info(self): + for track in self: + if track.partner_id: + track.partner_name = track.partner_id.name + track.partner_email = track.partner_id.email + track.partner_phone = track.partner_id.phone @api.depends('date', 'duration') def _compute_end_date(self): From e7c4e9427594b3ddb80bb3a7b7080bb96a0b00e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 5 Mar 2020 13:38:47 +0000 Subject: [PATCH 6/8] [FIX] website_event: copy website_menu when copying an event Currently website_menu boolean field is not copied when copying an event. It comes from an issue related to duplicating website menus. It seems real issue has been fixed at 1a8993e0cbcc7d9871a22602f1a14d5270540f64 . Current copy=False on website_menu is a wrong fix due to some mismatch in forward-port. We can therefore copy website_menu again. LINKS Task ID 2089156 Community PR odoo/odoo#42911 --- addons/website_event/models/event_event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_event/models/event_event.py b/addons/website_event/models/event_event.py index ec7e6073c85..6cd2a7d5bd2 100644 --- a/addons/website_event/models/event_event.py +++ b/addons/website_event/models/event_event.py @@ -33,7 +33,7 @@ class Event(models.Model): # website website_published = fields.Boolean(tracking=True) website_menu = fields.Boolean( - string='Dedicated Menu', copy=False, + string='Dedicated Menu', compute='_compute_from_event_type', readonly=False, store=True, help="Creates menus Introduction, Location and Register on the page " "of the event on the website.") From c9746f11fa966644d9ee97cdf0cb4d2389ddead1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 4 Mar 2020 12:55:06 +0000 Subject: [PATCH 7/8] [IMP] event(_*): slightly improve event type and questions demo data Notably clean a bit existing event type (data and demo) and add some questions on conference event type. LINKS Task ID 2089156 Community PR #42911 --- addons/event/data/event_data.xml | 11 ++--- addons/event/data/event_demo.xml | 17 ++++---- .../website_event_questions/__manifest__.py | 1 + .../data/event_demo.xml | 23 ++--------- .../data/event_question_demo.xml | 41 +++++++++++++++++++ .../website_event_track/data/event_data.xml | 5 +-- 6 files changed, 61 insertions(+), 37 deletions(-) create mode 100644 addons/website_event_questions/data/event_question_demo.xml diff --git a/addons/event/data/event_data.xml b/addons/event/data/event_data.xml index a9ab41942a6..0c430e37799 100644 --- a/addons/event/data/event_data.xml +++ b/addons/event/data/event_data.xml @@ -21,18 +21,15 @@ - - Physical Event - - - - Ticketing - + + Conference + + diff --git a/addons/event/data/event_demo.xml b/addons/event/data/event_demo.xml index 6cf245d577b..086b40eacc2 100644 --- a/addons/event/data/event_demo.xml +++ b/addons/event/data/event_demo.xml @@ -5,17 +5,24 @@ Exhibition - + Training + - Sport + + + US/Pacific + + + + Europe/Brussels @@ -29,7 +36,6 @@ US/Pacific - @@ -60,9 +66,8 @@ - + - unlimited @@ -97,7 +102,6 @@ - unlimited @@ -112,7 +116,6 @@ US/Pacific - diff --git a/addons/website_event_questions/__manifest__.py b/addons/website_event_questions/__manifest__.py index b4cddc33bc4..099abeef1a7 100644 --- a/addons/website_event_questions/__manifest__.py +++ b/addons/website_event_questions/__manifest__.py @@ -13,6 +13,7 @@ 'security/ir.model.access.csv', ], 'demo': [ + 'data/event_question_demo.xml', 'data/event_demo.xml', 'data/event_registration_demo.xml', ], diff --git a/addons/website_event_questions/data/event_demo.xml b/addons/website_event_questions/data/event_demo.xml index 0a8b5e4f31b..cd199671613 100644 --- a/addons/website_event_questions/data/event_demo.xml +++ b/addons/website_event_questions/data/event_demo.xml @@ -1,23 +1,8 @@ - - Meal Type - - - - - Mixed - 1 - - - - Vegetarian - 2 - - - - Pastafarian - 3 - + + + Europe/Brussels + \ No newline at end of file diff --git a/addons/website_event_questions/data/event_question_demo.xml b/addons/website_event_questions/data/event_question_demo.xml new file mode 100644 index 00000000000..414ddb44735 --- /dev/null +++ b/addons/website_event_questions/data/event_question_demo.xml @@ -0,0 +1,41 @@ + + + + + Participate to Social Event + + + + + Yes + 1 + + + + No + 2 + + + + + + Meal Type + + + + + Mixed + 1 + + + + Vegetarian + 2 + + + + Pastafarian + 3 + + + diff --git a/addons/website_event_track/data/event_data.xml b/addons/website_event_track/data/event_data.xml index 772b0673ed2..2f32534c80a 100644 --- a/addons/website_event_track/data/event_data.xml +++ b/addons/website_event_track/data/event_data.xml @@ -1,10 +1,7 @@ - - Conference - - + From 45f32c5795e0af3b0436ed77f1a79311830b2d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 9 Mar 2020 12:03:31 +0000 Subject: [PATCH 8/8] [REF] event: rename event type default_registration_max to seats_max Purpose is to have matching names between event type and event to ease code understanding. LINKS Task ID 2089156 Community PR odoo/odoo#42911 Upgrade PR odoo/upgrade#912 --- addons/event/models/event_event.py | 10 +++++----- addons/event/tests/common.py | 2 +- addons/event/tests/test_event_internals.py | 12 ++++++------ addons/event/views/event_views.xml | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/addons/event/models/event_event.py b/addons/event/models/event_event.py index a1fd7ae8df7..301cfb99266 100644 --- a/addons/event/models/event_event.py +++ b/addons/event/models/event_event.py @@ -34,7 +34,7 @@ class EventType(models.Model): readonly=False, store=True) # registration has_seats_limitation = fields.Boolean('Limited Seats') - default_registration_max = fields.Integer( + seats_max = fields.Integer( 'Maximum Registrations', compute='_compute_default_registration', copy=True, readonly=False, store=True, help="It will select this default maximum value when you choose this event") @@ -89,7 +89,7 @@ class EventType(models.Model): def _compute_default_registration(self): for template in self: if not template.has_seats_limitation: - template.default_registration_max = 0 + template.seats_max = 0 class EventEvent(models.Model): @@ -324,7 +324,7 @@ class EventEvent(models.Model): blacklisted during create (see ``_field_computed`` attribute used in create to compute protected field from re-computation) """ for event in self: - if event.event_type_id.default_registration_max: + if event.event_type_id.seats_max: event.seats_availability = 'limited' if not event.seats_availability: event.seats_availability = 'unlimited' @@ -353,8 +353,8 @@ class EventEvent(models.Model): event.event_ticket_ids = False continue - if event.event_type_id.default_registration_max: - event.seats_max = event.event_type_id.default_registration_max + if event.event_type_id.seats_max: + event.seats_max = event.event_type_id.seats_max if event.event_type_id.auto_confirm: event.auto_confirm = event.event_type_id.auto_confirm diff --git a/addons/event/tests/common.py b/addons/event/tests/common.py index 8c32e558185..62509636ed9 100644 --- a/addons/event/tests/common.py +++ b/addons/event/tests/common.py @@ -53,7 +53,7 @@ class TestEventCommon(common.SavepointCase): 'auto_confirm': True, 'is_online': False, 'has_seats_limitation': True, - 'default_registration_max': 30, + 'seats_max': 30, 'use_timezone': True, 'default_timezone': 'Europe/Paris', 'use_ticket': True, diff --git a/addons/event/tests/test_event_internals.py b/addons/event/tests/test_event_internals.py index b0ab686fa76..1a381f3f37b 100644 --- a/addons/event/tests/test_event_internals.py +++ b/addons/event/tests/test_event_internals.py @@ -63,7 +63,7 @@ class TestEventData(TestEventCommon): self.assertEqual(event.address_id, self.env.user.company_id.partner_id) # seats: coming from event type configuration self.assertEqual(event.seats_availability, 'limited') - self.assertEqual(event.seats_available, event.event_type_id.default_registration_max) + self.assertEqual(event.seats_available, event.event_type_id.seats_max) self.assertEqual(event.seats_unconfirmed, 0) self.assertEqual(event.seats_reserved, 0) self.assertEqual(event.seats_used, 0) @@ -92,7 +92,7 @@ class TestEventData(TestEventCommon): 'name': 'reg_done', }) reg_done.write({'state': 'done'}) - self.assertEqual(event.seats_available, event.event_type_id.default_registration_max - 6) + self.assertEqual(event.seats_available, event.event_type_id.seats_max - 6) self.assertEqual(event.seats_unconfirmed, 1) self.assertEqual(event.seats_reserved, 5) self.assertEqual(event.seats_used, 1) @@ -134,7 +134,7 @@ class TestEventData(TestEventCommon): event.write({'event_type_id': event_type.id}) self.assertEqual(event.date_tz, 'Europe/Paris') self.assertEqual(event.seats_availability, 'limited') - self.assertEqual(event.seats_max, event_type.default_registration_max) + self.assertEqual(event.seats_max, event_type.seats_max) self.assertTrue(event.auto_confirm) self.assertFalse(event.is_online) self.assertEqual(event.event_mail_ids.interval_nbr, 1) @@ -287,17 +287,17 @@ class TestEventTypeData(TestEventCommon): event_type = self.env['event.type'].create({ 'name': 'Testing fields computation', 'has_seats_limitation': True, - 'default_registration_max': 30, + 'seats_max': 30, 'use_ticket': True, }) self.assertTrue(event_type.has_seats_limitation) - self.assertEqual(event_type.default_registration_max, 30) + self.assertEqual(event_type.seats_max, 30) self.assertEqual(event_type.event_type_ticket_ids.mapped('name'), ['Registration']) # reset seats limitation event_type.write({'has_seats_limitation': False}) self.assertFalse(event_type.has_seats_limitation) - self.assertEqual(event_type.default_registration_max, 0) + self.assertEqual(event_type.seats_max, 0) # reset tickets event_type.write({'use_ticket': False}) diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index ed6fbbb926c..8a18a767fc5 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -80,7 +80,7 @@
-