Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion addons/website_event/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ def events(self, page=1, slug_tags=None, **searches):
'slugify_tags': self._slugify_tags,
'search_count': event_count,
'original_search': fuzzy_search_term and search,
'website': website
'website': website,
'structured_data': events.get_json_ld(),
}

return request.render("website_event.index", values)
Expand Down Expand Up @@ -219,6 +220,7 @@ def event_page(self, event, page, **post):
page = view.key if view else page
values['seo_object'] = request.website.get_template(page)
values['main_object'] = event
values['structured_data'] = event.get_json_ld(is_detail_page=True)
except ValueError:
# page not found
page = 'website.page_404'
Expand Down Expand Up @@ -264,6 +266,7 @@ def _prepare_event_register_values(self, event, **post):
return {
'event': event,
'main_object': event,
'structured_data': event.get_json_ld(is_detail_page=True),
'range': range,
'google_url': lazy(lambda: urls.get('google_url')),
'iCal_url': lazy(lambda: urls.get('iCal_url')),
Expand Down
162 changes: 162 additions & 0 deletions addons/website_event/models/event_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from markupsafe import Markup

from odoo import api, fields, models, tools, _
from odoo.addons.website.helpers.jsonld_builder import JsonLd
from odoo.addons.website.tools import text_from_html
from odoo.exceptions import UserError, ValidationError
from odoo.fields import Domain
from odoo.tools.misc import get_lang, format_date
Expand All @@ -26,6 +28,7 @@ class EventEvent(models.Model):
'website.cover_properties.mixin',
'website.searchable.mixin',
'website.page_visibility_options.mixin',
'website.structured_data.mixin',
]

def _default_cover_properties(self):
Expand Down Expand Up @@ -650,3 +653,162 @@ def _search_render_results(self, fetch_fields, mapping, icon, limit):
data['tag_ids'] = event.tag_ids.read(['name'])
data['image_url'] = event._get_image_url()
return results_data

def _to_structured_data_summary_event(self):
"""Return summary structured data for a single event card.
Returns None if event is not public, ongoing, or lacks a physical address.
:returns: JsonLd event schema or None
:rtype: JsonLd | None
"""
self.ensure_one()
if not self.address_id or self.is_ongoing or self.is_done or self.website_visibility != 'public':
# Google event rich results require a public upcoming event with
# a physical place.
return None
base_url = self.get_base_url()
address = self.address_id.sudo()
street = ', '.join(part for part in (address.street, address.street2) if part)
address_schema_data = {}
if street:
address_schema_data["streetAddress"] = street
if address.city:
address_schema_data["addressLocality"] = address.city
if address.zip:
address_schema_data["postalCode"] = address.zip
if address.state_id.code:
address_schema_data["addressRegion"] = address.state_id.code
if address.country_id.code:
address_schema_data["addressCountry"] = address.country_id.code
schema_data = {
"name": self.name,
"url": self.website_absolute_url,
"startDate": JsonLd.to_iso_datetime(self.date_begin),
}
nested_schema_data = {}
if image_url := self.website_id.image_url(self, 'image_1920'):
nested_schema_data["image"] = JsonLd("ImageObject", {"url": f"{base_url}{image_url}"})
nested_schema_data["location"] = JsonLd("Place", {"name": self.address_name}).add_nested({"address": JsonLd("PostalAddress", address_schema_data)})
return JsonLd("Event", schema_data).add_nested(nested_schema_data)

def _to_structured_data_event(self):
"""Return full structured data for a single event detail page.
Extends summary data with organizer, offers, and event status.
:returns: JsonLd event schema with all details or None if summary invalid
:rtype: JsonLd | None
"""
self.ensure_one()
event_jsonld = self._to_structured_data_summary_event()
if not event_jsonld:
return None
description = self.subtitle or (self.description and text_from_html(self.description, True))
nested_schema_data: dict[str, JsonLd | list[JsonLd] | None] = {}
if self.organizer_id:
organizer_sudo = self.organizer_id.sudo()
organizer_data = {}
if organizer_sudo == self.env.company.partner_id:
organizer_data["@id"] = f"{self.get_base_url()}/#organization"
else:
organizer_data["name"] = organizer_sudo.name
if organizer_sudo.website:
organizer_data["url"] = organizer_sudo.website
nested_schema_data["organizer"] = JsonLd("Organization", organizer_data)
event_status = "EventCancelled" if self.kanban_state == 'cancel' else "EventScheduled"
offers_jsonld = [self._to_structured_data_ticket_offer(ticket) for ticket in self.event_ticket_ids]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
offers_jsonld = [self._to_structured_data_ticket_offer(ticket) for ticket in self.event_ticket_ids]
offers_jsonlds = self._to_structured_data_ticket_offer()

You can move this loop inside the offers.

def _to_structured_data_ticket_offer(self, ticket):
    # builds ONE offer
    ...

def _to_structured_data_ticket_offers(self):
    return [
        self._to_structured_data_ticket_offer(ticket)
        for ticket in self.event_ticket_ids
    ]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/addons/website_event_sale/models/event_event.py

We are using this inside this file, It is lot more easier to extend when it's not returning a list.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is still not returning a list, you can always extend _to_structured_data_ticket_offer.

schema_data = {
"endDate": JsonLd.to_iso_datetime(self.date_end),
"eventStatus": f"https://schema.org/{event_status}",
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
}
if description:
schema_data["description"] = description
nested_schema_data["offers"] = offers_jsonld
return event_jsonld.set(schema_data).add_nested(nested_schema_data)

def _to_structured_data_ticket_offer(self, ticket):
"""Create a JSON-LD Offer for a single event ticket.
Availability is SoldOut if ticket is sold out or past end_sale_datetime.
:param ticket: Event ticket record
:returns: JsonLd offer schema
:rtype: JsonLd
"""
now = fields.Datetime.now()
is_expired = ticket.end_sale_datetime and ticket.end_sale_datetime < now
is_available = not (ticket.is_sold_out or is_expired)
availability = "https://schema.org/InStock" if is_available else "https://schema.org/SoldOut"
offer_jsonld = JsonLd("Offer", {
"name": ticket.name,
"availability": availability,
"validFrom": JsonLd.to_iso_datetime(ticket.start_sale_datetime),
"validThrough": JsonLd.to_iso_datetime(ticket.end_sale_datetime),
})
if is_available:
offer_jsonld.set({"url": f"{self.website_absolute_url}/register"})
return offer_jsonld

def _to_structured_data_collectionpage(self):
"""Return CollectionPage structured data for the events listing page.
:returns: JsonLd collection page schema with valid events
:rtype: JsonLd
"""
website = self.env['website'].get_current_website()
base_url = website.get_base_url()
schema_data = {
"name": self.env._("Events"),
"url": f"{base_url}/event",
}
nested_schema_data = {
"isPartOf": JsonLd("Organization", {"@id": f"{base_url}/#organization"}),
}
event_jsonlds = [
event_schema
for event in self
if (event_schema := event._to_structured_data_summary_event())
]
if event_jsonlds:
main_entity = JsonLd("ItemList").add_nested({
"itemListElement": [
JsonLd("ListItem", {"position": i + 1}).add_nested({"item": event_jsonld})
for i, event_jsonld in enumerate(event_jsonlds)
],
})
nested_schema_data["mainEntity"] = main_entity
return JsonLd("CollectionPage", schema_data).add_nested(nested_schema_data)

def _get_breadcrumb_items(self, is_detail_page=False):
"""Return breadcrumb items for a event page.
:param is_detail_page: If True, includes event detail in breadcrumb
:returns: List of (label, url) tuples for breadcrumb trail
:rtype: list[tuple[str, str]]
"""
website = self.env['website'].get_current_website()
base_url = website.get_base_url()
item = [
(website.name, base_url),
(f"{self.env._('Events')} | {website.name}", f"{base_url}/event"),
]
if is_detail_page:
item.append((f"{self.name} | {website.name}", f"{base_url}{self.website_url}"))
return item

def _build_structured_data(self, is_detail_page=False):
"""Return rendered structured data for event list and detail pages.
Always includes breadcrumb schema; detail pages include event schema.
:param is_detail_page: If True, build detail page schemas; else listing page
:returns: List of JsonLd schemas
:rtype: list[JsonLd]
"""
schemas = super()._build_structured_data(is_detail_page=is_detail_page)
breadcrumb_jsonld = self._build_breadcrumb_schema(
self._get_breadcrumb_items(is_detail_page),
)
if is_detail_page:
self.ensure_one()
if event_schema_jsonld := self._to_structured_data_event():
schemas.append(event_schema_jsonld)
schemas.append(breadcrumb_jsonld)
return schemas
schemas.extend([
self._to_structured_data_collectionpage(),
breadcrumb_jsonld,
])
return schemas
1 change: 1 addition & 0 deletions addons/website_event_sale/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import event_event
from . import product
from . import product_pricelist
from . import sale_order
Expand Down
26 changes: 26 additions & 0 deletions addons/website_event_sale/models/event_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import models


class EventEvent(models.Model):
_inherit = 'event.event'

def _to_structured_data_ticket_offer(self, ticket):
"""Add ticket pricing to the structured data offer.

Uses the tax-included or tax-excluded price depending on the
website ``show_line_subtotals_tax_selection`` setting, matching
the price displayed on the event page.
"""
offer_jsonld = super()._to_structured_data_ticket_offer(ticket)
website = self.env['website'].get_current_website()
if website.show_line_subtotals_tax_selection == 'tax_excluded':
Comment on lines +17 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
website = self.env['website'].get_current_website()
if website.show_line_subtotals_tax_selection == 'tax_excluded':
if self.website_id.show_line_subtotals_tax_selection == 'tax_excluded':

use website_id , wherever available.

price = ticket.total_price_reduce
else:
price = ticket.total_price_reduce_taxinc
offer_jsonld.set({
'price': price,
'priceCurrency': self.company_id.currency_id.name,
})
return offer_jsonld