diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/website_event_track/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/website_event_track/models')
16 files changed, 1061 insertions, 0 deletions
diff --git a/addons/website_event_track/models/__init__.py b/addons/website_event_track/models/__init__.py new file mode 100644 index 00000000..e07e0223 --- /dev/null +++ b/addons/website_event_track/models/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_event +from . import event_sponsor +from . import event_sponsor_type +from . import event_track +from . import event_track_location +from . import event_track_stage +from . import event_track_tag +from . import event_track_tag_category +from . import event_track_visitor +from . import event_type +from . import res_config_settings +from . import website +from . import website_event_menu +from . import website_menu +from . import website_visitor diff --git a/addons/website_event_track/models/event_event.py b/addons/website_event_track/models/event_event.py new file mode 100644 index 00000000..1b62b47a --- /dev/null +++ b/addons/website_event_track/models/event_event.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.addons.http_routing.models.ir_http import slug + + +class Event(models.Model): + _inherit = "event.event" + + 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', compute='_compute_website_track', + readonly=False, store=True) + website_track_proposal = fields.Boolean( + 'Proposals on Website', compute='_compute_website_track_proposal', + 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', + compute='_compute_tracks_tag_ids', store=True) + + def _compute_track_count(self): + data = self.env['event.track'].read_group([('stage_id.is_cancel', '!=', True)], ['event_id'], ['event_id']) + result = dict((data['event_id'][0], data['event_id_count']) for data in data) + for event in self: + event.track_count = result.get(event.id, 0) + + def _compute_sponsor_count(self): + data = self.env['event.sponsor'].read_group([], ['event_id'], ['event_id']) + result = dict((data['event_id'][0], data['event_id_count']) for data in data) + for event in self: + event.sponsor_count = result.get(event.id, 0) + + @api.depends('event_type_id', 'website_menu') + def _compute_website_track(self): + """ Propagate event_type configuration (only at change); otherwise propagate + website_menu updated value. Also force True is track_proposal changes. """ + 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 event.website_menu and (event.website_menu != event._origin.website_menu or not event.website_track): + event.website_track = True + elif not event.website_menu: + event.website_track = False + + @api.depends('event_type_id', 'website_track') + def _compute_website_track_proposal(self): + """ Propagate event_type configuration (only at change); otherwise propagate + website_track updated value (both together True or False at update). """ + 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 event.website_track != event._origin.website_track or not event.website_track or not event.website_track_proposal: + event.website_track_proposal = event.website_track + + @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 + + # ------------------------------------------------------------ + # WEBSITE MENU MANAGEMENT + # ------------------------------------------------------------ + + def toggle_website_track(self, val): + self.website_track = val + + def toggle_website_track_proposal(self, val): + self.website_track_proposal = val + + def _get_menu_update_fields(self): + return super(Event, self)._get_menu_update_fields() + ['website_track', 'website_track_proposal'] + + def _update_website_menus(self, menus_update_by_field=None): + super(Event, self)._update_website_menus(menus_update_by_field=menus_update_by_field) + for event in self: + if event.menu_id and (not menus_update_by_field or event in menus_update_by_field.get('website_track')): + event._update_website_menu_entry('website_track', 'track_menu_ids', '_get_track_menu_entries') + if event.menu_id and (not menus_update_by_field or event in menus_update_by_field.get('website_track_proposal')): + event._update_website_menu_entry('website_track_proposal', 'track_proposal_menu_ids', '_get_track_proposal_menu_entries') + + def _get_menu_type_field_matching(self): + res = super(Event, self)._get_menu_type_field_matching() + res['track_proposal'] = 'website_track_proposal' + return res + + def _get_track_menu_entries(self): + """ Method returning menu entries to display on the website view of the + event, possibly depending on some options in inheriting modules. + + Each menu entry is a tuple containing : + * name: menu item name + * url: if set, url to a route (do not use xml_id in that case); + * xml_id: template linked to the page (do not use url in that case); + * menu_type: key linked to the menu, used to categorize the created + website.event.menu; + """ + self.ensure_one() + return [ + (_('Talks'), '/event/%s/track' % slug(self), False, 10, 'track'), + (_('Agenda'), '/event/%s/agenda' % slug(self), False, 70, 'track') + ] + + def _get_track_proposal_menu_entries(self): + """ See website_event_track._get_track_menu_entries() """ + self.ensure_one() + return [(_('Talk Proposals'), '/event/%s/track_proposal' % slug(self), False, 15, 'track_proposal')] diff --git a/addons/website_event_track/models/event_sponsor.py b/addons/website_event_track/models/event_sponsor.py new file mode 100644 index 00000000..403134e0 --- /dev/null +++ b/addons/website_event_track/models/event_sponsor.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.modules.module import get_resource_path + + +class Sponsor(models.Model): + _name = "event.sponsor" + _description = 'Event Sponsor' + _order = "sequence, sponsor_type_id" + _rec_name = 'name' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + event_id = fields.Many2one('event.event', 'Event', required=True) + sponsor_type_id = fields.Many2one('event.sponsor.type', 'Sponsoring Type', required=True) + url = fields.Char('Sponsor Website', compute='_compute_url', readonly=False, store=True) + sequence = fields.Integer('Sequence') + active = fields.Boolean(default=True) + # contact information + partner_id = fields.Many2one('res.partner', 'Sponsor/Customer', required=True) + partner_name = fields.Char('Name', related='partner_id.name') + partner_email = fields.Char('Email', related='partner_id.email') + partner_phone = fields.Char('Phone', related='partner_id.phone') + partner_mobile = fields.Char('Mobile', related='partner_id.mobile') + name = fields.Char('Sponsor Name', compute='_compute_name', readonly=False, store=True) + email = fields.Char('Sponsor Email', compute='_compute_email', readonly=False, store=True) + phone = fields.Char('Sponsor Phone', compute='_compute_phone', readonly=False, store=True) + mobile = fields.Char('Sponsor Mobile', compute='_compute_mobile', readonly=False, store=True) + # image + image_512 = fields.Image( + string="Logo", max_width=512, max_height=512, + compute='_compute_image_512', readonly=False, store=True) + image_256 = fields.Image("Image 256", related="image_512", max_width=256, max_height=256, store=False) + image_128 = fields.Image("Image 128", related="image_512", max_width=128, max_height=128, store=False) + website_image_url = fields.Char( + string='Image URL', + compute='_compute_website_image_url', compute_sudo=True, store=False) + + @api.depends('partner_id') + def _compute_url(self): + for sponsor in self: + if sponsor.partner_id.website or not sponsor.url: + sponsor.url = sponsor.partner_id.website + + @api.depends('partner_id') + def _compute_name(self): + self._synchronize_with_partner('name') + + @api.depends('partner_id') + def _compute_email(self): + self._synchronize_with_partner('email') + + @api.depends('partner_id') + def _compute_phone(self): + self._synchronize_with_partner('phone') + + @api.depends('partner_id') + def _compute_mobile(self): + self._synchronize_with_partner('mobile') + + @api.depends('partner_id') + def _compute_image_512(self): + self._synchronize_with_partner('image_512') + + @api.depends('image_256', 'partner_id.image_256') + def _compute_website_image_url(self): + for sponsor in self: + if sponsor.image_256: + sponsor.website_image_url = self.env['website'].image_url(sponsor, 'image_256', size=256) + elif sponsor.partner_id.image_256: + sponsor.website_image_url = self.env['website'].image_url(sponsor.partner_id, 'image_256', size=256) + else: + sponsor.website_image_url = get_resource_path('website_event_track', 'static/src/img', 'event_sponsor_default_%d.png' % (sponsor.id % 1)) + + def _synchronize_with_partner(self, fname): + """ Synchronize with partner if not set. Setting a value does not write + on partner as this may be event-specific information. """ + for sponsor in self: + if not sponsor[fname]: + sponsor[fname] = sponsor.partner_id[fname] + + # ------------------------------------------------------------ + # MESSAGING + # ------------------------------------------------------------ + + def _message_get_suggested_recipients(self): + recipients = super(Sponsor, self)._message_get_suggested_recipients() + for sponsor in self: + if sponsor.partner_id: + sponsor._message_add_suggested_recipient( + recipients, + partner=sponsor.partner_id, + reason=_('Sponsor') + ) + return recipients diff --git a/addons/website_event_track/models/event_sponsor_type.py b/addons/website_event_track/models/event_sponsor_type.py new file mode 100644 index 00000000..8eb3eb71 --- /dev/null +++ b/addons/website_event_track/models/event_sponsor_type.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class SponsorType(models.Model): + _name = "event.sponsor.type" + _description = 'Event Sponsor Type' + _order = "sequence" + + name = fields.Char('Sponsor Type', required=True, translate=True) + sequence = fields.Integer('Sequence') + display_ribbon_style = fields.Selection([ + ('no_ribbon', 'No Ribbon'), + ('Gold', 'Gold'), + ('Silver', 'Silver'), + ('Bronze', 'Bronze')], string='Ribbon Style') diff --git a/addons/website_event_track/models/event_track.py b/addons/website_event_track/models/event_track.py new file mode 100644 index 00000000..ea626184 --- /dev/null +++ b/addons/website_event_track/models/event_track.py @@ -0,0 +1,508 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta +from pytz import utc +from random import randint + +from odoo import api, fields, models +from odoo.addons.http_routing.models.ir_http import slug +from odoo.osv import expression +from odoo.tools.mail import is_html_empty +from odoo.tools.translate import _, html_translate + + +class Track(models.Model): + _name = "event.track" + _description = 'Event Track' + _order = 'priority, date' + _inherit = ['mail.thread', 'mail.activity.mixin', 'website.seo.metadata', 'website.published.mixin'] + + @api.model + def _get_default_stage_id(self): + return self.env['event.track.stage'].search([], limit=1).id + + # description + name = fields.Char('Title', required=True, translate=True) + event_id = fields.Many2one('event.event', 'Event', required=True) + active = fields.Boolean(default=True) + 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') + tag_ids = fields.Many2many('event.track.tag', string='Tags') + description = fields.Html(translate=html_translate, sanitize_attributes=False, sanitize_form=False) + color = fields.Integer('Color') + priority = fields.Selection([ + ('0', 'Low'), ('1', 'Medium'), + ('2', 'High'), ('3', 'Highest')], + 'Priority', required=True, default='1') + # management + stage_id = fields.Many2one( + 'event.track.stage', string='Stage', ondelete='restrict', + index=True, copy=False, default=_get_default_stage_id, + group_expand='_read_group_stage_ids', + required=True, tracking=True) + is_accepted = fields.Boolean('Is Accepted', related='stage_id.is_accepted', readonly=True) + kanban_state = fields.Selection([ + ('normal', 'Grey'), + ('done', 'Green'), + ('blocked', 'Red')], string='Kanban State', + copy=False, default='normal', required=True, tracking=True, + help="A track's kanban state indicates special situations affecting it:\n" + " * Grey is the default situation\n" + " * Red indicates something is preventing the progress of this track\n" + " * Green indicates the track is ready to be pulled to the next stage") + # speaker + partner_id = fields.Many2one('res.partner', 'Speaker') + partner_name = fields.Char( + string='Name', compute='_compute_partner_name', + readonly=False, store=True, tracking=10) + partner_email = fields.Char( + string='Email', compute='_compute_partner_email', + readonly=False, store=True, tracking=20) + partner_phone = fields.Char( + string='Phone', compute='_compute_partner_phone', + readonly=False, store=True, tracking=30) + partner_biography = fields.Html( + string='Biography', compute='_compute_partner_biography', + readonly=False, store=True) + partner_function = fields.Char( + 'Job Position', related='partner_id.function', + compute_sudo=True, readonly=True) + partner_company_name = fields.Char( + 'Company Name', related='partner_id.parent_name', + compute_sudo=True, readonly=True) + image = fields.Image( + string="Speaker Photo", compute="_compute_speaker_image", + readonly=False, store=True, + max_width=256, max_height=256) + location_id = fields.Many2one('event.track.location', 'Location') + # time information + date = fields.Datetime('Track Date') + date_end = fields.Datetime('Track End Date', compute='_compute_end_date', store=True) + duration = fields.Float('Duration', default=1.5, help="Track duration in hours.") + is_track_live = fields.Boolean( + 'Is Track Live', compute='_compute_track_time_data', + help="Track has started and is ongoing") + is_track_soon = fields.Boolean( + 'Is Track Soon', compute='_compute_track_time_data', + help="Track begins soon") + is_track_today = fields.Boolean( + 'Is Track Today', compute='_compute_track_time_data', + help="Track begins today") + is_track_upcoming = fields.Boolean( + 'Is Track Upcoming', compute='_compute_track_time_data', + help="Track is not yet started") + is_track_done = fields.Boolean( + 'Is Track Done', compute='_compute_track_time_data', + help="Track is finished") + track_start_remaining = fields.Integer( + 'Minutes before track starts', compute='_compute_track_time_data', + help="Remaining time before track starts (seconds)") + track_start_relative = fields.Integer( + 'Minutes compare to track start', compute='_compute_track_time_data', + help="Relative time compared to track start (seconds)") + # frontend description + website_image = fields.Image(string="Website Image", max_width=1024, max_height=1024) + website_image_url = fields.Char( + string='Image URL', compute='_compute_website_image_url', + compute_sudo=True, store=False) + # wishlist / visitors management + event_track_visitor_ids = fields.One2many( + 'event.track.visitor', 'track_id', string="Track Visitors", + groups="event.group_event_user") + is_reminder_on = fields.Boolean('Is Reminder On', compute='_compute_is_reminder_on') + wishlist_visitor_ids = fields.Many2many( + 'website.visitor', string="Visitor Wishlist", + compute="_compute_wishlist_visitor_ids", compute_sudo=True, + search="_search_wishlist_visitor_ids", + groups="event.group_event_user") + wishlist_visitor_count = fields.Integer( + string="# Wishlisted", + compute="_compute_wishlist_visitor_ids", compute_sudo=True, + groups="event.group_event_user") + wishlisted_by_default = fields.Boolean( + string='Always Wishlisted', + help="""If set, the talk will be starred for each attendee registered to the event. The attendee won't be able to un-star this talk.""") + # Call to action + website_cta = fields.Boolean('Magic Button') + website_cta_title = fields.Char('Button Title') + website_cta_url = fields.Char('Button Target URL') + website_cta_delay = fields.Integer('Button appears') + # time information for CTA + is_website_cta_live = fields.Boolean( + 'Is CTA Live', compute='_compute_cta_time_data', + help="CTA button is available") + website_cta_start_remaining = fields.Integer( + 'Minutes before CTA starts', compute='_compute_cta_time_data', + help="Remaining time before CTA starts (seconds)") + + @api.depends('name') + def _compute_website_url(self): + super(Track, self)._compute_website_url() + for track in self: + if track.id: + track.website_url = '/event/%s/track/%s' % (slug(track.event_id), slug(track)) + + # SPEAKER + + @api.depends('partner_id') + def _compute_partner_name(self): + for track in self: + if not track.partner_name or track.partner_id: + track.partner_name = track.partner_id.name + + @api.depends('partner_id') + def _compute_partner_email(self): + for track in self: + if not track.partner_email or track.partner_id: + track.partner_email = track.partner_id.email + + @api.depends('partner_id') + def _compute_partner_phone(self): + for track in self: + if not track.partner_phone or track.partner_id: + track.partner_phone = track.partner_id.phone + + @api.depends('partner_id') + def _compute_partner_biography(self): + for track in self: + if not track.partner_biography: + track.partner_biography = track.partner_id.website_description + elif track.partner_id and is_html_empty(track.partner_biography) and \ + not is_html_empty(track.partner_id.website_description): + track.partner_biography = track.partner_id.website_description + + @api.depends('partner_id') + def _compute_speaker_image(self): + for track in self: + if not track.image: + track.image = track.partner_id.image_256 + + # TIME + + @api.depends('date', 'duration') + def _compute_end_date(self): + for track in self: + if track.date: + delta = timedelta(minutes=60 * track.duration) + track.date_end = track.date + delta + else: + track.date_end = False + + + # FRONTEND DESCRIPTION + + @api.depends('image', 'partner_id.image_256') + def _compute_website_image_url(self): + for track in self: + if track.website_image: + track.website_image_url = self.env['website'].image_url(track, 'website_image', size=1024) + else: + track.website_image_url = '/website_event_track/static/src/img/event_track_default_%d.jpeg' % (track.id % 2) + + # WISHLIST / VISITOR MANAGEMENT + + @api.depends('wishlisted_by_default', 'event_track_visitor_ids.visitor_id', + 'event_track_visitor_ids.partner_id', 'event_track_visitor_ids.is_wishlisted', + 'event_track_visitor_ids.is_blacklisted') + @api.depends_context('uid') + def _compute_is_reminder_on(self): + current_visitor = self.env['website.visitor']._get_visitor_from_request(force_create=False) + if self.env.user._is_public() and not current_visitor: + for track in self: + track.is_reminder_on = track.wishlisted_by_default + else: + if self.env.user._is_public(): + domain = [('visitor_id', '=', current_visitor.id)] + elif current_visitor: + domain = [ + '|', + ('partner_id', '=', self.env.user.partner_id.id), + ('visitor_id', '=', current_visitor.id) + ] + else: + domain = [('partner_id', '=', self.env.user.partner_id.id)] + + event_track_visitors = self.env['event.track.visitor'].sudo().search_read( + expression.AND([ + domain, + [('track_id', 'in', self.ids)] + ]), fields=['track_id', 'is_wishlisted', 'is_blacklisted'] + ) + + wishlist_map = { + track_visitor['track_id'][0]: { + 'is_wishlisted': track_visitor['is_wishlisted'], + 'is_blacklisted': track_visitor['is_blacklisted'] + } for track_visitor in event_track_visitors + } + for track in self: + if wishlist_map.get(track.id): + track.is_reminder_on = wishlist_map.get(track.id)['is_wishlisted'] or (track.wishlisted_by_default and not wishlist_map[track.id]['is_blacklisted']) + else: + track.is_reminder_on = track.wishlisted_by_default + + @api.depends('event_track_visitor_ids.visitor_id', 'event_track_visitor_ids.is_wishlisted') + def _compute_wishlist_visitor_ids(self): + results = self.env['event.track.visitor'].read_group( + [('track_id', 'in', self.ids), ('is_wishlisted', '=', True)], + ['track_id', 'visitor_id:array_agg'], + ['track_id'] + ) + visitor_ids_map = {result['track_id'][0]: result['visitor_id'] for result in results} + for track in self: + track.wishlist_visitor_ids = visitor_ids_map.get(track.id, []) + track.wishlist_visitor_count = len(visitor_ids_map.get(track.id, [])) + + def _search_wishlist_visitor_ids(self, operator, operand): + if operator == "not in": + raise NotImplementedError("Unsupported 'Not In' operation on track wishlist visitors") + + track_visitors = self.env['event.track.visitor'].sudo().search([ + ('visitor_id', operator, operand), + ('is_wishlisted', '=', True) + ]) + return [('id', 'in', track_visitors.track_id.ids)] + + # TIME + + @api.depends('date', 'date_end') + def _compute_track_time_data(self): + """ Compute start and remaining time for track itself. Do everything in + UTC as we compute only time deltas here. """ + now_utc = utc.localize(fields.Datetime.now().replace(microsecond=0)) + for track in self: + if not track.date: + track.is_track_live = track.is_track_soon = track.is_track_today = track.is_track_upcoming = track.is_track_done = False + track.track_start_relative = track.track_start_remaining = 0 + continue + date_begin_utc = utc.localize(track.date, is_dst=False) + date_end_utc = utc.localize(track.date_end, is_dst=False) + track.is_track_live = date_begin_utc <= now_utc < date_end_utc + track.is_track_soon = (date_begin_utc - now_utc).total_seconds() < 30*60 if date_begin_utc > now_utc else False + track.is_track_today = date_begin_utc.date() == now_utc.date() + track.is_track_upcoming = date_begin_utc > now_utc + track.is_track_done = date_end_utc <= now_utc + if date_begin_utc >= now_utc: + track.track_start_relative = int((date_begin_utc - now_utc).total_seconds()) + track.track_start_remaining = track.track_start_relative + else: + track.track_start_relative = int((now_utc - date_begin_utc).total_seconds()) + track.track_start_remaining = 0 + + @api.depends('date', 'date_end', 'website_cta', 'website_cta_delay') + def _compute_cta_time_data(self): + """ Compute start and remaining time for track itself. Do everything in + UTC as we compute only time deltas here. """ + now_utc = utc.localize(fields.Datetime.now().replace(microsecond=0)) + for track in self: + if not track.website_cta: + track.is_website_cta_live = track.website_cta_start_remaining = False + continue + + date_begin_utc = utc.localize(track.date, is_dst=False) + timedelta(minutes=track.website_cta_delay or 0) + date_end_utc = utc.localize(track.date_end, is_dst=False) + track.is_website_cta_live = date_begin_utc <= now_utc <= date_end_utc + if date_begin_utc >= now_utc: + td = date_begin_utc - now_utc + track.website_cta_start_remaining = int(td.total_seconds()) + else: + track.website_cta_start_remaining = 0 + + # ------------------------------------------------------------ + # CRUD + # ------------------------------------------------------------ + + @api.model_create_multi + def create(self, vals_list): + for values in vals_list: + if values.get('website_cta_url'): + values['website_cta_url'] = self.env['res.partner']._clean_website(values['website_cta_url']) + + tracks = super(Track, self).create(vals_list) + + for track in tracks: + email_values = {} if self.env.user.email else {'email_from': self.env.company.catchall_formatted} + track.event_id.message_post_with_view( + 'website_event_track.event_track_template_new', + values={'track': track}, + subject=track.name, + subtype_id=self.env.ref('website_event_track.mt_event_track').id, + **email_values, + ) + track._synchronize_with_stage(track.stage_id) + + return tracks + + def write(self, vals): + if vals.get('website_cta_url'): + vals['website_cta_url'] = self.env['res.partner']._clean_website(vals['website_cta_url']) + if 'stage_id' in vals and 'kanban_state' not in vals: + vals['kanban_state'] = 'normal' + if vals.get('stage_id'): + stage = self.env['event.track.stage'].browse(vals['stage_id']) + self._synchronize_with_stage(stage) + res = super(Track, self).write(vals) + if vals.get('partner_id'): + self.message_subscribe([vals['partner_id']]) + return res + + @api.model + def _read_group_stage_ids(self, stages, domain, order): + """ Always display all stages """ + return stages.search([], order=order) + + def _synchronize_with_stage(self, stage): + if stage.is_done: + self.is_published = True + elif stage.is_cancel: + self.is_published = False + + # ------------------------------------------------------------ + # MESSAGING + # ------------------------------------------------------------ + + def _track_template(self, changes): + res = super(Track, self)._track_template(changes) + track = self[0] + if 'stage_id' in changes and track.stage_id.mail_template_id: + res['stage_id'] = (track.stage_id.mail_template_id, { + 'composition_mode': 'comment', + 'auto_delete_message': True, + 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'), + 'email_layout_xmlid': 'mail.mail_notification_light' + }) + return res + + def _track_subtype(self, init_values): + self.ensure_one() + if 'kanban_state' in init_values and self.kanban_state == 'blocked': + return self.env.ref('website_event_track.mt_track_blocked') + elif 'kanban_state' in init_values and self.kanban_state == 'done': + return self.env.ref('website_event_track.mt_track_ready') + return super(Track, self)._track_subtype(init_values) + + def _message_get_suggested_recipients(self): + recipients = super(Track, self)._message_get_suggested_recipients() + for track in self: + if track.partner_email and track.partner_email != track.partner_id.email: + track._message_add_suggested_recipient(recipients, email=track.partner_email, reason=_('Speaker Email')) + return recipients + + def _message_post_after_hook(self, message, msg_vals): + if self.partner_email and not self.partner_id: + # we consider that posting a message with a specified recipient (not a follower, a specific one) + # on a document without customer means that it was created through the chatter using + # suggested recipients. This heuristic allows to avoid ugly hacks in JS. + new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.partner_email) + if new_partner: + self.search([ + ('partner_id', '=', False), + ('partner_email', '=', new_partner.email), + ('stage_id.is_cancel', '=', False), + ]).write({'partner_id': new_partner.id}) + return super(Track, self)._message_post_after_hook(message, msg_vals) + + # ------------------------------------------------------------ + # ACTION + # ------------------------------------------------------------ + + def open_track_speakers_list(self): + return { + 'name': _('Speakers'), + 'domain': [('id', 'in', self.mapped('partner_id').ids)], + 'view_mode': 'kanban,form', + 'res_model': 'res.partner', + 'view_id': False, + 'type': 'ir.actions.act_window', + } + + def get_backend_menu_id(self): + return self.env.ref('event.event_main_menu').id + + # ------------------------------------------------------------ + # TOOLS + # ------------------------------------------------------------ + + def _get_event_track_visitors(self, force_create=False): + self.ensure_one() + + force_visitor_create = self.env.user._is_public() + visitor_sudo = self.env['website.visitor']._get_visitor_from_request(force_create=force_visitor_create) + if visitor_sudo: + visitor_sudo._update_visitor_last_visit() + + if self.env.user._is_public(): + domain = [('visitor_id', '=', visitor_sudo.id)] + elif visitor_sudo: + domain = [ + '|', + ('partner_id', '=', self.env.user.partner_id.id), + ('visitor_id', '=', visitor_sudo.id) + ] + else: + domain = [('partner_id', '=', self.env.user.partner_id.id)] + + track_visitors = self.env['event.track.visitor'].sudo().search( + expression.AND([domain, [('track_id', 'in', self.ids)]]) + ) + missing = self - track_visitors.track_id + if missing and force_create: + track_visitors += self.env['event.track.visitor'].sudo().create([{ + 'visitor_id': visitor_sudo.id, + 'partner_id': self.env.user.partner_id.id if not self.env.user._is_public() else False, + 'track_id': track.id, + } for track in missing]) + + return track_visitors + + def _get_track_suggestions(self, restrict_domain=None, limit=None): + """ Returns the next tracks suggested after going to the current one + given by self. Tracks always belong to the same event. + + Heuristic is + + * live first; + * then ordered by start date, finished being sent to the end; + * wishlisted (manually or by default); + * tag matching with current track; + * location matching with current track; + * finally a random to have an "equivalent wave" randomly given; + + :param restrict_domain: an additional domain to restrict candidates; + :param limit: number of tracks to return; + """ + self.ensure_one() + + base_domain = [ + '&', + ('event_id', '=', self.event_id.id), + ('id', '!=', self.id), + ] + if restrict_domain: + base_domain = expression.AND([ + base_domain, + restrict_domain + ]) + + track_candidates = self.search(base_domain, limit=None, order='date asc') + if not track_candidates: + return track_candidates + + track_candidates = track_candidates.sorted( + lambda track: + (track.is_published, + track.track_start_remaining == 0 # First get the tracks that started less than 10 minutes ago ... + and track.track_start_relative < (10 * 60) + and not track.is_track_done, # ... AND not finished + track.track_start_remaining > 0, # Then the one that will begin later (the sooner come first) + -1 * track.track_start_remaining, + track.is_reminder_on, + not track.wishlisted_by_default, + len(track.tag_ids & self.tag_ids), + track.location_id == self.location_id, + randint(0, 20), + ), reverse=True + ) + + return track_candidates[:limit] diff --git a/addons/website_event_track/models/event_track_location.py b/addons/website_event_track/models/event_track_location.py new file mode 100644 index 00000000..4c56e837 --- /dev/null +++ b/addons/website_event_track/models/event_track_location.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class TrackLocation(models.Model): + _name = "event.track.location" + _description = 'Event Track Location' + + name = fields.Char('Location', required=True) diff --git a/addons/website_event_track/models/event_track_stage.py b/addons/website_event_track/models/event_track_stage.py new file mode 100644 index 00000000..9e1a177c --- /dev/null +++ b/addons/website_event_track/models/event_track_stage.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class TrackStage(models.Model): + _name = 'event.track.stage' + _description = 'Event Track Stage' + _order = 'sequence, id' + + name = fields.Char(string='Stage Name', required=True, translate=True) + sequence = fields.Integer(string='Sequence', default=1) + mail_template_id = fields.Many2one( + 'mail.template', string='Email Template', + domain=[('model', '=', 'event.track')], + help="If set an email will be sent to the customer when the track reaches this step.") + fold = fields.Boolean( + string='Folded in Kanban', + help='This stage is folded in the kanban view when there are no records in that stage to display.') + is_accepted = fields.Boolean( + string='Accepted Stage', + help='Accepted tracks are displayed in agenda views but not accessible.') + is_done = fields.Boolean( + string='Done Stage', + help='Done tracks are automatically published so that they are available in frontend.') + is_cancel = fields.Boolean(string='Canceled Stage') + is_done = fields.Boolean() + color = fields.Integer(string='Color') diff --git a/addons/website_event_track/models/event_track_tag.py b/addons/website_event_track/models/event_track_tag.py new file mode 100644 index 00000000..6efcba1e --- /dev/null +++ b/addons/website_event_track/models/event_track_tag.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from random import randint + +from odoo import fields, models + + +class TrackTag(models.Model): + _name = "event.track.tag" + _description = 'Event Track Tag' + _order = "category_id, sequence, name" + + def _default_color(self): + return randint(1, 11) + + name = fields.Char('Tag Name', required=True) + track_ids = fields.Many2many('event.track', string='Tracks') + color = fields.Integer( + string='Color Index', default=lambda self: self._default_color(), + help="Note that colorless tags won't be available on the website.") + sequence = fields.Integer('Sequence', default=10) + category_id = fields.Many2one('event.track.tag.category', string="Category", ondelete="set null") + + _sql_constraints = [ + ('name_uniq', 'unique (name)', "Tag name already exists !"), + ] diff --git a/addons/website_event_track/models/event_track_tag_category.py b/addons/website_event_track/models/event_track_tag_category.py new file mode 100644 index 00000000..05f14e7a --- /dev/null +++ b/addons/website_event_track/models/event_track_tag_category.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class TrackTagCategory(models.Model): + _name = "event.track.tag.category" + _description = 'Event Track Tag Category' + _order = "sequence" + + name = fields.Char("Name", required=True, translate=True) + sequence = fields.Integer('Sequence', default=10) + tag_ids = fields.One2many('event.track.tag', 'category_id', string="Tags") diff --git a/addons/website_event_track/models/event_track_visitor.py b/addons/website_event_track/models/event_track_visitor.py new file mode 100644 index 00000000..b84d7c6a --- /dev/null +++ b/addons/website_event_track/models/event_track_visitor.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class TrackVisitor(models.Model): + """ Table linking track and visitors. """ + _name = 'event.track.visitor' + _description = 'Track / Visitor Link' + _table = 'event_track_visitor' + _rec_name = 'track_id' + _order = 'track_id' + + partner_id = fields.Many2one( + 'res.partner', string='Partner', compute='_compute_partner_id', + index=True, ondelete='set null', readonly=False, store=True) + visitor_id = fields.Many2one( + 'website.visitor', string='Visitor', index=True, ondelete='cascade') + track_id = fields.Many2one( + 'event.track', string='Track', + index=True, required=True, ondelete='cascade') + is_wishlisted = fields.Boolean(string="Is Wishlisted") + is_blacklisted = fields.Boolean(string="Is reminder off", help="As key track cannot be un-wishlisted, this field store the partner choice to remove the reminder for key tracks.") + + @api.depends('visitor_id') + def _compute_partner_id(self): + for track_visitor in self: + if track_visitor.visitor_id.partner_id and not track_visitor.partner_id: + track_visitor.partner_id = track_visitor.visitor_id.partner_id + elif not track_visitor.partner_id: + track_visitor.partner_id = False diff --git a/addons/website_event_track/models/event_type.py b/addons/website_event_track/models/event_type.py new file mode 100644 index 00000000..23be8452 --- /dev/null +++ b/addons/website_event_track/models/event_type.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class EventType(models.Model): + _inherit = 'event.type' + + website_track = fields.Boolean( + string='Tracks on Website', compute='_compute_website_menu_data', + readonly=False, store=True) + website_track_proposal = fields.Boolean( + string='Tracks Proposals on Website', compute='_compute_website_menu_data', + readonly=False, store=True) + + @api.depends('website_menu') + def _compute_website_menu_data(self): + """ Simply activate or de-activate all menus at once. """ + for event_type in self: + event_type.website_track = event_type.website_menu + event_type.website_track_proposal = event_type.website_menu diff --git a/addons/website_event_track/models/res_config_settings.py b/addons/website_event_track/models/res_config_settings.py new file mode 100644 index 00000000..8d87e90d --- /dev/null +++ b/addons/website_event_track/models/res_config_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + events_app_name = fields.Char('Events App Name', related='website_id.events_app_name', readonly=False) diff --git a/addons/website_event_track/models/website.py b/addons/website_event_track/models/website.py new file mode 100644 index 00000000..f6f21d5e --- /dev/null +++ b/addons/website_event_track/models/website.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +from PIL import Image + +from odoo import api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools import ImageProcess +from odoo.tools.translate import _ + + +class Website(models.Model): + _inherit = "website" + + app_icon = fields.Image(string='Website App Icon', compute='_compute_app_icon', store=True, readonly=True, help='This field holds the image used as mobile app icon on the website (PNG format).') + events_app_name = fields.Char(string='Events App Name', compute='_compute_events_app_name', store=True, readonly=False, help="This fields holds the Event's Progressive Web App name.") + + @api.depends('name') + def _compute_events_app_name(self): + for website in self: + if not website.events_app_name: + website.events_app_name = _('%s Events') % website.name + + @api.constrains('events_app_name') + def _check_events_app_name(self): + for website in self: + if not website.events_app_name: + raise ValidationError(_('"Events App Name" field is required.')) + + @api.depends('favicon') + def _compute_app_icon(self): + """ Computes a squared image based on the favicon to be used as mobile webapp icon. + App Icon should be in PNG format and size of at least 512x512. + """ + for website in self: + if not website.favicon: + website.app_icon = False + continue + image = ImageProcess(website.favicon) + w, h = image.image.size + square_size = w if w > h else h + image.crop_resize(square_size, square_size) + image.image = image.image.resize((512, 512)) + image.operationsCount += 1 + website.app_icon = image.image_base64(output_format='PNG') 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 00000000..70561b0a --- /dev/null +++ b/addons/website_event_track/models/website_event_menu.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class EventMenu(models.Model): + _inherit = "website.event.menu" + + menu_type = fields.Selection( + selection_add=[('track', 'Event Tracks Menus'), ('track_proposal', 'Event Proposals Menus')], + ondelete={'track': 'cascade', 'track_proposal': 'cascade'}) diff --git a/addons/website_event_track/models/website_menu.py b/addons/website_event_track/models/website_menu.py new file mode 100644 index 00000000..703c3afb --- /dev/null +++ b/addons/website_event_track/models/website_menu.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class WebsiteMenu(models.Model): + _inherit = "website.menu" + + def unlink(self): + """ Override to synchronize event configuration fields with menu deletion. + This should be cleaned in upcoming versions. """ + event_updates = {} + website_event_menus = self.env['website.event.menu'].search([('menu_id', 'in', self.ids)]) + for event_menu in website_event_menus: + to_update = event_updates.setdefault(event_menu.event_id, list()) + # specifically check for /track in menu URL; to avoid unchecking track field when removing + # agenda page that has also menu_type='track' + if event_menu.menu_type == 'track' and '/track' in event_menu.menu_id.url: + to_update.append('website_track') + + # call super that resumes the unlink of menus entries (including website event menus) + res = super(WebsiteMenu, self).unlink() + + # update events + for event, to_update in event_updates.items(): + if to_update: + event.write(dict((fname, False) for fname in to_update)) + + return res diff --git a/addons/website_event_track/models/website_visitor.py b/addons/website_event_track/models/website_visitor.py new file mode 100644 index 00000000..18072c86 --- /dev/null +++ b/addons/website_event_track/models/website_visitor.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class WebsiteVisitor(models.Model): + _name = 'website.visitor' + _inherit = ['website.visitor'] + + event_track_visitor_ids = fields.One2many( + 'event.track.visitor', 'visitor_id', string="Track Visitors", + groups='event.group_event_user') + event_track_wishlisted_ids = fields.Many2many( + 'event.track', string="Wishlisted Tracks", + compute="_compute_event_track_wishlisted_ids", compute_sudo=True, + search="_search_event_track_wishlisted_ids", + groups="event.group_event_user") + event_track_wishlisted_count = fields.Integer( + string="# Wishlisted", + compute="_compute_event_track_wishlisted_ids", compute_sudo=True, + groups='event.group_event_user') + + @api.depends('parent_id', 'event_track_visitor_ids.track_id', 'event_track_visitor_ids.is_wishlisted') + def _compute_event_track_wishlisted_ids(self): + # include parent's track visitors in a visitor o2m field. We don't add + # child one as child should not have track visitors (moved to the parent) + all_visitors = self + self.parent_id + results = self.env['event.track.visitor'].read_group( + [('visitor_id', 'in', all_visitors.ids), ('is_wishlisted', '=', True)], + ['visitor_id', 'track_id:array_agg'], + ['visitor_id'] + ) + track_ids_map = {result['visitor_id'][0]: result['track_id'] for result in results} + for visitor in self: + visitor_track_ids = track_ids_map.get(visitor.id, []) + parent_track_ids = track_ids_map.get(visitor.parent_id.id, []) + visitor.event_track_wishlisted_ids = visitor_track_ids + [track_id for track_id in parent_track_ids if track_id not in visitor_track_ids] + visitor.event_track_wishlisted_count = len(visitor.event_track_wishlisted_ids) + + def _search_event_track_wishlisted_ids(self, operator, operand): + """ Search visitors with terms on wishlisted tracks. E.g. [('event_track_wishlisted_ids', + 'in', [1, 2])] should return visitors having wishlisted tracks 1, 2 as + well as their children for notification purpose. """ + if operator == "not in": + raise NotImplementedError("Unsupported 'Not In' operation on track wishlist visitors") + + track_visitors = self.env['event.track.visitor'].sudo().search([ + ('track_id', operator, operand), + ('is_wishlisted', '=', True) + ]) + if track_visitors: + visitors = track_visitors.visitor_id + # search children, even archived one, to contact them + children = self.env['website.visitor'].with_context( + active_test=False + ).sudo().search([('parent_id', 'in', visitors.ids)]) + visitor_ids = (visitors + children).ids + else: + visitor_ids = [] + + return [('id', 'in', visitor_ids)] + + def _link_to_partner(self, partner, update_values=None): + """ Propagate partner update to track_visitor records """ + if partner: + track_visitor_wo_partner = self.event_track_visitor_ids.filtered(lambda track_visitor: not track_visitor.partner_id) + if track_visitor_wo_partner: + track_visitor_wo_partner.partner_id = partner + super(WebsiteVisitor, self)._link_to_partner(partner, update_values=update_values) + + def _link_to_visitor(self, target, keep_unique=True): + """ Override linking process to link wishlist to the final visitor. """ + self.event_track_visitor_ids.visitor_id = target.id + return super(WebsiteVisitor, self)._link_to_visitor(target, keep_unique=keep_unique) |
