Начальное наполнение

This commit is contained in:
parent 18aaa4c431
commit 666b0fe265
152 changed files with 107454 additions and 1 deletions

115
README.md
View File

@ -1,2 +1,115 @@
# website_event
Organize Events, Trainings & Webinars
-------------------------------------
### Schedule, Promote, Sell, Organize
Organize, promote and sell events online. Whether you organize meetings, conferences, trainings or webinars, Odoo gives you all the features you need to manage your events.
Create Awesome Event Pages
--------------------------
### Get rid of old WYSIWYG editors
Create beautiful event pages by drag & droping well designed *'Building Blocks'*. Publish event photos, speakers, schedule, etc.
Odoo's unique *'edit inline'* approach makes website creation surprisingly easy. "Want to introduce a speaker? to change the price of a ticket? to update a banner? promote sponsors?" just click and change.
Sell Tickets Online
-------------------
### Automate the registration and payment process
Sell registrations to your event with the multi-ticketing feature. Events can be free or for a fee. Attendees can pay online with a credit card or on invoice, based on your configuration.
Boost your sales with early-bird prices, special conditions for members, or extra services with multiple tickets.
A Clean Google Analytics Integration
------------------------------------
### Control your sales funnel with Google Analytics
Get a clear visibility of your sales funnel. Odoo's Google Analytics trackers are configured by default to track all kind of events related to shopping carts, call-to-actions, etc.
As Odoo marketing tools (mass mailing, campaigns, etc) are also linked with Google Analytics, you get a full view of your business.
Promote Events Efficiently
--------------------------
### Mass Mailing & Social Media
Use the segmentation, the social network integration and mass mailing features to promote your events to the right audience. Setup automated emails to attendees to send them last minute details.
Designer-Friendly Themes
------------------------
### Designers love working on Odoo
Themes are awesome and easy to design. You don't need to develop to create new pages, themes or building blocks. We use a clean HTML structure, a [bootstrap](http://getbootstrap.com/) CSS and our modularity allows to distribute your themes easily.
The building block approach allows the website to stay clean after the end-users start creating new contents.
Make Your Event More Visible
----------------------------
### SEO tools at your finger tips
SEO tools are ready to use, with no configuration required. Odoo suggests keywords according to Google most searched terms, Google Analytics tracks your shopping cart events and sitemap are created automatically.
We even do structured content automatically to promote your events and products efficiently in Google.
Leverage Social Media
---------------------
### Optimize: from Ads to Conversions
Create new landing pages easily with the Odoo inline editing feature. Send visitors of your different marketing campaigns to event landing pages to optimize conversions.
And Much More...
----------------
### Schedule
- Calendar of Events
- Publish related documents
- Ressources allocation
- Automate purchases (catering...)
- Multiple locations and organizers
- Mobile Interface
### Sell
- Online or offline sales
- Automated invoicing
- Cancellation policies
- Specific prices for members
- Dashboards and reporting
### Organize
- Advanced Planification
- Print Badges
- Automate Follow-up Emails
- Min/Max capacities
- Manage classes and ressources
- Create group of attendees
- Automate statisfaction surveys
Fully Integrated With Others Apps
---------------------------------
### Get hundreds of open source apps for free
### eCommerce
Promote products, sell online, optimize visitors' shopping experiences.
### Blog
Write news, attract new visitors, build customer loyalty.
### Our Team
Create a great "About us" page by presenting your team efficiently.

5
__init__.py Normal file
View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models

78
__manifest__.py Normal file
View File

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Events',
'version': '1.4',
'category': 'Marketing/Events',
'sequence': 140,
'summary': 'Publish events, sell tickets',
'website': 'https://www.odoo.com/app/events',
'depends': [
'event',
'website',
'website_partner',
'website_mail',
],
'data': [
'data/event_data.xml',
'data/website_snippet_data.xml',
'views/event_snippets.xml',
'views/snippets/s_events.xml',
'views/snippets/snippets.xml',
'views/event_templates_list.xml',
'views/event_templates_svg.xml',
'views/event_templates_page.xml',
'views/event_templates_page_registration.xml',
'views/event_templates_page_misc.xml',
'views/event_templates_widgets.xml',
'views/event_event_views.xml',
'views/event_registration_views.xml',
'views/event_question_views.xml',
'views/event_registration_answer_views.xml',
'views/event_tag_category_views.xml',
'views/event_tag_views.xml',
'views/event_type_views.xml',
'views/website_event_menu_views.xml',
'views/website_visitor_views.xml',
'views/event_menus.xml',
'views/website_pages_views.xml',
'views/event_event_add.xml',
'security/ir.model.access.csv',
'security/event_security.xml',
],
'demo': [
'data/res_partner_demo.xml',
'data/event_demo.xml',
'data/event_question_demo.xml',
'data/event_registration_demo.xml',
'data/event_registration_answer_demo.xml',
],
'application': True,
'assets': {
'web.assets_backend': [
'website_event/static/src/js/tours/**/*',
],
'web.assets_tests': [
'website_event/static/tests/**/*',
],
'web.assets_frontend': [
'website_event/static/src/js/tours/**/*',
'website_event/static/src/scss/event_templates_common.scss',
'website_event/static/src/scss/event_templates_list.scss',
'website_event/static/src/scss/event_templates_page.scss',
'website_event/static/src/js/display_timer_widget.js',
'website_event/static/src/js/register_toaster_widget.js',
'website_event/static/src/js/website_event.js',
'website_event/static/src/js/website_event_ticket_details.js',
],
'website.assets_wysiwyg': [
'/website_event/static/src/snippets/s_events/options.js',
'website_event/static/src/snippets/options.js',
],
'website.assets_editor': [
'website_event/static/src/js/systray_items/*.js',
],
},
'license': 'LGPL-3',
}

4
controllers/__init__.py Normal file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import main
from . import community

13
controllers/community.py Normal file
View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.http import request
class EventCommunityController(http.Controller):
@http.route('/event/<model("event.event"):event>/community', type="http", auth="public", website=True, sitemap=False)
def community(self, event, lang=None, **kwargs):
""" This skeleton route will be overriden in website_event_track_quiz, website_event_meet and website_event_meet_quiz. """
return request.render('website.page_404')

412
controllers/main.py Normal file
View File

@ -0,0 +1,412 @@
# -*- coding: utf-8 -*-
import babel.dates
import re
import werkzeug
from ast import literal_eval
from werkzeug.datastructures import OrderedMultiDict
from werkzeug.exceptions import NotFound
from odoo import fields, http, _
from odoo.addons.website.controllers.main import QueryURL
from odoo.http import request
from odoo.osv import expression
from odoo.tools.misc import get_lang
from odoo.tools import lazy
from odoo.exceptions import UserError
class WebsiteEventController(http.Controller):
def sitemap_event(env, rule, qs):
if not qs or qs.lower() in '/events':
yield {'loc': '/events'}
# ------------------------------------------------------------
# EVENT LIST
# ------------------------------------------------------------
def _get_events_search_options(self, **post):
return {
'displayDescription': False,
'displayDetail': False,
'displayExtraDetail': False,
'displayExtraLink': False,
'displayImage': False,
'allowFuzzy': not post.get('noFuzzy'),
'date': post.get('date'),
'tags': post.get('tags'),
'type': post.get('type'),
'country': post.get('country'),
}
@http.route(['/event', '/event/page/<int:page>', '/events', '/events/page/<int:page>'], type='http', auth="public", website=True, sitemap=sitemap_event)
def events(self, page=1, **searches):
Event = request.env['event.event']
SudoEventType = request.env['event.type'].sudo()
searches.setdefault('search', '')
searches.setdefault('date', 'upcoming')
searches.setdefault('tags', '')
searches.setdefault('type', 'all')
searches.setdefault('country', 'all')
website = request.website
step = 12 # Number of events per page
options = self._get_events_search_options(**searches)
order = 'date_begin'
if searches.get('date', 'upcoming') == 'old':
order = 'date_begin desc'
order = 'is_published desc, ' + order
search = searches.get('search')
event_count, details, fuzzy_search_term = website._search_with_fuzzy("events", search,
limit=page * step, order=order, options=options)
event_details = details[0]
events = event_details.get('results', Event)
events = events[(page - 1) * step:page * step]
# count by domains without self search
domain_search = [('name', 'ilike', fuzzy_search_term or searches['search'])] if searches['search'] else []
no_date_domain = event_details['no_date_domain']
dates = event_details['dates']
for date in dates:
if date[0] not in ['all', 'old']:
date[3] = Event.search_count(expression.AND(no_date_domain) + domain_search + date[2])
no_country_domain = event_details['no_country_domain']
countries = Event.read_group(expression.AND(no_country_domain) + domain_search, ["id", "country_id"],
groupby="country_id", orderby="country_id")
countries.insert(0, {
'country_id_count': sum([int(country['country_id_count']) for country in countries]),
'country_id': ("all", _("All Countries"))
})
search_tags = event_details['search_tags']
current_date = event_details['current_date']
current_type = None
current_country = None
if searches["type"] != 'all':
current_type = SudoEventType.browse(int(searches['type']))
if searches["country"] != 'all' and searches["country"] != 'online':
current_country = request.env['res.country'].browse(int(searches['country']))
pager = website.pager(
url="/event",
url_args=searches,
total=event_count,
page=page,
step=step,
scope=5)
keep = QueryURL('/event', **{
key: value for key, value in searches.items() if (
key == 'search' or
(value != 'upcoming' if key == 'date' else value != 'all'))
})
searches['search'] = fuzzy_search_term or search
values = {
'current_date': current_date,
'current_country': current_country,
'current_type': current_type,
'event_ids': events, # event_ids used in website_event_track so we keep name as it is
'dates': dates,
'categories': request.env['event.tag.category'].search([
('is_published', '=', True), '|', ('website_id', '=', website.id), ('website_id', '=', False)
]),
'countries': countries,
'pager': pager,
'searches': searches,
'search_tags': search_tags,
'keep': keep,
'search_count': event_count,
'original_search': fuzzy_search_term and search,
'website': website
}
if searches['date'] == 'old':
# the only way to display this content is to set date=old so it must be canonical
values['canonical_params'] = OrderedMultiDict([('date', 'old')])
return request.render("website_event.index", values)
# ------------------------------------------------------------
# EVENT PAGE
# ------------------------------------------------------------
@http.route(['''/event/<model("event.event"):event>/page/<path:page>'''], type='http', auth="public", website=True, sitemap=False)
def event_page(self, event, page, **post):
values = {
'event': event,
}
if '.' not in page:
page = 'website_event.%s' % page
try:
# Every event page view should have its own SEO.
values['seo_object'] = request.website.get_template(page)
values['main_object'] = event
except ValueError:
# page not found
values['path'] = re.sub(r"^website_event\.", '', page)
values['from_template'] = 'website_event.default_page' # .strip('website_event.')
page = request.env.user.has_group('website.group_website_designer') and 'website.page_404' or 'http_routing.404'
return request.render(page, values)
@http.route(['''/event/<model("event.event"):event>'''], type='http', auth="public", website=True, sitemap=True)
def event(self, event, **post):
if event.menu_id and event.menu_id.child_id:
target_url = event.menu_id.child_id[0].url
else:
target_url = '/event/%s/register' % str(event.id)
if post.get('enable_editor') == '1':
target_url += '?enable_editor=1'
return request.redirect(target_url)
@http.route(['''/event/<model("event.event"):event>/register'''], type='http', auth="public", website=True, sitemap=False)
def event_register(self, event, **post):
values = self._prepare_event_register_values(event, **post)
return request.render("website_event.event_description_full", values)
def _prepare_event_register_values(self, event, **post):
"""Return the require values to render the template."""
urls = lazy(event._get_event_resource_urls)
return {
'event': event,
'main_object': event,
'range': range,
'google_url': lazy(lambda: urls.get('google_url')),
'iCal_url': lazy(lambda: urls.get('iCal_url')),
'registration_error_code': post.get('registration_error_code'),
}
def _process_tickets_form(self, event, form_details):
""" Process posted data about ticket order. Generic ticket are supported
for event without tickets (generic registration).
:return: list of order per ticket: [{
'id': if of ticket if any (0 if no ticket),
'ticket': browse record of ticket if any (None if no ticket),
'name': ticket name (or generic 'Registration' name if no ticket),
'quantity': number of registrations for that ticket,
}, {...}]
"""
ticket_order = {}
for key, value in form_details.items():
registration_items = key.split('nb_register-')
if len(registration_items) != 2:
continue
ticket_order[int(registration_items[1])] = int(value)
ticket_dict = dict((ticket.id, ticket) for ticket in request.env['event.event.ticket'].sudo().search([
('id', 'in', [tid for tid in ticket_order.keys() if tid]),
('event_id', '=', event.id)
]))
return [{
'id': tid if ticket_dict.get(tid) else 0,
'ticket': ticket_dict.get(tid),
'name': ticket_dict[tid]['name'] if ticket_dict.get(tid) else _('Registration'),
'quantity': count,
} for tid, count in ticket_order.items() if count]
@http.route(['/event/<model("event.event"):event>/registration/new'], type='json', auth="public", methods=['POST'], website=True)
def registration_new(self, event, **post):
tickets = self._process_tickets_form(event, post)
availability_check = True
if event.seats_limited:
ordered_seats = 0
for ticket in tickets:
ordered_seats += ticket['quantity']
if event.seats_available < ordered_seats:
availability_check = False
if not tickets:
return False
default_first_attendee = {}
if not request.env.user._is_public():
default_first_attendee = {
"name": request.env.user.name,
"email": request.env.user.email,
"phone": request.env.user.mobile or request.env.user.phone,
}
else:
visitor = request.env['website.visitor']._get_visitor_from_request()
if visitor.email:
default_first_attendee = {
"name": visitor.display_name,
"email": visitor.email,
"phone": visitor.mobile,
}
return request.env['ir.ui.view']._render_template("website_event.registration_attendee_details", {
'tickets': tickets,
'event': event,
'availability_check': availability_check,
'default_first_attendee': default_first_attendee,
})
def _process_attendees_form(self, event, form_details):
""" Process data posted from the attendee details form.
Extracts question answers:
- For both questions asked 'once_per_order' and questions asked to every attendee
- For questions of type 'simple_choice', extracting the suggested answer id
- For questions of type 'text_box', extracting the text answer of the attendee.
:param form_details: posted data from frontend registration form, like
{'1-name': 'r', '1-email': 'r@r.com', '1-phone': '', '1-event_ticket_id': '1'}
"""
allowed_fields = request.env['event.registration']._get_website_registration_allowed_fields()
registration_fields = {key: v for key, v in request.env['event.registration']._fields.items() if key in allowed_fields}
for ticket_id in list(filter(lambda x: x is not None, [form_details[field] if 'event_ticket_id' in field else None for field in form_details.keys()])):
if int(ticket_id) not in event.event_ticket_ids.ids and len(event.event_ticket_ids.ids) > 0:
raise UserError(_("This ticket is not available for sale for this event"))
registrations = {}
general_answer_ids = []
general_identification_answers = {}
# as we may have several questions populating the same field (e.g: the phone)
# we use this to hold the fields that have already been handled
# goal is to use the answer to the first question of every 'type' (aka name / phone / email / company name)
already_handled_fields_data = {}
for key, value in form_details.items():
if not value:
continue
key_values = key.split('-')
# Special case for handling event_ticket_id data that holds only 2 values
if len(key_values) == 2:
registration_index, field_name = key_values
if field_name not in registration_fields:
continue
registrations.setdefault(registration_index, dict())[field_name] = int(value) or False
continue
registration_index, question_type, question_id = key_values
answer_values = None
if question_type == 'simple_choice':
answer_values = {
'question_id': int(question_id),
'value_answer_id': int(value)
}
else:
answer_values = {
'question_id': int(question_id),
'value_text_box': value
}
if answer_values and not int(registration_index):
general_answer_ids.append((0, 0, answer_values))
elif answer_values:
registrations.setdefault(registration_index, dict())\
.setdefault('registration_answer_ids', list()).append((0, 0, answer_values))
if question_type in ('name', 'email', 'phone', 'company_name')\
and question_type not in already_handled_fields_data.get(registration_index, []):
if question_type not in registration_fields:
continue
field_name = question_type
already_handled_fields_data.setdefault(registration_index, list()).append(field_name)
if not int(registration_index):
general_identification_answers[field_name] = value
else:
registrations.setdefault(registration_index, dict())[field_name] = value
if general_answer_ids:
for registration in registrations.values():
registration.setdefault('registration_answer_ids', list()).extend(general_answer_ids)
if general_identification_answers:
for registration in registrations.values():
registration.update(general_identification_answers)
return list(registrations.values())
def _create_attendees_from_registration_post(self, event, registration_data):
""" Also try to set a visitor (from request) and
a partner (if visitor linked to a user for example). Purpose is to gather
as much informations as possible, notably to ease future communications.
Also try to update visitor informations based on registration info. """
visitor_sudo = request.env['website.visitor']._get_visitor_from_request(force_create=True)
registrations_to_create = []
for registration_values in registration_data:
registration_values['event_id'] = event.id
if not registration_values.get('partner_id') and visitor_sudo.partner_id:
registration_values['partner_id'] = visitor_sudo.partner_id.id
elif not registration_values.get('partner_id'):
registration_values['partner_id'] = False if request.env.user._is_public() else request.env.user.partner_id.id
# update registration based on visitor
registration_values['visitor_id'] = visitor_sudo.id
registrations_to_create.append(registration_values)
return request.env['event.registration'].sudo().create(registrations_to_create)
@http.route(['''/event/<model("event.event"):event>/registration/confirm'''], type='http', auth="public", methods=['POST'], website=True)
def registration_confirm(self, event, **post):
""" Check before creating and finalize the creation of the registrations
that we have enough seats for all selected tickets.
If we don't, the user is instead redirected to page to register with a
formatted error message. """
registrations_data = self._process_attendees_form(event, post)
event_ticket_ids = {registration['event_ticket_id'] for registration in registrations_data}
event_tickets = request.env['event.event.ticket'].browse(event_ticket_ids)
if any(event_ticket.seats_limited and event_ticket.seats_available < len(registrations_data) for event_ticket in event_tickets):
return request.redirect('/event/%s/register?registration_error_code=insufficient_seats' % event.id)
attendees_sudo = self._create_attendees_from_registration_post(event, registrations_data)
return request.redirect(('/event/%s/registration/success?' % event.id) + werkzeug.urls.url_encode({'registration_ids': ",".join([str(id) for id in attendees_sudo.ids])}))
@http.route(['/event/<model("event.event"):event>/registration/success'], type='http', auth="public", methods=['GET'], website=True, sitemap=False)
def event_registration_success(self, event, registration_ids):
# fetch the related registrations, make sure they belong to the correct visitor / event pair
visitor = request.env['website.visitor']._get_visitor_from_request()
if not visitor:
raise NotFound()
attendees_sudo = request.env['event.registration'].sudo().search([
('id', 'in', [str(registration_id) for registration_id in registration_ids.split(',')]),
('event_id', '=', event.id),
('visitor_id', '=', visitor.id),
])
return request.render("website_event.registration_complete",
self._get_registration_confirm_values(event, attendees_sudo))
def _get_registration_confirm_values(self, event, attendees_sudo):
urls = event._get_event_resource_urls()
return {
'attendees': attendees_sudo,
'event': event,
'google_url': urls.get('google_url'),
'iCal_url': urls.get('iCal_url')
}
# ------------------------------------------------------------
# TOOLS (HELPERS)
# ------------------------------------------------------------
def get_formated_date(self, event):
start_date = fields.Datetime.from_string(event.date_begin).date()
end_date = fields.Datetime.from_string(event.date_end).date()
month = babel.dates.get_month_names('abbreviated', locale=get_lang(event.env).code)[start_date.month]
return ('%s %s%s') % (month, start_date.strftime("%e"), (end_date != start_date and ("-" + end_date.strftime("%e")) or ""))
def _extract_searched_event_tags(self, searches):
tags = request.env['event.tag']
if searches.get('tags'):
try:
tag_ids = literal_eval(searches['tags'])
except:
pass
else:
# perform a search to filter on existing / valid tags implicitely + apply rules on color
tags = request.env['event.tag'].search([('id', 'in', tag_ids)])
return tags

35
data/event_data.xml Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="menu_events" model="website.menu">
<field name="name">Events</field>
<field name="url">/event</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">30</field>
</record>
<record id="action_open_website" model="ir.actions.act_url">
<field name="name">Website Home</field>
<field name="target">self</field>
<field name="url">/event</field>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_open_website"/>
<field name="state">open</field>
</record>
<record id="mt_event_published" model="mail.message.subtype">
<field name="name">Event published</field>
<field name="res_model">event.event</field>
<field name="default" eval="False"/>
<field name="description">Event published</field>
</record>
<record id="mt_event_unpublished" model="mail.message.subtype">
<field name="name">Event unpublished</field>
<field name="res_model">event.event</field>
<field name="default" eval="False"/>
<field name="description">Event unpublished</field>
</record>
</data>
</odoo>

230
data/event_demo.xml Normal file
View File

@ -0,0 +1,230 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Event Type -->
<record id="event.event_type_0" model="event.type">
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_type_1" model="event.type">
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_type_2" model="event.type">
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<!-- Event Event -->
<record id="event.event_0" model="event.event">
<field name="website_published" eval="True"/>
<field name="subtitle">Get Inspired • Stay Connected • Have Fun</field>
<field name="cover_properties">{"background-image": "url('/website_event/static/src/img/event_cover_0.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
<field name="description" type="html">
<div class="oe_structure">
<h5>Join us for this 3-day Event</h5>
<p class="lead mb-3">Every year we invite our community, partners and end-users to come and meet us! It's the ideal event to get together and present new features, roadmap of future versions, achievements of the software, workshops, training sessions, etc....</p>
<p class="mb-3">This event is also an opportunity to showcase our partners' case studies, methodology or developments. Be there and see directly from the source the features of the version 12!
</p>
<div class="text-bg-light border-start border-2 border-secondary p-3">
<p class="mb-0"><i class="fa fa-info-circle me-2"/>This event and all the conferences are in <b>English</b>!</p>
</div>
<h5 class="mb-2">What's new?</h5>
<ul class="mb-5">
<li class="mb-2"><b>The Design Fair is preceded by 2 days of Training Sessions for experts!</b><br/> We propose 3 different training sessions, 2 days each.</li>
<li class="mb-2"><b>The whole event is open to all public!</b> <br/>We ask a participation fee of 49.50€ for the costs for the 3 days (coffee breaks, catering, drinks and a surprising concert and beer party).<br/> For those who don't want to contribute, there is a free ticket, therefore, catering and access to evening events aren't included.</li>
<li class="mb-2"><b>The plenary sessions in the morning will be shorter</b> and we will give more time for thematical meetings, conferences, workshops and tutorial sessions in the afternoon.</li>
</ul>
<h5 class="mb-2">Program</h5>
<p>Conferences, workshops and trainings will be organized in 6 rooms:</p>
<ul class="mb-5">
<li><b>Technical Rooms</b> - One dedicated to advanced Odoo developers, one for new developers.</li>
<li><b>Technical Rooms</b> - One dedicated to advanced Odoo developers, one for new developers.</li>
<li><b>Business Room</b> - To discuss implementation methodologies, best sales practices, etc.</li>
<li><b>Workshop Room</b> - Mainly for developers.</li>
</ul>
<div class="rounded-end border-start border-secondary p-3 mb-3" style="border-start-width: 3px !important;">
<p class="mb-0"><em>If you wish to make a presentation, please send your topic proposal as soon as possible for approval to Mr. Famke Jenssens at ngh (a) yourcompany (dot) com. The presentations should be, for example, a presentation of a community module, a case study, methodology feedback, technical, etc. Each presentation must be in English.</em></p>
</div>
<p class="mb-3">For any additional information, please contact us at <a href="mailto:events@yourcompany.com">events@yourcompany.com</a>.</p>
<div class="text-bg-light border-start border-2 border-secondary p-3">
<p class="mb-0">OpenElec Applications reserves the right to cancel, re-name or re-locate the event or change the dates on which it is held.</p>
</div>
</div>
</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_1" model="event.event">
<field name="website_menu" eval="True"/>
<field name="website_published" eval="True"/>
<field name="menu_register_cta" eval="True"/>
<field name="subtitle">The Great Reno Balloon Race is the world's largest free hot-air ballooning event.</field>
<field name="cover_properties">{"background-image": "url('/website_event/static/src/img/event_cover_1.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
<field name="description" type="html">
<div class="oe_structure">
<h5>Join us for the greatest ballon race of all times!</h5>
<p class="lead mb-3">The best aeronauts of the world will gather on this event to offer you the most spectacular show.</p>
<p class="lead mb-3">Around one hundred ballons will simultaneously take flight and turn the sky into a beautiful canvas of colours.</p>
<p class="lead mb-3">This is the perfect place for spending a nice day with your family, we guarantee you will be leaving with beautiful everlasting memories!</p>
<p class="mb-3">For any additional information, please contact us at <a href="mailto:events@yourcompany.com">events@yourcompany.com</a>.</p>
<div class="text-bg-light border-start border-2 border-secondary p-3">
<p class="mb-1">We reserve the right to cancel, re-name or re-locate the event or change the dates on which it is held in case the weather fails us.</p>
<p class="mb0">The safety of our attendees and our aeronauts comes first!</p>
</div>
</div>
</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_2" model="event.event">
<field name="website_published" eval="True"/>
<field name="subtitle">Enhance your architectural business and improve professional skills.</field>
<field name="cover_properties">{"background-image": "url('/website_event/static/src/img/event_cover_2.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
<field name="description" type="html">
<div class="oe_structure">
<h5>Conference for Architects</h5>
<p class="lead">During this conference, our team will give a detailed overview of our business applications. Youll know all the benefits of using it.</p>
<h6>Objectives</h6>
<p>Having attended this conference, participants should be able to:</p>
<ul class="mb-4">
<li>Understand the various modules;</li>
<li>Functional flow of the main applications;</li>
</ul>
<h6>Program</h6>
<ul class="mb-4">
<li>Introduction, CRM, Sales Management</li>
<li>Purchase, Sales &amp; Purchase management, Financial accounting.</li>
<li>Project management, Human resources, Contract management.</li>
<li>Warehouse management, Manufacturing (MRP) &amp; Sales, Import/Export.</li>
<li>Point of Sale (POS), Introduction to report customization.</li>
</ul>
<p>For any additional information, please contact us at <a href="mailto:events@odoo.com">events@odoo.com</a></p>
<section class="s_we_speaker p-3 mb-4" itemscope="itemscope" itemtype="http://schema.org/Person" itemprop="performer">
<span class="badge text-bg-secondary o_wevent_badge float-end">SPEAKER</span>
<img src="/mail/static/src/img/odoobot.png" width="70" class="img-fluid rounded-circle float-start me-3" alt=""/>
<div class="overflow-hidden">
<h4 class="mt-3 mb-1" itemprop="name">John DOE</h4>
<h6 class="mb-4">Company</h6>
<p>At just 13 years old, John DOE was already starting to develop his first business applications for customers. After mastering civil engineering, he founded TinyERP. This was the first phase of OpenERP which would later became Odoo, the most installed open-source business software worldwide.</p>
</div>
</section>
<div class="alert alert-info">
<p class="mb-0">Chamber Works reserves the right to cancel, re-name or re-locate the event or change the dates on which it is held.</p>
</div>
</div>
</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_3" model="event.event">
<field name="website_published" eval="True"/>
<field name="subtitle">Experience live music, local food and beverages.</field>
<field name="cover_properties">{"background-image": "url('/website_event/static/src/img/event_cover_3.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
<field name="description" type="html">
<div class="oe_structure">
<h5>Here it is, the 12th edition of our Live Musical Festival!</h5>
<p class="lead mb-3">Once again we assembled the most legendary bands in Rock history.</p>
<p class="lead mb-3">Bands like Bar Fighters, Led Slippers and Link Floyd will offer you the show of the century during our three day event.</p>
<p class="lead mb-3">This is the perfect place for spending a nice time with your friends while listening to some of the most iconic rock songs of all times!</p>
<p class="mb-3">For any additional information, please contact us at <a href="mailto:events@yourcompany.com">events@yourcompany.com</a>.</p>
<div class="text-bg-light border-start border-2 border-secondary p-3">
<p class="mb-1">We reserve the right to cancel, re-name or re-locate the event or change the dates on which it is held in case the weather fails us.</p>
</div>
</div>
</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_4" model="event.event">
<field name="website_published" eval="True"/>
<field name="subtitle">Discover how to grow a sustainable business with our experts.</field>
<field name="cover_properties">{"background-image": "url('/website_event/static/src/img/event_cover_4.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_5" model="event.event">
<field name="website_published" eval="True"/>
<field name="subtitle">Bring your outdoor field hockey season to the next level by taking the field at this 9th annual Field Hockey tournament.</field>
<field name="cover_properties">{"background-image": "url('/website_event/static/src/img/event_cover_5.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
<field name="description" type="html">
<div class="oe_structure">
<h5>Seasoned Hockey Fans and curious people, this tournament is for you!</h5>
<p class="lead mb-3">The best Hockey teams of the country will compete for the national Hockey trophy.</p>
<p class="lead mb-3">If you don't know anything about Hockey, this is a great introduction to this wonderful sport as you will will be able to see some training process and also have some time
to chat with experienced players and trainers once the tournament is over!
</p>
<p class="mb-3">For any additional information, please contact us at <a href="mailto:events@yourcompany.com">events@yourcompany.com</a>.</p>
<div class="text-bg-light border-start border-2 border-secondary p-3">
<p class="mb-1">We reserve the right to cancel, re-name or re-locate the event or change the dates on which it is held in case the weather fails us.</p>
</div>
</div>
</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_6" model="event.event">
<field name="website_published" eval="False"/>
<field name="cover_properties">{"background-image": "none", "background-color": "secondary", "opacity": ""}</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_7" model="event.event">
<field name="website_menu" eval="True"/>
<field name="website_published" eval="True"/>
<field name="menu_register_cta" eval="True"/>
<field name="subtitle">Our newest collection will be revealed online! Interact with us on our live streams!</field>
<field name="cover_properties">{"background-image": "url('/website_event/static/src/img/event_cover_7.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
<field name="description" type="html">
<div class="oe_structure">
<h5>The finest OpenWood furnitures are coming to your house in a brand new collection</h5>
<p>
And this time, we go fully ONLINE! Meet us in our live streams from the comfort of your house.<br/>
Special discount codes will be handed out during the various streams, make sure to be there on time.
</p>
<p class="mb-3">For any additional information, please contact us at <a href="mailto:events@openwood.example.com">events@penwood.example.com</a>.</p>
<div class="text-bg-light border-start border-2 border-secondary p-3">
<p class="mb-1">This event is fully online and FREE, if you have paid for tickets, you should get a refund.<br/>
It will require a good Internet connection to get the best video quality.</p>
</div>
</div>
</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
</odoo>

View File

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<!-- EVENT TYPE SPECIFIC -->
<record id="event_type_data_sports_question_0" model="event.question">
<field name="title">How did you learn about this event?</field>
<field name="once_per_order" eval="False"/>
<field name="event_type_id" ref="event.event_type_2"/>
</record>
<record id="event_type_data_sports_question_0_answer_0" model="event.question.answer">
<field name="name">Social Media</field>
<field name="sequence">1</field>
<field name="question_id" ref="website_event.event_type_data_sports_question_0"/>
</record>
<record id="event_type_data_sports_question_0_answer_1" model="event.question.answer">
<field name="name">Blog Post</field>
<field name="sequence">2</field>
<field name="question_id" ref="website_event.event_type_data_sports_question_0"/>
</record>
<record id="event_type_data_sports_question_0_answer_2" model="event.question.answer">
<field name="name">Radio Ad</field>
<field name="sequence">3</field>
<field name="question_id" ref="website_event.event_type_data_sports_question_0"/>
</record>
<!-- EVENT SPECIFIC -->
<record id="event_0_question_0" model="event.question">
<field name="title">Meal Type</field>
<field name="question_type">simple_choice</field>
<field name="once_per_order" eval="False"/>
<field name="event_id" ref="event.event_0"/>
</record>
<record id="event_0_question_0_answer_0" model="event.question.answer">
<field name="name">Mixed</field>
<field name="sequence">1</field>
<field name="question_id" ref="website_event.event_0_question_0"/>
</record>
<record id="event_0_question_0_answer_1" model="event.question.answer">
<field name="name">Vegetarian</field>
<field name="sequence">2</field>
<field name="question_id" ref="website_event.event_0_question_0"/>
</record>
<record id="event_0_question_0_answer_2" model="event.question.answer">
<field name="name">Pastafarian</field>
<field name="sequence">3</field>
<field name="question_id" ref="website_event.event_0_question_0"/>
</record>
<record id="event_0_question_1" model="event.question">
<field name="title">Allergies</field>
<field name="question_type">text_box</field>
<field name="once_per_order" eval="False"/>
<field name="event_id" ref="event.event_0"/>
</record>
<record id="event_0_question_2" model="event.question">
<field name="title">How did you learn about this event?</field>
<field name="question_type">simple_choice</field>
<field name="once_per_order" eval="True"/>
<field name="event_id" ref="event.event_0"/>
</record>
<record id="event_0_question_2_answer_0" model="event.question.answer">
<field name="name">Our website</field>
<field name="sequence">1</field>
<field name="question_id" ref="website_event.event_0_question_2"/>
</record>
<record id="event_0_question_2_answer_1" model="event.question.answer">
<field name="name">Commercials</field>
<field name="sequence">2</field>
<field name="question_id" ref="website_event.event_0_question_2"/>
</record>
<record id="event_0_question_2_answer_2" model="event.question.answer">
<field name="name">A friend</field>
<field name="sequence">3</field>
<field name="question_id" ref="website_event.event_0_question_2"/>
</record>
<!-- Questions of: "Great Reno Ballon Race" -->
<record id="event_1_question_0" model="event.question">
<field name="title">How did you learn about this event?</field>
<field name="question_type">simple_choice</field>
<field name="once_per_order" eval="False"/>
<field name="event_id" ref="event.event_1"/>
</record>
<record id="event_1_question_0_answer_0" model="event.question.answer">
<field name="name">Social Media</field>
<field name="sequence">1</field>
<field name="question_id" ref="website_event.event_1_question_0"/>
</record>
<record id="event_1_question_0_answer_1" model="event.question.answer">
<field name="name">Blog Post</field>
<field name="sequence">2</field>
<field name="question_id" ref="website_event.event_1_question_0"/>
</record>
<record id="event_1_question_0_answer_2" model="event.question.answer">
<field name="name">Radio Ad</field>
<field name="sequence">3</field>
<field name="question_id" ref="website_event.event_1_question_0"/>
</record>
<!-- Questions of: "Hockey Tournament" -->
<record id="event_5_question_0" model="event.question">
<field name="title">How did you learn about this event?</field>
<field name="question_type">simple_choice</field>
<field name="once_per_order" eval="False"/>
<field name="event_id" ref="event.event_5"/>
</record>
<record id="event_5_question_0_answer_0" model="event.question.answer">
<field name="name">Social Media</field>
<field name="sequence">1</field>
<field name="question_id" ref="website_event.event_5_question_0"/>
</record>
<record id="event_5_question_0_answer_1" model="event.question.answer">
<field name="name">Blog Post</field>
<field name="sequence">2</field>
<field name="question_id" ref="website_event.event_5_question_0"/>
</record>
<record id="event_5_question_0_answer_2" model="event.question.answer">
<field name="name">Radio Ad</field>
<field name="sequence">3</field>
<field name="question_id" ref="website_event.event_5_question_0"/>
</record>
<record id="event_5_question_1" model="event.question">
<field name="title">What's your Hockey level?</field>
<field name="question_type">text_box</field>
<field name="once_per_order" eval="False"/>
<field name="event_id" ref="event.event_5"/>
</record>
<!-- Questions of: "OpenWood: Furniture Collection Online Reveal" -->
<record id="event_7_question_0" model="event.question">
<field name="title">Which field are you working in</field>
<field name="question_type">simple_choice</field>
<field name="once_per_order" eval="False"/>
<field name="event_id" ref="event.event_7"/>
</record>
<record id="event_7_question_0_answer_0" model="event.question.answer">
<field name="name">Consumers</field>
<field name="sequence">1</field>
<field name="question_id" ref="website_event.event_7_question_0"/>
</record>
<record id="event_7_question_0_answer_1" model="event.question.answer">
<field name="name">Sales</field>
<field name="sequence">2</field>
<field name="question_id" ref="website_event.event_7_question_0"/>
</record>
<record id="event_7_question_0_answer_2" model="event.question.answer">
<field name="name">Research</field>
<field name="sequence">3</field>
<field name="question_id" ref="website_event.event_7_question_0"/>
</record>
<record id="event_7_question_1" model="event.question">
<field name="title">How did you hear about us?</field>
<field name="question_type">text_box</field>
<field name="once_per_order" eval="True"/>
<field name="event_id" ref="event.event_7"/>
</record>
</data></odoo>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="event_registration_0_0_registration_answer_0" model="event.registration.answer">
<field name="value_answer_id" ref="website_event.event_0_question_0_answer_0" />
<field name="question_id" ref="website_event.event_0_question_0" />
<field name="registration_id" ref="event.event_registration_0_0" />
</record>
<record id="event_registration_0_0_registration_answer_1" model="event.registration.answer">
<field name="value_text_box">Fish Nuts</field>
<field name="question_id" ref="website_event.event_0_question_1" />
<field name="registration_id" ref="event.event_registration_0_0" />
</record>
<record id="event_registration_0_0_registration_answer_2" model="event.registration.answer">
<field name="value_answer_id" ref="website_event.event_0_question_2_answer_0" />
<field name="question_id" ref="website_event.event_0_question_2" />
<field name="registration_id" ref="event.event_registration_0_0" />
</record>
<record id="event_registration_0_1_registration_answer_0" model="event.registration.answer">
<field name="value_answer_id" ref="website_event.event_0_question_0_answer_1" />
<field name="question_id" ref="website_event.event_0_question_0" />
<field name="registration_id" ref="event.event_registration_0_1" />
</record>
<record id="event_registration_0_1_registration_answer_1" model="event.registration.answer">
<field name="value_answer_id" ref="website_event.event_0_question_2_answer_0" />
<field name="question_id" ref="website_event.event_0_question_2" />
<field name="registration_id" ref="event.event_registration_0_1" />
</record>
<record id="event_registration_0_2_registration_answer_0" model="event.registration.answer">
<field name="value_answer_id" ref="website_event.event_0_question_0_answer_2" />
<field name="question_id" ref="website_event.event_0_question_0" />
<field name="registration_id" ref="event.event_registration_0_2" />
</record>
<record id="event_registration_0_2_registration_answer_1" model="event.registration.answer">
<field name="value_answer_id" ref="website_event.event_0_question_2_answer_2" />
<field name="question_id" ref="website_event.event_0_question_2" />
<field name="registration_id" ref="event.event_registration_0_2" />
</record>
</data></odoo>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<!-- OpenWood Collection Online Reveal: Gemini (all) -->
<record id="event.event_registration_7_0" model="event.registration">
<field name="visitor_id" ref="website.website_visitor_0"/>
</record>
<record id="event.event_registration_7_1" model="event.registration">
<field name="visitor_id" ref="website.website_visitor_0"/>
</record>
<record id="event.event_registration_7_2" model="event.registration">
<field name="visitor_id" ref="website.website_visitor_1"/>
</record>
<record id="event.event_registration_7_3" model="event.registration">
<field name="visitor_id" ref="website.website_visitor_1"/>
</record>
</data></odoo>

311
data/res_partner_demo.xml Normal file
View File

@ -0,0 +1,311 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="base.res_partner_1" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">Wood Corner brings honesty and seriousness to wood industry while helping customers deal with trees, flowers and fungi.</field>
<field name="website_description" type="html">
<div><section class="s_text_image o_colored_level" data-snippet="s_image_text" data-name="Image - Text">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-3 pt8 pb8 o_colored_level">
<img src="/web/image/website.s_image_text_default_image" class="img img-fluid mx-auto" alt=""/>
</div>
<div class="col-lg-9 pt8 pb8 o_colored_level">
<h2 class="o_default_snippet_text">Happy to be Sponsor</h2>
<p class="o_default_snippet_text">As a team, we are happy to contribute to this event.</p>
<p class="o_default_snippet_text">Come see us live, we hope to meet you!</p>
<p><a href="#" class="btn btn-primary o_default_snippet_text">Discover more</a></p>
</div>
</div>
</div>
</section></div></field>
</record>
<record id="base.res_partner_2" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">Deco Addict brings honesty and seriousness to wood industry while helping customers deal with trees, flowers and fungi.</field>
<field name="website_description" type="html">
<div><section class="s_text_image o_colored_level" data-snippet="s_image_text" data-name="Image - Text">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-3 pt8 pb8 o_colored_level">
<img src="/web/image/website.s_image_text_default_image" class="img img-fluid mx-auto" alt=""/>
</div>
<div class="col-lg-9 pt8 pb8 o_colored_level">
<h2 class="o_default_snippet_text">Happy to be Sponsor</h2>
<p class="o_default_snippet_text">As a team, we are happy to contribute to this event.</p>
<p class="o_default_snippet_text">Come see us live, we hope to meet you!</p>
<p><a href="#" class="btn btn-primary o_default_snippet_text">Discover more</a></p>
</div>
</div>
</div>
</section></div></field>
</record>
<record id="base.res_partner_3" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">Gemini Furniture brings honesty and seriousness to wood industry while helping customers deal with trees, flowers and fungi.</field>
<field name="website_description" type="html">
<div><section class="s_text_image o_colored_level" data-snippet="s_image_text" data-name="Image - Text">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-3 pt8 pb8 o_colored_level">
<img src="/web/image/website.s_image_text_default_image" class="img img-fluid mx-auto" alt=""/>
</div>
<div class="col-lg-9 pt8 pb8 o_colored_level">
<h2 class="o_default_snippet_text">Happy to be Sponsor</h2>
<p class="o_default_snippet_text">As a team, we are happy to contribute to this event.</p>
<p class="o_default_snippet_text">Come see us live, we hope to meet you!</p>
<p><a href="#" class="btn btn-primary o_default_snippet_text">Discover more</a></p>
</div>
</div>
</div>
</section></div></field>
</record>
<record id="base.res_partner_4" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">Ready Mat brings honesty and seriousness to wood industry while helping customers deal with trees, flowers and fungi.</field>
<field name="website_description" type="html">
<div><section class="s_text_image o_colored_level" data-snippet="s_image_text" data-name="Image - Text">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-3 pt8 pb8 o_colored_level">
<img src="/web/image/website.s_image_text_default_image" class="img img-fluid mx-auto" alt=""/>
</div>
<div class="col-lg-9 pt8 pb8 o_colored_level">
<h2 class="o_default_snippet_text">Happy to be Sponsor</h2>
<p class="o_default_snippet_text">As a team, we are happy to contribute to this event.</p>
<p class="o_default_snippet_text">Come see us live, we hope to meet you!</p>
<p><a href="#" class="btn btn-primary o_default_snippet_text">Discover more</a></p>
</div>
</div>
</div>
</section></div></field>
</record>
<record id="base.res_partner_10" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">The Jackson Group brings honesty and seriousness to wood industry while helping customers deal with trees, flowers and fungi.</field>
<field name="website_description" type="html">
<div><section class="s_text_image o_colored_level" data-snippet="s_image_text" data-name="Image - Text">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-3 pt8 pb8 o_colored_level">
<img src="/web/image/website.s_image_text_default_image" class="img img-fluid mx-auto" alt=""/>
</div>
<div class="col-lg-9 pt8 pb8 o_colored_level">
<h2 class="o_default_snippet_text">Happy to be Sponsor</h2>
<p class="o_default_snippet_text">As a team, we are happy to contribute to this event.</p>
<p class="o_default_snippet_text">Come see us live, we hope to meet you!</p>
<p><a href="#" class="btn btn-primary o_default_snippet_text">Discover more</a></p>
</div>
</div>
</div>
</section></div></field>
</record>
<record id="base.res_partner_12" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">Azure Interior brings honesty and seriousness to wood industry while helping customers deal with trees, flowers and fungi.</field>
<field name="website_description" type="html">
<div><section class="s_text_image o_colored_level" data-snippet="s_image_text" data-name="Image - Text">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-3 pt8 pb8 o_colored_level">
<img src="/web/image/website.s_image_text_default_image" class="img img-fluid mx-auto" alt=""/>
</div>
<div class="col-lg-9 pt8 pb8 o_colored_level">
<h2 class="o_default_snippet_text">Happy to be Sponsor</h2>
<p class="o_default_snippet_text">As a team, we are happy to contribute to this event.</p>
<p class="o_default_snippet_text">Come see us live, we hope to meet you!</p>
<p><a href="#" class="btn btn-primary o_default_snippet_text">Discover more</a></p>
</div>
</div>
</div>
</section></div></field>
</record>
<record id="event.res_partner_event_1" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">Bloem brings honesty and seriousness to wood industry while helping customers deal with trees, flowers and fungi.</field>
<field name="website_description" type="html">
<div><section class="s_text_image o_colored_level" data-snippet="s_image_text" data-name="Image - Text">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-3 pt8 pb8 o_colored_level">
<img src="/web/image/website.s_image_text_default_image" class="img img-fluid mx-auto" alt=""/>
</div>
<div class="col-lg-9 pt8 pb8 o_colored_level">
<h2 class="o_default_snippet_text">Happy to be Sponsor</h2>
<p class="o_default_snippet_text">As a team, we are happy to contribute to this event.</p>
<p class="o_default_snippet_text">Come see us live, we hope to meet you!</p>
<p><a href="#" class="btn btn-primary o_default_snippet_text">Discover more</a></p>
</div>
</div>
</div>
</section></div></field>
</record>
<record id="event.res_partner_event_2" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">OpenWood brings honesty and seriousness to wood industry while helping customers deal with trees, flowers and fungi.</field>
<field name="website_description" type="html">
<div><section class="s_text_image o_colored_level" data-snippet="s_image_text" data-name="Image - Text">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-3 pt8 pb8 o_colored_level">
<img src="/web/image/website.s_image_text_default_image" class="img img-fluid mx-auto" alt=""/>
</div>
<div class="col-lg-9 pt8 pb8 o_colored_level">
<h2 class="o_default_snippet_text">Happy to be Sponsor</h2>
<p class="o_default_snippet_text">As a team, we are happy to contribute to this event.</p>
<p class="o_default_snippet_text">Come see us live, we hope to meet you!</p>
<p><a href="#" class="btn btn-primary o_default_snippet_text">Discover more</a></p>
</div>
</div>
</div>
</section></div></field>
</record>
<record id="event.res_partner_event_3" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">Tree Dealers brings honesty and seriousness to wood industry while helping customers deal with trees, flowers and fungi.</field>
<field name="website_description" type="html">
<div><section class="s_text_image o_colored_level" data-snippet="s_image_text" data-name="Image - Text">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-3 pt8 pb8 o_colored_level">
<img src="/web/image/website.s_image_text_default_image" class="img img-fluid mx-auto" alt=""/>
</div>
<div class="col-lg-9 pt8 pb8 o_colored_level">
<h2 class="o_default_snippet_text">Happy to be Sponsor</h2>
<p class="o_default_snippet_text">As a team, we are happy to contribute to this event.</p>
<p class="o_default_snippet_text">Come see us live, we hope to meet you!</p>
<p><a href="#" class="btn btn-primary o_default_snippet_text">Discover more</a></p>
</div>
</div>
</div>
</section></div></field>
</record>
<record id="event.res_partner_event_4" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">Shangai Pterocarpus Furniture brings honesty and seriousness to wood industry while helping customers deal with trees, flowers and fungi.</field>
<field name="website_description" type="html">
<div><section class="s_text_image o_colored_level" data-snippet="s_image_text" data-name="Image - Text">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-3 pt8 pb8 o_colored_level">
<img src="/web/image/website.s_image_text_default_image" class="img img-fluid mx-auto" alt=""/>
</div>
<div class="col-lg-9 pt8 pb8 o_colored_level">
<h2 class="o_default_snippet_text">Happy to be Sponsor</h2>
<p class="o_default_snippet_text">As a team, we are happy to contribute to this event.</p>
<p class="o_default_snippet_text">Come see us live, we hope to meet you!</p>
<p><a href="#" class="btn btn-primary o_default_snippet_text">Discover more</a></p>
</div>
</div>
</div>
</section></div></field>
</record>
<!-- 15-16-28: children of 12 (Azure Interior) -->
<record id="base.res_partner_address_15" model="res.partner">
<field name="is_published" eval="True"/>
<field name="website">http://azure.example.com</field>
<field name="website_description" type="html">
<p>
Brandon works in IT sector <b>since 10 years</b>. He is known
notably for selling mouse traps. With that trick he cut
IT budget by almost half within the last 2 years.
</p>
</field>
</record>
<record id="base.res_partner_address_16" model="res.partner">
<field name="is_published" eval="True"/>
<field name="website">http://azure.example.com</field>
<field name="website_description" type="html">
<p>
Nicole works in IT sector <b>since 20 years</b>. She
develops software to help develop websites. She sold her
first company at 30 years old and manage to grow Azure Interior
from 1 to 55 employees mostly by reselling services on
Odoo.
</p><p>
Nicole is <b>author of several books</b>, including Amazon best seller
"How Azure and Odoo will change the business world!".
</p>
</field>
</record>
<record id="base.res_partner_address_28" model="res.partner">
<field name="is_published" eval="True"/>
<field name="website">http://azure.example.com</field>
<field name="website_description" type="html">
<p>
Colleen Diaz works in IT sector <b>since 10 years</b>. He is known
notably for selling mouse traps. With that trick he cut
IT budget by almost half within the last 2 years.
</p>
</field>
</record>
<!-- 17-18: children of 10 (Jackson Group) -->
<record id="base.res_partner_address_17" model="res.partner">
<field name="is_published" eval="True"/>
<field name="website">http://jackson.group.example.com</field>
<field name="website_description" type="html">
<p>
Toni Rhodes works in IT sector <b>since 10 years</b>. He is known
notably for selling mouse traps. With that trick he cut
IT budget by almost half within the last 2 years.
Famous Managing Partner.
</p>
</field>
</record>
<record id="base.res_partner_address_18" model="res.partner">
<field name="is_published" eval="True"/>
<field name="website">http://jackson.group.example.com</field>
<field name="website_description" type="html">
<p>
Gordon Owens works in IT sector <b>since 10 years</b>. He is known
notably for selling mouse traps. With that trick he cut
IT budget by almost half within the last 2 years.
Famous Senior Consultant.
</p>
</field>
</record>
<!-- 3-4-31: children of 2 (Deco Addict) -->
<record id="base.res_partner_address_3" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">Douglas Fletcher is a mighty functional consultant at Deco Addict</field>
<field name="website_description" type="html">
<p>
Douglas Fletcher works in IT sector <b>since 10 years</b>. He is known
notably for selling mouse traps. With that trick he cut
IT budget by almost half within the last 2 years.
</p></field>
</record>
<record id="base.res_partner_address_4" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">Floyd Steward is a mighty analyst at Deco Addict.</field>
<field name="website_description" type="html">
<p>
Floyd Steward works in IT sector <b>since 10 years</b>. He is known
notably for selling mouse traps. With that trick he cut
IT budget by almost half within the last 2 years.
</p></field>
</record>
<record id="base.res_partner_address_31" model="res.partner">
<field name="is_published">True</field>
<field name="website_short_description">Addison Olson is a mighty sales representative at Deco Addict.</field>
<field name="website_description" type="html">
<p>
Addison Olson works in IT sector <b>since 10 years</b>. He is known
notably for selling mouse traps. With that trick he cut
IT budget by almost half within the last 2 years.
</p></field>
</record>
</odoo>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo><data>
<record id="ir_filters_event_list_snippet" model="ir.filters">
<field name="name">Upcoming Events</field>
<field name="model_id">event.event</field>
<field name="user_id" eval="False" />
<field name="domain">[('date_begin', '&gt;=', context_today())]</field>
<field name="sort">['date_begin asc']</field>
</record>
<record id="website_snippet_filter_event_list" model="website.snippet.filter">
<field name="filter_id" ref="website_event.ir_filters_event_list_snippet"/>
<field name="field_names">name,subtitle</field>
<field name="limit" eval="16"/>
<field name="name">Upcoming Events</field>
</record>
</data></odoo>

1544
i18n/af.po Normal file

File diff suppressed because it is too large Load Diff

1685
i18n/ar.po Normal file

File diff suppressed because it is too large Load Diff

1545
i18n/az.po Normal file

File diff suppressed because it is too large Load Diff

1650
i18n/bg.po Normal file

File diff suppressed because it is too large Load Diff

1545
i18n/bs.po Normal file

File diff suppressed because it is too large Load Diff

1709
i18n/ca.po Normal file

File diff suppressed because it is too large Load Diff

1681
i18n/cs.po Normal file

File diff suppressed because it is too large Load Diff

1682
i18n/da.po Normal file

File diff suppressed because it is too large Load Diff

1719
i18n/de.po Normal file

File diff suppressed because it is too large Load Diff

1546
i18n/el.po Normal file

File diff suppressed because it is too large Load Diff

1543
i18n/en_GB.po Normal file

File diff suppressed because it is too large Load Diff

1705
i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

1708
i18n/es_419.po Normal file

File diff suppressed because it is too large Load Diff

1544
i18n/es_CO.po Normal file

File diff suppressed because it is too large Load Diff

1542
i18n/es_CR.po Normal file

File diff suppressed because it is too large Load Diff

1542
i18n/es_DO.po Normal file

File diff suppressed because it is too large Load Diff

1545
i18n/es_EC.po Normal file

File diff suppressed because it is too large Load Diff

1542
i18n/es_PA.po Normal file

File diff suppressed because it is too large Load Diff

1543
i18n/es_PE.po Normal file

File diff suppressed because it is too large Load Diff

1542
i18n/es_PY.po Normal file

File diff suppressed because it is too large Load Diff

1542
i18n/es_VE.po Normal file

File diff suppressed because it is too large Load Diff

1704
i18n/et.po Normal file

File diff suppressed because it is too large Load Diff

1649
i18n/fa.po Normal file

File diff suppressed because it is too large Load Diff

1699
i18n/fi.po Normal file

File diff suppressed because it is too large Load Diff

1708
i18n/fr.po Normal file

File diff suppressed because it is too large Load Diff

1542
i18n/gl.po Normal file

File diff suppressed because it is too large Load Diff

1544
i18n/gu.po Normal file

File diff suppressed because it is too large Load Diff

1676
i18n/he.po Normal file

File diff suppressed because it is too large Load Diff

1552
i18n/hr.po Normal file

File diff suppressed because it is too large Load Diff

1657
i18n/hu.po Normal file

File diff suppressed because it is too large Load Diff

1705
i18n/id.po Normal file

File diff suppressed because it is too large Load Diff

1540
i18n/is.po Normal file

File diff suppressed because it is too large Load Diff

1703
i18n/it.po Normal file

File diff suppressed because it is too large Load Diff

1677
i18n/ja.po Normal file

File diff suppressed because it is too large Load Diff

1542
i18n/ka.po Normal file

File diff suppressed because it is too large Load Diff

1542
i18n/kab.po Normal file

File diff suppressed because it is too large Load Diff

1544
i18n/km.po Normal file

File diff suppressed because it is too large Load Diff

1679
i18n/ko.po Normal file

File diff suppressed because it is too large Load Diff

1540
i18n/lb.po Normal file

File diff suppressed because it is too large Load Diff

1657
i18n/lt.po Normal file

File diff suppressed because it is too large Load Diff

1643
i18n/lv.po Normal file

File diff suppressed because it is too large Load Diff

1547
i18n/mk.po Normal file

File diff suppressed because it is too large Load Diff

1549
i18n/mn.po Normal file

File diff suppressed because it is too large Load Diff

1550
i18n/nb.po Normal file

File diff suppressed because it is too large Load Diff

1706
i18n/nl.po Normal file

File diff suppressed because it is too large Load Diff

1687
i18n/pl.po Normal file

File diff suppressed because it is too large Load Diff

1691
i18n/pt.po Normal file

File diff suppressed because it is too large Load Diff

1702
i18n/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

1550
i18n/ro.po Normal file

File diff suppressed because it is too large Load Diff

1709
i18n/ru.po Normal file

File diff suppressed because it is too large Load Diff

1665
i18n/sk.po Normal file

File diff suppressed because it is too large Load Diff

1657
i18n/sl.po Normal file

File diff suppressed because it is too large Load Diff

1693
i18n/sr.po Normal file

File diff suppressed because it is too large Load Diff

1545
i18n/sr@latin.po Normal file

File diff suppressed because it is too large Load Diff

1690
i18n/sv.po Normal file

File diff suppressed because it is too large Load Diff

1688
i18n/th.po Normal file

File diff suppressed because it is too large Load Diff

1703
i18n/tr.po Normal file

File diff suppressed because it is too large Load Diff

1688
i18n/uk.po Normal file

File diff suppressed because it is too large Load Diff

1675
i18n/vi.po Normal file

File diff suppressed because it is too large Load Diff

1640
i18n/website_event.pot Normal file

File diff suppressed because it is too large Load Diff

1676
i18n/zh_CN.po Normal file

File diff suppressed because it is too large Load Diff

1673
i18n/zh_TW.po Normal file

File diff suppressed because it is too large Load Diff

16
models/__init__.py Normal file
View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import event_event
from . import event_question
from . import event_question_answer
from . import event_registration
from . import event_registration_answer
from . import event_tag_category
from . import event_tag
from . import event_type
from . import website
from . import website_event_menu
from . import website_menu
from . import website_snippet_filter
from . import website_visitor

618
models/event_event.py Normal file
View File

@ -0,0 +1,618 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from dateutil.relativedelta import relativedelta
import json
import werkzeug.urls
from pytz import utc, timezone
from odoo import api, fields, models, _
from odoo.addons.http_routing.models.ir_http import slug
from odoo.exceptions import ValidationError
from odoo.osv import expression
from odoo.tools.misc import get_lang, format_date
GOOGLE_CALENDAR_URL = 'https://www.google.com/calendar/render?'
class Event(models.Model):
_name = 'event.event'
_inherit = [
'event.event',
'website.seo.metadata',
'website.published.multi.mixin',
'website.cover_properties.mixin',
'website.searchable.mixin',
]
def _default_cover_properties(self):
res = super()._default_cover_properties()
res.update({
'background-image': "url('/website_event/static/src/img/event_cover_4.jpg')",
'opacity': '0.4',
'resize_class': 'cover_auto'
})
return res
def _default_question_ids(self):
return self.env['event.type']._default_question_ids()
# description
subtitle = fields.Char('Event Subtitle', translate=True)
# registration
is_participating = fields.Boolean("Is Participating", compute="_compute_is_participating")
# website
website_published = fields.Boolean(tracking=True)
website_menu = fields.Boolean(
string='Website Menu',
compute='_compute_website_menu', precompute=True, readonly=False, store=True,
help="Allows to display and manage event-specific menus on website.")
menu_id = fields.Many2one('website.menu', 'Event Menu', copy=False)
menu_register_cta = fields.Boolean(
'Extra Register Button', compute='_compute_menu_register_cta',
readonly=False, store=True)
# sub-menus management
introduction_menu = fields.Boolean(
"Introduction Menu", compute="_compute_website_menu_data",
readonly=False, store=True)
introduction_menu_ids = fields.One2many(
"website.event.menu", "event_id", string="Introduction Menus",
domain=[("menu_type", "=", "introduction")])
location_menu = fields.Boolean(
"Location Menu", compute="_compute_website_menu_data",
readonly=False, store=True)
location_menu_ids = fields.One2many(
"website.event.menu", "event_id", string="Location Menus",
domain=[("menu_type", "=", "location_menu")])
address_name = fields.Char(related='address_id.name')
register_menu = fields.Boolean(
"Register Menu", compute="_compute_website_menu_data",
readonly=False, store=True)
register_menu_ids = fields.One2many(
"website.event.menu", "event_id", string="Register Menus",
domain=[("menu_type", "=", "register")])
community_menu = fields.Boolean(
"Community Menu", compute="_compute_community_menu",
readonly=False, store=True,
help="Display community tab on website")
community_menu_ids = fields.One2many(
"website.event.menu", "event_id", string="Event Community Menus",
domain=[("menu_type", "=", "community")])
# live information
is_ongoing = fields.Boolean(
'Is Ongoing', compute='_compute_time_data', search='_search_is_ongoing',
help="Whether event has begun")
is_done = fields.Boolean(
'Is Done', compute='_compute_time_data')
start_today = fields.Boolean(
'Start Today', compute='_compute_time_data',
help="Whether event is going to start today if still not ongoing")
start_remaining = fields.Integer(
'Remaining before start', compute='_compute_time_data',
help="Remaining time before event starts (minutes)")
# questions
question_ids = fields.One2many(
'event.question', 'event_id', 'Questions', copy=True,
compute='_compute_question_ids', 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)])
def _compute_is_participating(self):
"""Heuristic
* public, no visitor: not participating as we have no information;
* check only confirmed and attended registrations, a draft registration
does not make the attendee participating;
* public and visitor: check visitor is linked to a registration. As
visitors are merged on the top parent, current visitor check is
sufficient even for successive visits;
* logged, no visitor: check partner is linked to a registration. Do
not check the email as it is not really secure;
* logged as visitor: check partner or visitor are linked to a
registration;
"""
current_visitor = self.env['website.visitor']._get_visitor_from_request(force_create=False)
base_domain = [('event_id', 'in', self.ids), ('state', 'in', ['open', 'done'])]
if self.env.user._is_public() and not current_visitor:
events = self.env['event.event']
elif self.env.user._is_public():
events = self.env['event.registration'].sudo().search(
expression.AND([base_domain, [('visitor_id', '=', current_visitor.id)]])
).event_id
else:
if 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)]
events = self.env['event.registration'].sudo().search(
expression.AND([base_domain, domain])
).event_id
for event in self:
event.is_participating = event in events
@api.depends('event_type_id')
def _compute_website_menu(self):
""" Also ensure a value for website_menu as it is a trigger notably for
track related menus. """
for event in self:
if event.event_type_id and event.event_type_id != event._origin.event_type_id:
event.website_menu = event.event_type_id.website_menu
elif not event.website_menu:
event.website_menu = False
@api.depends("event_type_id", "website_menu", "community_menu")
def _compute_community_menu(self):
""" Set False in base module. Sub modules will add their own logic
(meet or track_quiz). """
for event in self:
event.community_menu = False
@api.depends("website_menu")
def _compute_website_menu_data(self):
""" Synchronize with website_menu at change and let people update them
at will afterwards. """
for event in self:
event.introduction_menu = event.website_menu
event.location_menu = event.website_menu
event.register_menu = event.website_menu
@api.depends("event_type_id", "website_menu")
def _compute_menu_register_cta(self):
""" At type onchange: synchronize. At website_menu update: synchronize. """
for event in self:
if event.event_type_id and event.event_type_id != event._origin.event_type_id:
event.menu_register_cta = event.event_type_id.menu_register_cta
elif event.website_menu and (event.website_menu != event._origin.website_menu or not event.menu_register_cta):
event.menu_register_cta = True
elif not event.website_menu:
event.menu_register_cta = False
@api.depends('date_begin', 'date_end')
def _compute_time_data(self):
""" Compute start and remaining time. Do everything in UTC as we compute only
time deltas here. """
now_utc = utc.localize(fields.Datetime.now().replace(microsecond=0))
for event in self:
date_begin_utc = utc.localize(event.date_begin, is_dst=False)
date_end_utc = utc.localize(event.date_end, is_dst=False)
event.is_ongoing = date_begin_utc <= now_utc <= date_end_utc
event.is_done = now_utc > date_end_utc
event.start_today = date_begin_utc.date() == now_utc.date()
if date_begin_utc >= now_utc:
td = date_begin_utc - now_utc
event.start_remaining = int(td.total_seconds() / 60)
else:
event.start_remaining = 0
@api.depends('name')
def _compute_website_url(self):
super(Event, self)._compute_website_url()
for event in self:
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.depends('event_type_id')
def _compute_question_ids(self):
""" Update event questions from its event type. Depends are set only on
event_type_id itself to emulate an onchange. Changing event type content
itself should not trigger this method.
When synchronizing questions:
* lines with no registered answers are removed;
* type lines are added;
"""
if self._origin.question_ids:
# lines to keep: those with already given answers
questions_tokeep_ids = self.env['event.registration.answer'].search(
[('question_id', 'in', self._origin.question_ids.ids)]
).question_id.ids
else:
questions_tokeep_ids = []
for event in self:
if not event.event_type_id and not event.question_ids:
event.question_ids = self._default_question_ids()
continue
if questions_tokeep_ids:
questions_toremove = event._origin.question_ids.filtered(
lambda question: question.id not in questions_tokeep_ids)
command = [(3, question.id) for question in questions_toremove]
else:
command = [(5, 0)]
event.question_ids = command
# copy questions so changes in the event don't affect the event type
for question in event.event_type_id.question_ids:
event.question_ids += question.copy({'event_type_id': False})
# -------------------------------------------------------------------------
# CONSTRAINT METHODS
# -------------------------------------------------------------------------
@api.constrains('website_id')
def _check_website_id(self):
for event in self:
if event.website_id and event.website_id.company_id != event.company_id:
raise ValidationError(_("The website must be from the same company as the event."))
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
events = super().create(vals_list)
events._update_website_menus()
return events
def write(self, vals):
menus_state_by_field = self._split_menus_state_by_field()
res = super(Event, self).write(vals)
menus_update_by_field = self._get_menus_update_by_field(menus_state_by_field, force_update=vals.keys())
self._update_website_menus(menus_update_by_field=menus_update_by_field)
return res
# ------------------------------------------------------------
# WEBSITE MENU MANAGEMENT
# ------------------------------------------------------------
def toggle_website_menu(self, val):
self.website_menu = val
def _get_menu_update_fields(self):
"""" Return a list of fields triggering a split of menu to activate /
menu to de-activate. Due to saas-13.3 improvement of menu management
this is done using side-methods to ease inheritance.
:return list: list of fields, each of which triggering a menu update
like website_menu, website_track, ... """
return ['community_menu', 'introduction_menu', 'location_menu', 'register_menu']
def _get_menu_type_field_matching(self):
return {
'community': 'community_menu',
'introduction': 'introduction_menu',
'location': 'location_menu',
'register': 'register_menu',
}
def _split_menus_state_by_field(self):
""" For each field linked to a menu, get the set of events having this
menu activated and de-activated. Purpose is to find those whose value
changed and update the underlying menus.
:return dict: key = name of field triggering a website menu update, get {
'activated': subset of self having its menu currently set to True
'deactivated': subset of self having its menu currently set to False
} """
menus_state_by_field = dict()
for fname in self._get_menu_update_fields():
activated = self.filtered(lambda event: event[fname])
menus_state_by_field[fname] = {
'activated': activated,
'deactivated': self - activated,
}
return menus_state_by_field
def _get_menus_update_by_field(self, menus_state_by_field, force_update=None):
""" For each field linked to a menu, get the set of events requiring
this menu to be activated or de-activated based on previous recorded
value.
:param menus_state_by_field: see ``_split_menus_state_by_field``;
:param force_update: list of field to which we force update of menus. This
is used notably when a direct write to a stored editable field messes with
its pre-computed value, notably in a transient mode (aka demo for example);
:return dict: key = name of field triggering a website menu update, get {
'activated': subset of self having its menu toggled to True
'deactivated': subset of self having its menu toggled to False
} """
menus_update_by_field = dict()
for fname in self._get_menu_update_fields():
if fname in force_update:
menus_update_by_field[fname] = self
else:
menus_update_by_field[fname] = self.env['event.event']
menus_update_by_field[fname] |= menus_state_by_field[fname]['activated'].filtered(lambda event: not event[fname])
menus_update_by_field[fname] |= menus_state_by_field[fname]['deactivated'].filtered(lambda event: event[fname])
return menus_update_by_field
def _get_website_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);
* sequence: specific sequence of menu entry to be set on the menu;
* menu_type: type of menu entry (used in inheriting modules to ease
menu management; not used in this module in 13.3 due to technical
limitations);
"""
self.ensure_one()
return [
(_('Introduction'), False, 'website_event.template_intro', 1, 'introduction'),
(_('Location'), False, 'website_event.template_location', 50, 'location'),
(_('Register'), '/event/%s/register' % slug(self), False, 100, 'register'),
(_('Community'), '/event/%s/community' % slug(self), False, 80, 'community'),
]
def _update_website_menus(self, menus_update_by_field=None):
""" Synchronize event configuration and its menu entries for frontend.
:param menus_update_by_field: see ``_get_menus_update_by_field``"""
for event in self:
if event.menu_id and not event.website_menu:
# do not rely on cascade, as it is done in SQL -> not calling override and
# letting some ir.ui.views in DB
(event.menu_id + event.menu_id.child_id).sudo().unlink()
elif event.website_menu and not event.menu_id:
root_menu = self.env['website.menu'].sudo().create({'name': event.name, 'website_id': event.website_id.id})
event.menu_id = root_menu
if event.menu_id and (not menus_update_by_field or event in menus_update_by_field.get('community_menu')):
event._update_website_menu_entry('community_menu', 'community_menu_ids', 'community')
if event.menu_id and (not menus_update_by_field or event in menus_update_by_field.get('introduction_menu')):
event._update_website_menu_entry('introduction_menu', 'introduction_menu_ids', 'introduction')
if event.menu_id and (not menus_update_by_field or event in menus_update_by_field.get('location_menu')):
event._update_website_menu_entry('location_menu', 'location_menu_ids', 'location')
if event.menu_id and (not menus_update_by_field or event in menus_update_by_field.get('register_menu')):
event._update_website_menu_entry('register_menu', 'register_menu_ids', 'register')
def _update_website_menu_entry(self, fname_bool, fname_o2m, fmenu_type):
""" Generic method to create menu entries based on a flag on event. This
method is a bit obscure, but is due to preparation of adding new menus
entries and pages for event in a stable version, leading to some constraints
while developing.
:param fname_bool: field name (e.g. website_track)
:param fname_o2m: o2m linking towards website.event.menu matching the
boolean fields (normally an entry of website.event.menu with type matching
the boolean field name)
:param method_name: method returning menu entries information: url, sequence, ...
"""
self.ensure_one()
new_menu = None
menu_data = [menu_info for menu_info in self._get_website_menu_entries()
if menu_info[4] == fmenu_type]
if self[fname_bool] and not self[fname_o2m]:
# menus not found but boolean True: get menus to create
for name, url, xml_id, menu_sequence, menu_type in menu_data:
new_menu = self._create_menu(menu_sequence, name, url, xml_id, menu_type)
elif not self[fname_bool]:
# will cascade delete to the website.event.menu
self[fname_o2m].mapped('menu_id').sudo().unlink()
return new_menu
def _create_menu(self, sequence, name, url, xml_id, menu_type):
""" Create a new menu for the current event.
If url: create a website menu. Menu leads directly to the URL that
should be a valid route.
If xml_id: create a new page using the qweb template given by its
xml_id. Take its url back thanks to new_page of website, then link
it to a menu. Template is duplicated and linked to a new url, meaning
each menu will have its own copy of the template. This is currently
limited to two menus: introduction and location.
:param menu_type: type of menu. Mainly used for inheritance purpose
allowing more fine-grain tuning of menus.
"""
self.check_access_rights('write')
view_id = False
if not url:
# add_menu=False, ispage=False -> simply create a new ir.ui.view with name
# and template
page_result = self.env['website'].sudo().new_page(
name=f'{name} {self.name}', template=xml_id,
add_menu=False, ispage=False)
url = f"/event/{slug(self)}/page{page_result['url']}" # url contains starting "/"
view_id = page_result['view_id']
website_menu = self.env['website.menu'].sudo().create({
'name': name,
'url': url,
'parent_id': self.menu_id.id,
'sequence': sequence,
'website_id': self.website_id.id,
})
self.env['website.event.menu'].create({
'menu_id': website_menu.id,
'event_id': self.id,
'menu_type': menu_type,
'view_id': view_id,
})
return website_menu
# ------------------------------------------------------------
# TOOLS
# ------------------------------------------------------------
def google_map_link(self, zoom=8):
""" Temporary method for stable """
return self._google_map_link(zoom=zoom)
def _google_map_link(self, zoom=8):
self.ensure_one()
if self.address_id:
return self.sudo().address_id.google_map_link(zoom=zoom)
return None
def _track_subtype(self, init_values):
self.ensure_one()
if init_values.keys() & {'is_published', 'website_published'}:
if self.is_published:
return self.env.ref('website_event.mt_event_published', raise_if_not_found=False)
return self.env.ref('website_event.mt_event_unpublished', raise_if_not_found=False)
return super(Event, self)._track_subtype(init_values)
def _get_event_resource_urls(self):
url_date_start = self.date_begin.astimezone(timezone(self.date_tz)).strftime('%Y%m%dT%H%M%S')
url_date_stop = self.date_end.astimezone(timezone(self.date_tz)).strftime('%Y%m%dT%H%M%S')
params = {
'action': 'TEMPLATE',
'text': self.name,
'dates': f'{url_date_start}/{url_date_stop}',
'ctz': self.date_tz,
'details': self.name,
}
if self.address_id:
params.update(location=self.address_inline)
encoded_params = werkzeug.urls.url_encode(params)
google_url = GOOGLE_CALENDAR_URL + encoded_params
iCal_url = f'/event/{self.id:d}/ics?{encoded_params}'
return {'google_url': google_url, 'iCal_url': iCal_url}
def _default_website_meta(self):
res = super(Event, self)._default_website_meta()
event_cover_properties = json.loads(self.cover_properties)
# background-image might contain single quotes eg `url('/my/url')`
res['default_opengraph']['og:image'] = res['default_twitter']['twitter:image'] = event_cover_properties.get('background-image', 'none')[4:-1].strip("'")
res['default_opengraph']['og:title'] = res['default_twitter']['twitter:title'] = self.name
res['default_opengraph']['og:description'] = res['default_twitter']['twitter:description'] = self.subtitle
res['default_twitter']['twitter:card'] = 'summary'
res['default_meta_description'] = self.subtitle
return res
def get_backend_menu_id(self):
return self.env.ref('event.event_main_menu').id
@api.model
def _search_build_dates(self):
today = fields.Datetime.today()
def sdn(date):
return fields.Datetime.to_string(date.replace(hour=23, minute=59, second=59))
def sd(date):
return fields.Datetime.to_string(date)
def get_month_filter_domain(filter_name, months_delta):
first_day_of_the_month = today.replace(day=1)
filter_string = _('This month') if months_delta == 0 \
else format_date(self.env, value=today + relativedelta(months=months_delta),
date_format='LLLL', lang_code=get_lang(self.env).code).capitalize()
return [filter_name, filter_string, [
("date_end", ">=", sd(first_day_of_the_month + relativedelta(months=months_delta))),
("date_begin", "<", sd(first_day_of_the_month + relativedelta(months=months_delta+1)))],
0]
return [
['upcoming', _('Upcoming Events'), [("date_end", ">", sd(today))], 0],
['today', _('Today'), [
("date_end", ">", sd(today)),
("date_begin", "<", sdn(today))],
0],
get_month_filter_domain('month', 0),
['old', _('Past Events'), [
("date_end", "<", sd(today))],
0],
['all', _('All Events'), [], 0]
]
@api.model
def _search_get_detail(self, website, order, options):
with_description = options['displayDescription']
with_date = options['displayDetail']
date = options.get('date', 'all')
country = options.get('country')
tags = options.get('tags')
event_type = options.get('type', 'all')
domain = [website.website_domain()]
if event_type != 'all':
domain.append([("event_type_id", "=", int(event_type))])
search_tags = self.env['event.tag']
if tags:
try:
tag_ids = literal_eval(tags)
except SyntaxError:
pass
else:
# perform a search to filter on existing / valid tags implicitely + apply rules on color
search_tags = self.env['event.tag'].search([('id', 'in', tag_ids)])
# Example: You filter on age: 10-12 and activity: football.
# Doing it this way allows to only get events who are tagged "age: 10-12" AND "activity: football".
# Add another tag "age: 12-15" to the search and it would fetch the ones who are tagged:
# ("age: 10-12" OR "age: 12-15") AND "activity: football
for tags in search_tags.grouped('category_id').values():
domain.append([('tag_ids', 'in', tags.ids)])
no_country_domain = domain.copy()
if country:
if country == 'online':
domain.append([("country_id", "=", False)])
elif country != 'all':
domain.append(['|', ("country_id", "=", int(country)), ("country_id", "=", False)])
no_date_domain = domain.copy()
dates = self._search_build_dates()
current_date = None
for date_details in dates:
if date == date_details[0]:
domain.append(date_details[2])
no_country_domain.append(date_details[2])
if date_details[0] != 'upcoming':
current_date = date_details[1]
search_fields = ['name']
fetch_fields = ['name', 'website_url', 'address_name']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
'address_name': {'name': 'address_name', 'type': 'text', 'match': True},
}
if with_description:
search_fields.append('subtitle')
fetch_fields.append('subtitle')
mapping['description'] = {'name': 'subtitle', 'type': 'text', 'match': True}
if with_date:
mapping['detail'] = {'name': 'range', 'type': 'html'}
# Bypassing the access rigths of partner to search the address.
def search_in_address(env, search_term):
ret = env['event.event'].sudo()._search([
('address_search', 'ilike', search_term),
])
return [('id', 'in', ret)]
return {
'model': 'event.event',
'base_domain': domain,
'search_fields': search_fields,
'search_extra': search_in_address,
'fetch_fields': fetch_fields,
'mapping': mapping,
'icon': 'fa-ticket',
# for website_event main controller:
'dates': dates,
'current_date': current_date,
'search_tags': search_tags,
'no_date_domain': no_date_domain,
'no_country_domain': no_country_domain,
}
def _search_render_results(self, fetch_fields, mapping, icon, limit):
with_date = 'detail' in mapping
results_data = super()._search_render_results(fetch_fields, mapping, icon, limit)
if with_date:
for event, data in zip(self, results_data):
begin = self.env['ir.qweb.field.date'].record_to_html(event, 'date_begin', {})
end = self.env['ir.qweb.field.date'].record_to_html(event, 'date_end', {})
data['range'] = '%s🠖%s' % (begin, end) if begin != end else begin
return results_data

66
models/event_question.py Normal file
View File

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class EventQuestion(models.Model):
_name = 'event.question'
_rec_name = 'title'
_order = 'sequence,id'
_description = 'Event Question'
title = fields.Char(required=True, translate=True)
question_type = fields.Selection([
('simple_choice', 'Selection'),
('text_box', 'Text Input'),
('name', 'Name'),
('email', 'Email'),
('phone', 'Phone'),
('company_name', 'Company'),
], default='simple_choice', string="Question Type", required=True)
event_type_id = fields.Many2one('event.type', 'Event Type', ondelete='cascade')
event_id = fields.Many2one('event.event', 'Event', ondelete='cascade')
answer_ids = fields.One2many('event.question.answer', 'question_id', "Answers", copy=True)
sequence = fields.Integer(default=10)
once_per_order = fields.Boolean('Ask once per order',
help="If True, this question will be asked only once and its value will be propagated to every attendees."
"If not it will be asked for every attendee of a reservation.")
is_mandatory_answer = fields.Boolean('Mandatory Answer')
@api.constrains('event_type_id', 'event_id')
def _constrains_event(self):
if any(question.event_type_id and question.event_id for question in self):
raise UserError(_("Question cannot be linked to both an Event and an Event Type."))
def write(self, vals):
""" We add a check to prevent changing the question_type of a question that already has answers.
Indeed, it would mess up the event.registration.answer (answer type not matching the question type). """
if 'question_type' in vals:
questions_new_type = self.filtered(lambda question: question.question_type != vals['question_type'])
if questions_new_type:
answer_count = self.env['event.registration.answer'].search_count([('question_id', 'in', questions_new_type.ids)])
if answer_count > 0:
raise UserError(_("You cannot change the question type of a question that already has answers!"))
return super(EventQuestion, self).write(vals)
@api.ondelete(at_uninstall=False)
def _unlink_except_answered_question(self):
if self.env['event.registration.answer'].search_count([('question_id', 'in', self.ids)]):
raise UserError(_('You cannot delete a question that has already been answered by attendees.'))
def action_view_question_answers(self):
""" Allow analyzing the attendees answers to event questions in a convenient way:
- A graph view showing counts of each suggestions for simple_choice questions
(Along with secondary pivot and tree views)
- A tree view showing textual answers values for text_box questions. """
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("website_event.action_event_registration_report")
action['domain'] = [('question_id', '=', self.id)]
if self.question_type == 'simple_choice':
action['views'] = [(False, 'graph'), (False, 'pivot'), (False, 'tree')]
elif self.question_type == 'text_box':
action['views'] = [(False, 'tree')]
return action

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class EventQuestionAnswer(models.Model):
""" Contains suggested answers to a 'simple_choice' event.question. """
_name = 'event.question.answer'
_order = 'sequence,id'
_description = 'Event Question Answer'
name = fields.Char('Answer', required=True, translate=True)
question_id = fields.Many2one('event.question', required=True, ondelete='cascade')
sequence = fields.Integer(default=10)
@api.ondelete(at_uninstall=False)
def _unlink_except_selected_answer(self):
if self.env['event.registration.answer'].search_count([('value_answer_id', 'in', self.ids)]):
raise UserError(_('You cannot delete an answer that has already been selected by attendees.'))

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class EventRegistration(models.Model):
_name = 'event.registration'
_inherit = ['event.registration']
visitor_id = fields.Many2one('website.visitor', string='Visitor', ondelete='set null')
registration_answer_ids = fields.One2many('event.registration.answer', 'registration_id', string='Attendee Answers')
registration_answer_choice_ids = fields.One2many('event.registration.answer', 'registration_id', string='Attendee Selection Answers',
domain=[('question_type', '=', 'simple_choice')])
def _get_website_registration_allowed_fields(self):
return {'name', 'phone', 'email', 'company_name', 'event_id', 'partner_id', 'event_ticket_id'}
def _get_registration_summary(self):
res = super()._get_registration_summary()
res['registration_answers'] = self.registration_answer_ids.filtered('value_answer_id').mapped('display_name')
return res

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class EventRegistrationAnswer(models.Model):
""" Represents the user input answer for a single event.question """
_name = 'event.registration.answer'
_description = 'Event Registration Answer'
_rec_names_search = ['value_answer_id', 'value_text_box']
question_id = fields.Many2one(
'event.question', ondelete='restrict', required=True,
domain="[('event_id', '=', event_id)]")
registration_id = fields.Many2one('event.registration', required=True, ondelete='cascade')
partner_id = fields.Many2one('res.partner', related='registration_id.partner_id')
event_id = fields.Many2one('event.event', related='registration_id.event_id')
question_type = fields.Selection(related='question_id.question_type')
value_answer_id = fields.Many2one('event.question.answer', string="Suggested answer")
value_text_box = fields.Text('Text answer')
_sql_constraints = [
('value_check', "CHECK(value_answer_id IS NOT NULL OR COALESCE(value_text_box, '') <> '')", "There must be a suggested value or a text value.")
]
# for displaying selected answers by attendees in attendees list view
@api.depends('value_answer_id', 'question_type', 'value_text_box')
def _compute_display_name(self):
for reg in self:
reg.display_name = reg.value_answer_id.name if reg.question_type == "simple_choice" else reg.value_text_box

15
models/event_tag.py Normal file
View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class EventTag(models.Model):
_name = 'event.tag'
_inherit = ['event.tag', 'website.published.multi.mixin']
def default_get(self, fields_list):
result = super().default_get(fields_list)
if self.env.context.get('default_website_id'):
result['website_id'] = self.env.context.get('default_website_id')
return result

View File

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class EventTagCategory(models.Model):
_name = 'event.tag.category'
_inherit = ['event.tag.category', 'website.published.multi.mixin']
def _default_is_published(self):
return True

38
models/event_type.py Normal file
View File

@ -0,0 +1,38 @@
# -*- 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):
_name = 'event.type'
_inherit = ['event.type']
def _default_question_ids(self):
return [
(0, 0, {'title': _('Name'), 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': _('Email'), 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': _('Phone'), 'question_type': 'phone'}),
]
website_menu = fields.Boolean('Display a dedicated menu on Website')
community_menu = fields.Boolean(
"Community Menu", compute="_compute_community_menu",
readonly=False, store=True,
help="Display community tab on website")
menu_register_cta = fields.Boolean(
'Extra Register Button', compute='_compute_menu_register_cta',
readonly=False, store=True)
question_ids = fields.One2many(
'event.question', 'event_type_id', default=_default_question_ids,
string='Questions', copy=True)
@api.depends('website_menu')
def _compute_community_menu(self):
for event_type in self:
event_type.community_menu = event_type.website_menu
@api.depends('website_menu')
def _compute_menu_register_cta(self):
for event_type in self:
event_type.menu_register_cta = event_type.website_menu

26
models/website.py Normal file
View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
from odoo.addons.http_routing.models.ir_http import url_for
class Website(models.Model):
_inherit = "website"
def get_suggested_controllers(self):
suggested_controllers = super(Website, self).get_suggested_controllers()
suggested_controllers.append((_('Events'), url_for('/event'), 'website_event'))
return suggested_controllers
def get_cta_data(self, website_purpose, website_type):
cta_data = super(Website, self).get_cta_data(website_purpose, website_type)
if website_purpose == 'sell_more' and website_type == 'event':
cta_btn_text = _('Next Events')
return {'cta_btn_text': cta_btn_text, 'cta_btn_href': '/event'}
return cta_data
def _search_get_details(self, search_type, order, options):
result = super()._search_get_details(search_type, order, options)
if search_type in ['events', 'all']:
result.append(self.env['event.event']._search_get_detail(self, order, options))
return result

View File

@ -0,0 +1,24 @@
# -*- 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"
_rec_name = "menu_id"
menu_id = fields.Many2one('website.menu', string='Menu', ondelete='cascade')
event_id = fields.Many2one('event.event', string='Event', ondelete='cascade')
view_id = fields.Many2one('ir.ui.view', string='View', ondelete='cascade', help='Used when not being an url based menu')
menu_type = fields.Selection(
[('community', 'Community Menu'),
('introduction', 'Introduction'),
('location', 'Location'),
('register', 'Register'),
], string="Menu Type", required=True)
def unlink(self):
self.view_id.sudo().unlink()
return super(EventMenu, self).unlink()

30
models/website_menu.py Normal file
View File

@ -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. """
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())
for menu_type, fname in event_menu.event_id._get_menu_type_field_matching().items():
if event_menu.menu_type == menu_type:
to_update.append(fname)
# manually remove website_event_menus to call their ``unlink`` method. Otherwise
# super unlinks at db level and skip model-specific behavior.
website_event_menus.unlink()
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

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import models, fields, _
class WebsiteSnippetFilter(models.Model):
_inherit = 'website.snippet.filter'
def _get_hardcoded_sample(self, model):
samples = super()._get_hardcoded_sample(model)
if model._name == 'event.event':
data = [{
'cover_properties': '{"background-image": "url(\'/website_event/static/src/img/event_cover_1.jpg\')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}',
'name': _('Great Reno Ballon Race'),
'date_begin': fields.Date.today() + timedelta(days=10),
'date_end': fields.Date.today() + timedelta(days=11),
}, {
'cover_properties': '{"background-image": "url(\'/website_event/static/src/img/event_cover_2.jpg\')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}',
'name': _('Conference For Architects'),
'date_begin': fields.Date.today(),
'date_end': fields.Date.today() + timedelta(days=2),
}, {
'cover_properties': '{"background-image": "url(\'/website_event/static/src/img/event_cover_3.jpg\')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}',
'name': _('Live Music Festival'),
'date_begin': fields.Date.today() + timedelta(weeks=8),
'date_end': fields.Date.today() + timedelta(weeks=8, days=5),
}, {
'cover_properties': '{"background-image": "url(\'/website_event/static/src/img/event_cover_5.jpg\')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}',
'name': _('Hockey Tournament'),
'date_begin': fields.Date.today() + timedelta(days=7),
'date_end': fields.Date.today() + timedelta(days=7),
}, {
'cover_properties': '{"background-image": "url(\'/website_event/static/src/img/event_cover_7.jpg\')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}',
'name': _('OpenWood Collection Online Reveal'),
'date_begin': fields.Date.today() + timedelta(days=1),
'date_end': fields.Date.today() + timedelta(days=3),
}, {
'cover_properties': '{"background-image": "url(\'/website_event/static/src/img/event_cover_4.jpg\')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}',
'name': _('Business Workshops'),
'date_begin': fields.Date.today() + timedelta(days=2),
'date_end': fields.Date.today() + timedelta(days=4),
}]
merged = []
for index in range(0, max(len(samples), len(data))):
merged.append({**samples[index % len(samples)], **data[index % len(data)]})
# merge definitions
samples = merged
return samples

89
models/website_visitor.py Normal file
View File

@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.osv import expression
class WebsiteVisitor(models.Model):
_name = 'website.visitor'
_inherit = ['website.visitor']
event_registration_ids = fields.One2many(
'event.registration', 'visitor_id', string='Event Registrations',
groups="event.group_event_registration_desk")
event_registration_count = fields.Integer(
'# Registrations', compute='_compute_event_registration_count',
groups="event.group_event_registration_desk")
event_registered_ids = fields.Many2many(
'event.event', string="Registered Events",
compute="_compute_event_registered_ids", compute_sudo=True,
search="_search_event_registered_ids",
groups="event.group_event_registration_desk")
@api.depends('partner_id', 'event_registration_ids.name')
def _compute_display_name(self):
""" If there is an event registration for an anonymous visitor, use that
registered attendee name as visitor name. """
super()._compute_display_name()
# sudo is needed for `event_registration_ids`
for visitor in self.sudo().filtered(lambda v: not v.partner_id and v.event_registration_ids):
visitor.display_name = visitor.event_registration_ids[-1].name
@api.depends('event_registration_ids')
def _compute_event_registration_count(self):
read_group_res = self.env['event.registration']._read_group(
[('visitor_id', 'in', self.ids)],
['visitor_id'], ['__count'])
visitor_mapping = {visitor.id: count for visitor, count in read_group_res}
for visitor in self:
visitor.event_registration_count = visitor_mapping.get(visitor.id, 0)
@api.depends('event_registration_ids.email', 'event_registration_ids.phone')
def _compute_email_phone(self):
super(WebsiteVisitor, self)._compute_email_phone()
for visitor in self.filtered(lambda visitor: not visitor.email or not visitor.mobile):
linked_registrations = visitor.event_registration_ids.sorted(lambda reg: (reg.create_date, reg.id), reverse=False)
if not visitor.email:
visitor.email = next((reg.email for reg in linked_registrations if reg.email), False)
if not visitor.mobile:
visitor.mobile = next((reg.phone for reg in linked_registrations if reg.phone), False)
@api.depends('event_registration_ids')
def _compute_event_registered_ids(self):
# include parent's registrations in a visitor o2m field. We don't add
# child one as child should not have registrations (moved to the parent)
for visitor in self:
all_registrations = visitor.event_registration_ids
visitor.event_registered_ids = all_registrations.mapped('event_id')
def _search_event_registered_ids(self, operator, operand):
""" Search visitors with terms on events within their event registrations. E.g. [('event_registered_ids',
'in', [1, 2])] should return visitors having a registration on events 1, 2 as
well as their children for notification purpose. """
if operator == "not in":
raise NotImplementedError("Unsupported 'Not In' operation on visitors registrations")
all_registrations = self.env['event.registration'].sudo().search([
('event_id', operator, operand)
])
if all_registrations:
visitor_ids = all_registrations.with_context(active_test=False).visitor_id.ids
else:
visitor_ids = []
return [('id', 'in', visitor_ids)]
def _inactive_visitors_domain(self):
""" Visitors registered to events are considered always active and should not be deleted. """
domain = super()._inactive_visitors_domain()
return expression.AND([domain, [('event_registration_ids', '=', False)]])
def _merge_visitor(self, target):
""" Override linking process to link registrations to the final visitor. """
self.event_registration_ids.visitor_id = target.id
registration_wo_partner = self.event_registration_ids.filtered(lambda registration: not registration.partner_id)
if registration_wo_partner:
registration_wo_partner.partner_id = target.partner_id
return super()._merge_visitor(target)

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="event_event_public" model="ir.rule">
<field name="name">Event: public/portal: published read</field>
<field name="model_id" ref="event.model_event_event"/>
<field name="domain_force">[('website_published', '=', True)]</field>
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_event_tag_public" model="ir.rule">
<field name="name">Event Tag: public/portal: color = published and category = published</field>
<field name="model_id" ref="event.model_event_tag"/>
<field name="domain_force">[('category_id.website_published', '=', True), ('color', '!=', False), ('color', '!=', 0)]</field>
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_event_event_ticket_public" model="ir.rule">
<field name="name">Event Ticket: public/portal: published read</field>
<field name="model_id" ref="event.model_event_event_ticket"/>
<field name="domain_force">[('event_id.website_published', '=', True)]</field>
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_event_question_published" model="ir.rule">
<field name="name">Event Question: not event groups: event published read</field>
<field name="model_id" ref="website_event.model_event_question"/>
<field name="domain_force">[('event_id.is_published', '=', True)]</field>
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal')), (4, ref('base.group_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_event_question_event_user" model="ir.rule">
<field name="name">Event Question: event user: read all</field>
<field name="model_id" ref="website_event.model_event_question"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('event.group_event_registration_desk'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_event_question_answer_published" model="ir.rule">
<field name="name">Event Question Answer: not event groups: event published read</field>
<field name="model_id" ref="website_event.model_event_question_answer"/>
<field name="domain_force">[('question_id.event_id.is_published', '=', True)]</field>
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal')), (4, ref('base.group_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_event_question_answer_event_user" model="ir.rule">
<field name="name">Event Question Answer: event user: read all</field>
<field name="model_id" ref="website_event.model_event_question_answer"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('event.group_event_registration_desk'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
</data>
<record id="event.group_event_manager" model="res.groups">
<field name="implied_ids" eval="[(4, ref('website.group_website_restricted_editor'))]"/>
</record>
</odoo>

View File

@ -0,0 +1,29 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_event_event_public,event.event,model_event_event,base.group_public,1,0,0,0
access_event_event_portal,event.event,model_event_event,base.group_portal,1,0,0,0
access_event_event_employee,event.event,model_event_event,base.group_user,1,0,0,0
access_event_event_ticket_public,event.event.ticket,event.model_event_event_ticket,base.group_public,1,0,0,0
access_event_event_ticket_portal,event.event.ticket,event.model_event_event_ticket,base.group_portal,1,0,0,0
access_event_event_ticket_employee,event.event.ticket,event.model_event_event_ticket,base.group_user,1,0,0,0
access_event_type,event.type,event.model_event_type,,0,0,0,0
access_event_tag_category_public,event.tag.category,event.model_event_tag_category,base.group_public,1,0,0,0
access_event_tag_category_portal,event.tag.category,event.model_event_tag_category,base.group_portal,1,0,0,0
access_event_tag_category_employee,event.tag.category,event.model_event_tag_category,base.group_user,1,0,0,0
access_event_tag_public,event.tag,event.model_event_tag,base.group_public,1,0,0,0
access_event_tag_portal,event.tag,event.model_event_tag,base.group_portal,1,0,0,0
access_event_tag_employee,event.tag,event.model_event_tag,base.group_user,1,0,0,0
access_website_event_menu_public,website.event.menu,model_website_event_menu,base.group_public,1,0,0,0
access_website_event_menu_portal,website.event.menu,model_website_event_menu,base.group_portal,1,0,0,0
access_website_event_menu_employee,website.event.menu,model_website_event_menu,base.group_user,1,0,0,0
access_website_event_menu_user,website.event.menu.user,model_website_event_menu,event.group_event_user,1,1,1,1
access_website_visitor_user,website.visitor.user,model_website_visitor,event.group_event_user,1,1,0,0
access_event_question_public,event.question,model_event_question,base.group_public,1,0,0,0
access_event_question_portal,event.question,model_event_question,base.group_portal,1,0,0,0
access_event_question_employee,event.question,model_event_question,base.group_user,1,0,0,0
access_event_question_user,event.question.user,model_event_question,event.group_event_user,1,1,1,1
access_event_question_answer_public,event.question.answer,model_event_question_answer,base.group_public,1,0,0,0
access_event_question_answer_portal,event.question.answer,model_event_question_answer,base.group_portal,1,0,0,0
access_event_question_answer_employee,event.question.answer,model_event_question_answer,base.group_user,1,0,0,0
access_event_question_answer_registration,event.question.answer.registration,model_event_question_answer,event.group_event_registration_desk,1,1,0,0
access_event_question_answer_user,event.question.answer.user,model_event_question_answer,event.group_event_user,1,1,1,1
access_event_registration_answer,event.registration.answer,model_event_registration_answer,event.group_event_registration_desk,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_event_event_public event.event model_event_event base.group_public 1 0 0 0
3 access_event_event_portal event.event model_event_event base.group_portal 1 0 0 0
4 access_event_event_employee event.event model_event_event base.group_user 1 0 0 0
5 access_event_event_ticket_public event.event.ticket event.model_event_event_ticket base.group_public 1 0 0 0
6 access_event_event_ticket_portal event.event.ticket event.model_event_event_ticket base.group_portal 1 0 0 0
7 access_event_event_ticket_employee event.event.ticket event.model_event_event_ticket base.group_user 1 0 0 0
8 access_event_type event.type event.model_event_type 0 0 0 0
9 access_event_tag_category_public event.tag.category event.model_event_tag_category base.group_public 1 0 0 0
10 access_event_tag_category_portal event.tag.category event.model_event_tag_category base.group_portal 1 0 0 0
11 access_event_tag_category_employee event.tag.category event.model_event_tag_category base.group_user 1 0 0 0
12 access_event_tag_public event.tag event.model_event_tag base.group_public 1 0 0 0
13 access_event_tag_portal event.tag event.model_event_tag base.group_portal 1 0 0 0
14 access_event_tag_employee event.tag event.model_event_tag base.group_user 1 0 0 0
15 access_website_event_menu_public website.event.menu model_website_event_menu base.group_public 1 0 0 0
16 access_website_event_menu_portal website.event.menu model_website_event_menu base.group_portal 1 0 0 0
17 access_website_event_menu_employee website.event.menu model_website_event_menu base.group_user 1 0 0 0
18 access_website_event_menu_user website.event.menu.user model_website_event_menu event.group_event_user 1 1 1 1
19 access_website_visitor_user website.visitor.user model_website_visitor event.group_event_user 1 1 0 0
20 access_event_question_public event.question model_event_question base.group_public 1 0 0 0
21 access_event_question_portal event.question model_event_question base.group_portal 1 0 0 0
22 access_event_question_employee event.question model_event_question base.group_user 1 0 0 0
23 access_event_question_user event.question.user model_event_question event.group_event_user 1 1 1 1
24 access_event_question_answer_public event.question.answer model_event_question_answer base.group_public 1 0 0 0
25 access_event_question_answer_portal event.question.answer model_event_question_answer base.group_portal 1 0 0 0
26 access_event_question_answer_employee event.question.answer model_event_question_answer base.group_user 1 0 0 0
27 access_event_question_answer_registration event.question.answer.registration model_event_question_answer event.group_event_registration_desk 1 1 0 0
28 access_event_question_answer_user event.question.answer.user model_event_question_answer event.group_event_user 1 1 1 1
29 access_event_registration_answer event.registration.answer model_event_registration_answer event.group_event_registration_desk 1 1 1 1

BIN
static/description/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1 @@
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M13.238 11.483a1.927 1.927 0 0 1 .654-2.55c1.403-.912 2.927-1.233 4.181-.613.2.1.376.24.54.39L49.26 37.346c.472.426.741 1.025.741 1.653H28L13.238 11.483Z" fill="#FC868B"/><path d="M50 39c0 1.657-4.925 3-11 3s-11-1.343-11-3 4.925-3 11-3 11 1.343 11 3Z" fill="#F9464C"/><path d="M36.762 11.483a1.927 1.927 0 0 0-.654-2.55c-1.403-.912-2.927-1.233-4.181-.613-.2.1-.376.24-.54.39L.74 37.346A2.226 2.226 0 0 0 0 39h22l14.762-27.517Z" fill="#1AD3BB"/><path d="M31.693 20.93 25 14.677l-6.693 6.255L25 33.407l6.693-12.476Z" fill="#1A6F66"/><path d="M0 39c0 1.657 4.925 3 11 3s11-1.343 11-3-4.925-3-11-3-11 1.343-11 3Z" fill="#03AF89"/></svg>

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Some files were not shown because too many files have changed in this diff Show More