From fcc83968b6b1d3996b4e3b5e5d355ffc0aac32ec Mon Sep 17 00:00:00 2001
From: Riccardo Fiore
Date: Thu, 25 Jun 2026 23:04:51 +0200
Subject: [PATCH 1/2] [ADD] website_event_custom_registration
Add a per-event "Registration Mode" (native, free or external) on website
events. The registration block, the navbar call to action and the backend
event form adapt to the selected mode: a free event shows a custom message,
an external event links out to a foreign ticket portal, and native events
keep Odoo's standard ticketing. In non-native modes the native seat limit
is read-only and the registration smart buttons are hidden.
---
website_event_custom_registration/README.rst | 145 ++++++
website_event_custom_registration/__init__.py | 1 +
.../__manifest__.py | 20 +
.../website_event_custom_registration.pot | 109 ++++
.../models/__init__.py | 1 +
.../models/event_event.py | 41 ++
.../pyproject.toml | 3 +
.../readme/CONFIGURE.md | 11 +
.../readme/CONTRIBUTORS.md | 1 +
.../readme/DESCRIPTION.md | 20 +
.../readme/USAGE.md | 12 +
.../static/description/index.html | 487 ++++++++++++++++++
.../tests/__init__.py | 1 +
.../tests/test_event_custom_registration.py | 66 +++
.../views/event_event_views.xml | 68 +++
.../views/website_event_templates.xml | 94 ++++
16 files changed, 1080 insertions(+)
create mode 100644 website_event_custom_registration/README.rst
create mode 100644 website_event_custom_registration/__init__.py
create mode 100644 website_event_custom_registration/__manifest__.py
create mode 100644 website_event_custom_registration/i18n/website_event_custom_registration.pot
create mode 100644 website_event_custom_registration/models/__init__.py
create mode 100644 website_event_custom_registration/models/event_event.py
create mode 100644 website_event_custom_registration/pyproject.toml
create mode 100644 website_event_custom_registration/readme/CONFIGURE.md
create mode 100644 website_event_custom_registration/readme/CONTRIBUTORS.md
create mode 100644 website_event_custom_registration/readme/DESCRIPTION.md
create mode 100644 website_event_custom_registration/readme/USAGE.md
create mode 100644 website_event_custom_registration/static/description/index.html
create mode 100644 website_event_custom_registration/tests/__init__.py
create mode 100644 website_event_custom_registration/tests/test_event_custom_registration.py
create mode 100644 website_event_custom_registration/views/event_event_views.xml
create mode 100644 website_event_custom_registration/views/website_event_templates.xml
diff --git a/website_event_custom_registration/README.rst b/website_event_custom_registration/README.rst
new file mode 100644
index 000000000..062328aec
--- /dev/null
+++ b/website_event_custom_registration/README.rst
@@ -0,0 +1,145 @@
+.. image:: https://odoo-community.org/readme-banner-image
+ :target: https://odoo-community.org/get-involved?utm_source=readme
+ :alt: Odoo Community Association
+
+=================================
+Website Event Custom Registration
+=================================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:606fc643f798562a7407fd35e50661c5d3714df63c648b1850bf06c94369aff8
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Beta
+.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fevent-lightgray.png?logo=github
+ :target: https://github.com/OCA/event/tree/19.0/website_event_custom_registration
+ :alt: OCA/event
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/event-19-0/event-19-0-website_event_custom_registration
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/event&target_branch=19.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module lets each website event choose how attendees register,
+beyond Odoo's native ticketing, through a single ``Registration Mode``
+selection:
+
+- **Native ticketing**: Odoo's standard tickets and registration flow
+ (default, unchanged behaviour).
+- **Free / No registration**: no tickets; a custom rich-text message is
+ shown in the registration block instead.
+- **External registration**: a button redirects attendees to a foreign
+ ticket portal in a new tab.
+
+The three behaviours are combined into one Selection field to avoid the
+UI state clashes and XPath conflicts that separate modules would create.
+
+When an event is *free* or *external*, the native "Sold Out" /
+"Registrations Closed" labels and the navbar register link are
+suppressed, so the page never looks broken just because no native ticket
+is configured.
+
+When a non-native mode is selected, the backend event form also locks
+the native seat limit read-only and hides the registration statistics
+and attendees smart buttons, since no native registrations are
+collected.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Configuration
+=============
+
+To configure an event:
+
+1. Go to *Events* and open (or create) an event.
+2. In the **Registration** group, choose a **Registration Mode**:
+
+ - **Native ticketing**: configure tickets as usual in the *Tickets*
+ tab.
+ - **Free / No registration**: fill in the **Free Event Message**
+ shown to visitors.
+ - **External registration**: fill in the **External Ticket URL** and,
+ optionally, the **External Button Text** (defaults to
+ ``Get Tickets``).
+
+3. Save and publish the event.
+
+Usage
+=====
+
+On the public event page, the registration block adapts to the selected
+mode:
+
+- **Native ticketing**: the standard *Register* button and ticket modal.
+- **Free / No registration**: the configured message, with no ticket
+ controls and no "Sold Out" / "Closed" labels.
+- **External registration**: a *Get Tickets* button linking to the
+ external portal, opening in a new tab.
+
+The routing applies everywhere the core registration block is rendered:
+the event page's main call to action, the (desktop and mobile) sidebar,
+and the event sub-menu, so the experience stays consistent across the
+site.
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues `_.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+-------
+
+* Riccardo Fiore
+
+Contributors
+------------
+
+- Riccardo Fiore (Odrakir)
+
+Maintainers
+-----------
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+.. |maintainer-odrakirmusic| image:: https://github.com/odrakirmusic.png?size=40px
+ :target: https://github.com/odrakirmusic
+ :alt: odrakirmusic
+
+Current `maintainer `__:
+
+|maintainer-odrakirmusic|
+
+This module is part of the `OCA/event `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/website_event_custom_registration/__init__.py b/website_event_custom_registration/__init__.py
new file mode 100644
index 000000000..0650744f6
--- /dev/null
+++ b/website_event_custom_registration/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/website_event_custom_registration/__manifest__.py b/website_event_custom_registration/__manifest__.py
new file mode 100644
index 000000000..4ffb0235d
--- /dev/null
+++ b/website_event_custom_registration/__manifest__.py
@@ -0,0 +1,20 @@
+# Copyright 2026 Riccardo Fiore
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+{
+ "name": "Website Event Custom Registration",
+ "summary": "Choose free, native or external ticketing per website event",
+ "version": "19.0.1.0.0",
+ "category": "Marketing/Events",
+ "author": "Riccardo Fiore, Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/event",
+ "license": "AGPL-3",
+ "development_status": "Beta",
+ "maintainers": ["odrakirmusic"],
+ "depends": ["website_event"],
+ "data": [
+ "views/event_event_views.xml",
+ "views/website_event_templates.xml",
+ ],
+ "installable": True,
+ "application": False,
+}
diff --git a/website_event_custom_registration/i18n/website_event_custom_registration.pot b/website_event_custom_registration/i18n/website_event_custom_registration.pot
new file mode 100644
index 000000000..c8b693892
--- /dev/null
+++ b/website_event_custom_registration/i18n/website_event_custom_registration.pot
@@ -0,0 +1,109 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * website_event_custom_registration
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 19.0-20260609\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2026-06-21 15:14+0000\n"
+"PO-Revision-Date: 2026-06-21 15:14+0000\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields,help:website_event_custom_registration.field_event_event__external_ticket_url
+msgid ""
+"Address of the external ticketing portal attendees are sent to when the "
+"registration mode is 'External registration'."
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields,field_description:website_event_custom_registration.field_event_event__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model,name:website_event_custom_registration.model_event_event
+msgid "Event"
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields,field_description:website_event_custom_registration.field_event_event__external_button_text
+msgid "External Button Text"
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields,field_description:website_event_custom_registration.field_event_event__external_ticket_url
+msgid "External Ticket URL"
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields.selection,name:website_event_custom_registration.selection__event_event__registration_mode__external
+msgid "External registration"
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields.selection,name:website_event_custom_registration.selection__event_event__registration_mode__free
+msgid "Free / No registration"
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields,field_description:website_event_custom_registration.field_event_event__free_event_text
+msgid "Free Event Message"
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields,help:website_event_custom_registration.field_event_event__registration_mode
+msgid ""
+"How attendees register for this event on the website:\n"
+"- Native ticketing: use Odoo's built-in tickets and registration.\n"
+"- Free / No registration: show a custom message instead of tickets.\n"
+"- External registration: redirect attendees to a foreign ticket portal."
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields,field_description:website_event_custom_registration.field_event_event__id
+msgid "ID"
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields,help:website_event_custom_registration.field_event_event__external_button_text
+msgid "Label of the button that links to the external ticketing portal."
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields,help:website_event_custom_registration.field_event_event__free_event_text
+msgid ""
+"Message displayed on the event page when the registration mode is 'Free / No"
+" registration'."
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields.selection,name:website_event_custom_registration.selection__event_event__registration_mode__native
+msgid "Native ticketing"
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model_terms:ir.ui.view,arch_db:website_event_custom_registration.view_event_form
+msgid "Registration"
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model:ir.model.fields,field_description:website_event_custom_registration.field_event_event__registration_mode
+msgid "Registration Mode"
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model_terms:ir.ui.view,arch_db:website_event_custom_registration.view_event_form
+msgid "e.g. Free entry — just show up on the night."
+msgstr ""
+
+#. module: website_event_custom_registration
+#: model_terms:ir.ui.view,arch_db:website_event_custom_registration.view_event_form
+msgid "https://tickets.example.com/my-event"
+msgstr ""
diff --git a/website_event_custom_registration/models/__init__.py b/website_event_custom_registration/models/__init__.py
new file mode 100644
index 000000000..b6bc38997
--- /dev/null
+++ b/website_event_custom_registration/models/__init__.py
@@ -0,0 +1 @@
+from . import event_event
diff --git a/website_event_custom_registration/models/event_event.py b/website_event_custom_registration/models/event_event.py
new file mode 100644
index 000000000..278770e5f
--- /dev/null
+++ b/website_event_custom_registration/models/event_event.py
@@ -0,0 +1,41 @@
+# Copyright 2026 Riccardo Fiore
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from odoo import fields, models
+
+
+class EventEvent(models.Model):
+ _inherit = "event.event"
+
+ registration_mode = fields.Selection(
+ selection=[
+ ("native", "Native ticketing"),
+ ("free", "Free / No registration"),
+ ("external", "External registration"),
+ ],
+ default="native",
+ required=True,
+ help=(
+ "How attendees register for this event on the website:\n"
+ "- Native ticketing: use Odoo's built-in tickets and registration.\n"
+ "- Free / No registration: show a custom message instead of tickets.\n"
+ "- External registration: redirect attendees to a foreign ticket "
+ "portal."
+ ),
+ )
+ free_event_text = fields.Html(
+ string="Free Event Message",
+ translate=True,
+ sanitize_attributes=True,
+ help="Message displayed on the event page when the registration mode "
+ "is 'Free / No registration'.",
+ )
+ external_ticket_url = fields.Char(
+ string="External Ticket URL",
+ help="Address of the external ticketing portal attendees are sent to "
+ "when the registration mode is 'External registration'.",
+ )
+ external_button_text = fields.Char(
+ translate=True,
+ default="Get Tickets",
+ help="Label of the button that links to the external ticketing portal.",
+ )
diff --git a/website_event_custom_registration/pyproject.toml b/website_event_custom_registration/pyproject.toml
new file mode 100644
index 000000000..4231d0ccc
--- /dev/null
+++ b/website_event_custom_registration/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/website_event_custom_registration/readme/CONFIGURE.md b/website_event_custom_registration/readme/CONFIGURE.md
new file mode 100644
index 000000000..ae10a9295
--- /dev/null
+++ b/website_event_custom_registration/readme/CONFIGURE.md
@@ -0,0 +1,11 @@
+To configure an event:
+
+1. Go to *Events* and open (or create) an event.
+2. In the **Registration** group, choose a **Registration Mode**:
+ - **Native ticketing**: configure tickets as usual in the *Tickets*
+ tab.
+ - **Free / No registration**: fill in the **Free Event Message** shown
+ to visitors.
+ - **External registration**: fill in the **External Ticket URL** and,
+ optionally, the **External Button Text** (defaults to `Get Tickets`).
+3. Save and publish the event.
diff --git a/website_event_custom_registration/readme/CONTRIBUTORS.md b/website_event_custom_registration/readme/CONTRIBUTORS.md
new file mode 100644
index 000000000..14d07c072
--- /dev/null
+++ b/website_event_custom_registration/readme/CONTRIBUTORS.md
@@ -0,0 +1 @@
+- Riccardo Fiore (Odrakir) \
diff --git a/website_event_custom_registration/readme/DESCRIPTION.md b/website_event_custom_registration/readme/DESCRIPTION.md
new file mode 100644
index 000000000..a4012ec1a
--- /dev/null
+++ b/website_event_custom_registration/readme/DESCRIPTION.md
@@ -0,0 +1,20 @@
+This module lets each website event choose how attendees register, beyond
+Odoo's native ticketing, through a single `Registration Mode` selection:
+
+- **Native ticketing**: Odoo's standard tickets and registration flow
+ (default, unchanged behaviour).
+- **Free / No registration**: no tickets; a custom rich-text message is
+ shown in the registration block instead.
+- **External registration**: a button redirects attendees to a foreign
+ ticket portal in a new tab.
+
+The three behaviours are combined into one Selection field to avoid the UI
+state clashes and XPath conflicts that separate modules would create.
+
+When an event is *free* or *external*, the native "Sold Out" /
+"Registrations Closed" labels and the navbar register link are suppressed,
+so the page never looks broken just because no native ticket is configured.
+
+When a non-native mode is selected, the backend event form also locks the
+native seat limit read-only and hides the registration statistics and
+attendees smart buttons, since no native registrations are collected.
diff --git a/website_event_custom_registration/readme/USAGE.md b/website_event_custom_registration/readme/USAGE.md
new file mode 100644
index 000000000..3c28987e0
--- /dev/null
+++ b/website_event_custom_registration/readme/USAGE.md
@@ -0,0 +1,12 @@
+On the public event page, the registration block adapts to the selected
+mode:
+
+- **Native ticketing**: the standard *Register* button and ticket modal.
+- **Free / No registration**: the configured message, with no ticket
+ controls and no "Sold Out" / "Closed" labels.
+- **External registration**: a *Get Tickets* button linking to the external
+ portal, opening in a new tab.
+
+The routing applies everywhere the core registration block is rendered: the
+event page's main call to action, the (desktop and mobile) sidebar, and the
+event sub-menu, so the experience stays consistent across the site.
diff --git a/website_event_custom_registration/static/description/index.html b/website_event_custom_registration/static/description/index.html
new file mode 100644
index 000000000..a9dbd1359
--- /dev/null
+++ b/website_event_custom_registration/static/description/index.html
@@ -0,0 +1,487 @@
+
+
+
+
+
+README.rst
+
+
+
+
+
+
+
+
+
+
+
Website Event Custom Registration
+
+

+
This module lets each website event choose how attendees register,
+beyond Odoo’s native ticketing, through a single Registration Mode
+selection:
+
+- Native ticketing: Odoo’s standard tickets and registration flow
+(default, unchanged behaviour).
+- Free / No registration: no tickets; a custom rich-text message is
+shown in the registration block instead.
+- External registration: a button redirects attendees to a foreign
+ticket portal in a new tab.
+
+
The three behaviours are combined into one Selection field to avoid the
+UI state clashes and XPath conflicts that separate modules would create.
+
When an event is free or external, the native “Sold Out” /
+“Registrations Closed” labels and the navbar register link are
+suppressed, so the page never looks broken just because no native ticket
+is configured.
+
When a non-native mode is selected, the backend event form also locks
+the native seat limit read-only and hides the registration statistics
+and attendees smart buttons, since no native registrations are
+collected.
+
Table of contents
+
+
+
+
To configure an event:
+
+- Go to Events and open (or create) an event.
+- In the Registration group, choose a Registration Mode:
+- Native ticketing: configure tickets as usual in the Tickets
+tab.
+- Free / No registration: fill in the Free Event Message
+shown to visitors.
+- External registration: fill in the External Ticket URL and,
+optionally, the External Button Text (defaults to
+Get Tickets).
+
+
+- Save and publish the event.
+
+
+
+
+
On the public event page, the registration block adapts to the selected
+mode:
+
+- Native ticketing: the standard Register button and ticket modal.
+- Free / No registration: the configured message, with no ticket
+controls and no “Sold Out” / “Closed” labels.
+- External registration: a Get Tickets button linking to the
+external portal, opening in a new tab.
+
+
The routing applies everywhere the core registration block is rendered:
+the event page’s main call to action, the (desktop and mobile) sidebar,
+and the event sub-menu, so the experience stays consistent across the
+site.
+
+
+
+
Bugs are tracked on GitHub Issues.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
+
+
+
+
+
+
+
+
This module is maintained by the OCA.
+
+
+
+
OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
Current maintainer:
+

+
This module is part of the OCA/event project on GitHub.
+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+
+
+
+
+
+
diff --git a/website_event_custom_registration/tests/__init__.py b/website_event_custom_registration/tests/__init__.py
new file mode 100644
index 000000000..e35e20026
--- /dev/null
+++ b/website_event_custom_registration/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_event_custom_registration
diff --git a/website_event_custom_registration/tests/test_event_custom_registration.py b/website_event_custom_registration/tests/test_event_custom_registration.py
new file mode 100644
index 000000000..1a4f07396
--- /dev/null
+++ b/website_event_custom_registration/tests/test_event_custom_registration.py
@@ -0,0 +1,66 @@
+# Copyright 2026 Riccardo Fiore
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from odoo.tests.common import TransactionCase
+
+
+class TestEventCustomRegistration(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.event = cls.env["event.event"].create(
+ {
+ "name": "Test DJ Night",
+ "date_begin": "2099-01-01 20:00:00",
+ "date_end": "2099-01-02 02:00:00",
+ }
+ )
+
+ def _render_registration(self):
+ return str(
+ self.env["ir.qweb"]._render(
+ "website_event.registration_template",
+ {
+ "event": self.event,
+ "event_page": True,
+ "cta_additional_classes": "",
+ "registration_error_code": False,
+ },
+ )
+ )
+
+ def test_defaults(self):
+ self.assertEqual(self.event.registration_mode, "native")
+ self.assertEqual(self.event.external_button_text, "Get Tickets")
+
+ def test_free_mode_renders_message_and_hides_native_labels(self):
+ self.event.write(
+ {
+ "registration_mode": "free",
+ "free_event_text": "Free entry, just turn up
",
+ }
+ )
+ html = self._render_registration()
+ self.assertIn("Free entry, just turn up", html)
+ self.assertIn("o_wevent_free_registration", html)
+ # No native ticketing chrome leaks through.
+ self.assertNotIn("Sold Out", html)
+ self.assertNotIn("Registrations Closed", html)
+
+ def test_external_mode_renders_outbound_button(self):
+ self.event.write(
+ {
+ "registration_mode": "external",
+ "external_ticket_url": "https://tickets.example.com/dj-night",
+ "external_button_text": "Buy on Resident Advisor",
+ }
+ )
+ html = self._render_registration()
+ self.assertIn("https://tickets.example.com/dj-night", html)
+ self.assertIn("Buy on Resident Advisor", html)
+ self.assertIn('target="_blank"', html)
+ self.assertNotIn("Sold Out", html)
+
+ def test_native_mode_does_not_leak_custom_markup(self):
+ self.event.registration_mode = "native"
+ html = self._render_registration()
+ self.assertNotIn("o_wevent_free_registration", html)
diff --git a/website_event_custom_registration/views/event_event_views.xml b/website_event_custom_registration/views/event_event_views.xml
new file mode 100644
index 000000000..4b55b9be4
--- /dev/null
+++ b/website_event_custom_registration/views/event_event_views.xml
@@ -0,0 +1,68 @@
+
+
+
+
+ event.event.form.custom.registration
+ event.event
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ registration_mode != 'native'
+
+
+ registration_mode != 'native'
+
+
+
+
+ registration_mode != 'native'
+
+
+ registration_mode != 'native'
+
+
+
+
diff --git a/website_event_custom_registration/views/website_event_templates.xml b/website_event_custom_registration/views/website_event_templates.xml
new file mode 100644
index 000000000..8463ca76e
--- /dev/null
+++ b/website_event_custom_registration/views/website_event_templates.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ event_page and event.event_registrations_open and event.registration_mode == 'native'
+
+
+
+
+ event.event_registrations_open and event.registration_mode == 'native'
+
+
+
+
+ not event.event_registrations_open and event.registration_mode == 'native'
+
+
+
+
+
+
+ event.event_registrations_open and not hide_register_cta and event.registration_mode == 'native'
+
+
+
From 1b1889dc0c09a3706841e8f5a5a24d285cc5bdf8 Mon Sep 17 00:00:00 2001
From: Riccardo Fiore
Date: Wed, 1 Jul 2026 22:56:12 +0200
Subject: [PATCH 2/2] [IMP] website_event_custom_registration: option to keep
custom text when closed
Once registrations close (past, sold-out or cancelled event), free and
external events now show Odoo's standard 'Registrations Closed' notice
like native ones, instead of keeping their custom content forever.
A new per-event boolean keep_custom_text_when_closed (backend toggle,
visible for the free/external modes only) restores the previous
behaviour: the free event message or the external ticket button stays
visible after registrations close.
---
website_event_custom_registration/README.rst | 20 ++++++--
.../website_event_custom_registration.pot | 14 ++++++
.../models/event_event.py | 8 ++++
.../readme/CONFIGURE.md | 6 ++-
.../readme/DESCRIPTION.md | 8 +++-
.../readme/USAGE.md | 5 ++
.../static/description/index.html | 17 ++++++-
.../tests/test_event_custom_registration.py | 47 +++++++++++++++++++
.../views/event_event_views.xml | 7 +++
.../views/website_event_templates.xml | 23 ++++++---
10 files changed, 140 insertions(+), 15 deletions(-)
diff --git a/website_event_custom_registration/README.rst b/website_event_custom_registration/README.rst
index 062328aec..64ced09d8 100644
--- a/website_event_custom_registration/README.rst
+++ b/website_event_custom_registration/README.rst
@@ -48,8 +48,12 @@ UI state clashes and XPath conflicts that separate modules would create.
When an event is *free* or *external*, the native "Sold Out" /
"Registrations Closed" labels and the navbar register link are
-suppressed, so the page never looks broken just because no native ticket
-is configured.
+suppressed while registrations are open, so the page never looks broken
+just because no native ticket is configured. Once registrations close
+(the event has ended, is sold out or was cancelled), Odoo's standard
+"Registrations Closed" notice takes over by default; a per-event **Keep
+Custom Text When Closed** toggle keeps the custom message or button
+visible instead.
When a non-native mode is selected, the backend event form also locks
the native seat limit read-only and hides the registration statistics
@@ -77,7 +81,11 @@ To configure an event:
optionally, the **External Button Text** (defaults to
``Get Tickets``).
-3. Save and publish the event.
+3. For *free* and *external* events, optionally enable **Keep Custom
+ Text When Closed** to keep the custom message or button visible after
+ registrations close (past, sold-out or cancelled event), instead of
+ Odoo's standard "Registrations Closed" notice.
+4. Save and publish the event.
Usage
=====
@@ -91,6 +99,12 @@ mode:
- **External registration**: a *Get Tickets* button linking to the
external portal, opening in a new tab.
+Once registrations close (the event has ended, is sold out or was
+cancelled), free and external events show Odoo's standard "Registrations
+Closed" notice like native ones, unless **Keep Custom Text When Closed**
+is enabled on the event — then the custom message or button stays
+visible.
+
The routing applies everywhere the core registration block is rendered:
the event page's main call to action, the (desktop and mobile) sidebar,
and the event sub-menu, so the experience stays consistent across the
diff --git a/website_event_custom_registration/i18n/website_event_custom_registration.pot b/website_event_custom_registration/i18n/website_event_custom_registration.pot
index c8b693892..9105b9a66 100644
--- a/website_event_custom_registration/i18n/website_event_custom_registration.pot
+++ b/website_event_custom_registration/i18n/website_event_custom_registration.pot
@@ -71,6 +71,11 @@ msgstr ""
msgid "ID"
msgstr ""
+#. module: website_event_custom_registration
+#: model:ir.model.fields,field_description:website_event_custom_registration.field_event_event__keep_custom_text_when_closed
+msgid "Keep Custom Text When Closed"
+msgstr ""
+
#. module: website_event_custom_registration
#: model:ir.model.fields,help:website_event_custom_registration.field_event_event__external_button_text
msgid "Label of the button that links to the external ticketing portal."
@@ -88,6 +93,15 @@ msgstr ""
msgid "Native ticketing"
msgstr ""
+#. module: website_event_custom_registration
+#: model:ir.model.fields,help:website_event_custom_registration.field_event_event__keep_custom_text_when_closed
+msgid ""
+"Once registrations close (the event has ended, is sold out or was "
+"cancelled), Odoo normally shows its standard 'Registrations Closed' notice."
+" Enable this to keep showing this event's custom content instead: the free "
+"event message or the external ticket button."
+msgstr ""
+
#. module: website_event_custom_registration
#: model_terms:ir.ui.view,arch_db:website_event_custom_registration.view_event_form
msgid "Registration"
diff --git a/website_event_custom_registration/models/event_event.py b/website_event_custom_registration/models/event_event.py
index 278770e5f..feac36140 100644
--- a/website_event_custom_registration/models/event_event.py
+++ b/website_event_custom_registration/models/event_event.py
@@ -39,3 +39,11 @@ class EventEvent(models.Model):
default="Get Tickets",
help="Label of the button that links to the external ticketing portal.",
)
+ keep_custom_text_when_closed = fields.Boolean(
+ default=False,
+ help="Once registrations close (the event has ended, is sold out or "
+ "was cancelled), Odoo normally shows its standard 'Registrations "
+ "Closed' notice. Enable this to keep showing this event's custom "
+ "content instead: the free event message or the external ticket "
+ "button.",
+ )
diff --git a/website_event_custom_registration/readme/CONFIGURE.md b/website_event_custom_registration/readme/CONFIGURE.md
index ae10a9295..800dd1f92 100644
--- a/website_event_custom_registration/readme/CONFIGURE.md
+++ b/website_event_custom_registration/readme/CONFIGURE.md
@@ -8,4 +8,8 @@ To configure an event:
to visitors.
- **External registration**: fill in the **External Ticket URL** and,
optionally, the **External Button Text** (defaults to `Get Tickets`).
-3. Save and publish the event.
+3. For *free* and *external* events, optionally enable **Keep Custom Text
+ When Closed** to keep the custom message or button visible after
+ registrations close (past, sold-out or cancelled event), instead of
+ Odoo's standard "Registrations Closed" notice.
+4. Save and publish the event.
diff --git a/website_event_custom_registration/readme/DESCRIPTION.md b/website_event_custom_registration/readme/DESCRIPTION.md
index a4012ec1a..95f982868 100644
--- a/website_event_custom_registration/readme/DESCRIPTION.md
+++ b/website_event_custom_registration/readme/DESCRIPTION.md
@@ -12,8 +12,12 @@ The three behaviours are combined into one Selection field to avoid the UI
state clashes and XPath conflicts that separate modules would create.
When an event is *free* or *external*, the native "Sold Out" /
-"Registrations Closed" labels and the navbar register link are suppressed,
-so the page never looks broken just because no native ticket is configured.
+"Registrations Closed" labels and the navbar register link are suppressed
+while registrations are open, so the page never looks broken just because
+no native ticket is configured. Once registrations close (the event has
+ended, is sold out or was cancelled), Odoo's standard "Registrations
+Closed" notice takes over by default; a per-event **Keep Custom Text When
+Closed** toggle keeps the custom message or button visible instead.
When a non-native mode is selected, the backend event form also locks the
native seat limit read-only and hides the registration statistics and
diff --git a/website_event_custom_registration/readme/USAGE.md b/website_event_custom_registration/readme/USAGE.md
index 3c28987e0..b0dc53bb5 100644
--- a/website_event_custom_registration/readme/USAGE.md
+++ b/website_event_custom_registration/readme/USAGE.md
@@ -7,6 +7,11 @@ mode:
- **External registration**: a *Get Tickets* button linking to the external
portal, opening in a new tab.
+Once registrations close (the event has ended, is sold out or was
+cancelled), free and external events show Odoo's standard "Registrations
+Closed" notice like native ones, unless **Keep Custom Text When Closed** is
+enabled on the event — then the custom message or button stays visible.
+
The routing applies everywhere the core registration block is rendered: the
event page's main call to action, the (desktop and mobile) sidebar, and the
event sub-menu, so the experience stays consistent across the site.
diff --git a/website_event_custom_registration/static/description/index.html b/website_event_custom_registration/static/description/index.html
index a9dbd1359..1f796bc3d 100644
--- a/website_event_custom_registration/static/description/index.html
+++ b/website_event_custom_registration/static/description/index.html
@@ -390,8 +390,12 @@ Website Event Custom Registration
UI state clashes and XPath conflicts that separate modules would create.
When an event is free or external, the native “Sold Out” /
“Registrations Closed” labels and the navbar register link are
-suppressed, so the page never looks broken just because no native ticket
-is configured.
+suppressed while registrations are open, so the page never looks broken
+just because no native ticket is configured. Once registrations close
+(the event has ended, is sold out or was cancelled), Odoo’s standard
+“Registrations Closed” notice takes over by default; a per-event Keep
+Custom Text When Closed toggle keeps the custom message or button
+visible instead.
When a non-native mode is selected, the backend event form also locks
the native seat limit read-only and hides the registration statistics
and attendees smart buttons, since no native registrations are
@@ -425,6 +429,10 @@
Get Tickets).
+For free and external events, optionally enable Keep Custom
+Text When Closed to keep the custom message or button visible after
+registrations close (past, sold-out or cancelled event), instead of
+Odoo’s standard “Registrations Closed” notice.
Save and publish the event.
@@ -439,6 +447,11 @@
External registration: a Get Tickets button linking to the
external portal, opening in a new tab.
+Once registrations close (the event has ended, is sold out or was
+cancelled), free and external events show Odoo’s standard “Registrations
+Closed” notice like native ones, unless Keep Custom Text When Closed
+is enabled on the event — then the custom message or button stays
+visible.
The routing applies everywhere the core registration block is rendered:
the event page’s main call to action, the (desktop and mobile) sidebar,
and the event sub-menu, so the experience stays consistent across the
diff --git a/website_event_custom_registration/tests/test_event_custom_registration.py b/website_event_custom_registration/tests/test_event_custom_registration.py
index 1a4f07396..bcb89d8f6 100644
--- a/website_event_custom_registration/tests/test_event_custom_registration.py
+++ b/website_event_custom_registration/tests/test_event_custom_registration.py
@@ -64,3 +64,50 @@ def test_native_mode_does_not_leak_custom_markup(self):
self.event.registration_mode = "native"
html = self._render_registration()
self.assertNotIn("o_wevent_free_registration", html)
+
+ def test_closed_event_defaults_to_core_closed_notice(self):
+ # Once the event is over, core's "Registrations Closed" notice
+ # takes over from the custom content by default.
+ self.event.write(
+ {
+ "date_begin": "2020-01-01 20:00:00",
+ "date_end": "2020-01-02 02:00:00",
+ "registration_mode": "free",
+ "free_event_text": "
Free entry, just turn up
",
+ }
+ )
+ self.assertFalse(self.event.event_registrations_open)
+ html = self._render_registration()
+ self.assertNotIn("Free entry, just turn up", html)
+ self.assertIn("Closed", html)
+
+ def test_closed_event_keeps_custom_text_when_toggled(self):
+ self.event.write(
+ {
+ "date_begin": "2020-01-01 20:00:00",
+ "date_end": "2020-01-02 02:00:00",
+ "registration_mode": "free",
+ "free_event_text": "Free entry, just turn up
",
+ "keep_custom_text_when_closed": True,
+ }
+ )
+ html = self._render_registration()
+ self.assertIn("Free entry, just turn up", html)
+ self.assertNotIn("Closed", html)
+
+ def test_closed_external_event_follows_toggle(self):
+ self.event.write(
+ {
+ "date_begin": "2020-01-01 20:00:00",
+ "date_end": "2020-01-02 02:00:00",
+ "registration_mode": "external",
+ "external_ticket_url": "https://tickets.example.com/dj-night",
+ }
+ )
+ html = self._render_registration()
+ self.assertNotIn("https://tickets.example.com/dj-night", html)
+ self.assertIn("Closed", html)
+ self.event.keep_custom_text_when_closed = True
+ html = self._render_registration()
+ self.assertIn("https://tickets.example.com/dj-night", html)
+ self.assertNotIn("Closed", html)
diff --git a/website_event_custom_registration/views/event_event_views.xml b/website_event_custom_registration/views/event_event_views.xml
index 4b55b9be4..26cdba0d4 100644
--- a/website_event_custom_registration/views/event_event_views.xml
+++ b/website_event_custom_registration/views/event_event_views.xml
@@ -34,6 +34,13 @@
invisible="registration_mode != 'external'"
required="registration_mode == 'external'"
/>
+
+
diff --git a/website_event_custom_registration/views/website_event_templates.xml b/website_event_custom_registration/views/website_event_templates.xml
index 8463ca76e..aeb0fb118 100644
--- a/website_event_custom_registration/views/website_event_templates.xml
+++ b/website_event_custom_registration/views/website_event_templates.xml
@@ -20,11 +20,18 @@
inherit_id="website_event.registration_template"
name="Event Registration: custom modes (free / external)"
>
-
+
+
@@ -32,7 +39,7 @@
+ always in native mode; for free and external events it is
+ suppressed while registrations are open (it would wrongly flag
+ a missing native ticket) and takes over once registrations
+ close — unless the event keeps its custom text via
+ keep_custom_text_when_closed. -->
not event.event_registrations_open and event.registration_mode == 'native'
+ >not event.event_registrations_open and (event.registration_mode == 'native' or not event.keep_custom_text_when_closed)