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/event/models/event_event.py b/addons/event/models/event_event.py index e5f89ecf884..301cfb99266 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 @@ -49,8 +34,9 @@ class EventType(models.Model): readonly=False, store=True) # registration has_seats_limitation = fields.Boolean('Limited Seats') - default_registration_max = fields.Integer( - 'Maximum Registrations', default=0, + 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") 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.seats_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.seats_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.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 + + 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..e22283172e2 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=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(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,43 @@ 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.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: + 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 +100,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 +111,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 +118,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..62509636ed9 100644 --- a/addons/event/tests/common.py +++ b/addons/event/tests/common.py @@ -53,12 +53,12 @@ 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, '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..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') @@ -24,6 +84,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 +117,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..1a381f3f37b 100644 --- a/addons/event/tests/test_event_internals.py +++ b/addons/event/tests/test_event_internals.py @@ -58,12 +58,12 @@ 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 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) @@ -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) @@ -93,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) @@ -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.seats_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) @@ -294,19 +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, }) - 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.seats_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) + self.assertEqual(event_type.seats_max, 0) # reset tickets event_type.write({'use_ticket': False}) 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/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 @@
-
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 d70911b1524..fc6341b51a5 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({ @@ -39,11 +38,10 @@ 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 - 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) @@ -113,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}) diff --git a/addons/website_event/models/event_event.py b/addons/website_event/models/event_event.py index 491b8b60b17..6cd2a7d5bd2 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 @@ -28,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', + 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): @@ -50,15 +56,26 @@ 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 and not event.website_menu: + event.website_menu = False + elif event.event_type_id: + event.website_menu = event.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 +88,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 +100,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 +113,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_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_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/__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 89% rename from addons/website_event_sale/tests/test_ui.py rename to addons/website_event_sale/tests/test_frontend_buy_tickets.py index 60092d1bf09..a64dca5d4ad 100644 --- a/addons/website_event_sale/tests/test_ui.py +++ b/addons/website_event_sale/tests/test_frontend_buy_tickets.py @@ -4,9 +4,11 @@ import odoo.tests 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): @@ -15,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([{ @@ -34,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', 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 - - + 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..20ef5f43c97 100644 --- a/addons/website_event_track/models/event_event.py +++ b/addons/website_event_track/models/event_event.py @@ -8,23 +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 - - -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')]) + @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): @@ -32,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', @@ -59,8 +55,53 @@ 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('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: + 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']: @@ -96,31 +137,3 @@ class Event(models.Model): self.ensure_one() 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() - 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): 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')])