Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0fd055a
[ADD] tg_website_event_sale_combo_tickets: модуль для клиента
em230418 Oct 16, 2025
1d63d49
fixup! [ADD] tg_website_event_sale_combo_tickets: модуль для клиента
em230418 Oct 17, 2025
bfc5245
fixup! fixup! [ADD] tg_website_event_sale_combo_tickets: модуль для к…
em230418 Oct 17, 2025
8100c2b
fixup! fixup! fixup! [ADD] tg_website_event_sale_combo_tickets: модул…
em230418 Oct 20, 2025
06a30a5
fixup! fixup! fixup! fixup! [ADD] tg_website_event_sale_combo_tickets…
em230418 Oct 20, 2025
6079cc1
fixup! fixup! fixup! fixup! fixup! [ADD] tg_website_event_sale_combo_…
em230418 Oct 27, 2025
7c03868
fixup! fixup! fixup! fixup! fixup! fixup! [ADD] tg_website_event_sale…
em230418 Oct 27, 2025
dc8f4a1
fixup! fixup! fixup! fixup! fixup! fixup! fixup! [ADD] tg_website_eve…
em230418 Oct 30, 2025
264dc09
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! [ADD] tg_webs…
em230418 Oct 30, 2025
438d5aa
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! [ADD] …
em230418 Oct 31, 2025
8d8c3c6
saved
em230418 Dec 11, 2025
600de03
saved
em230418 Dec 11, 2025
d2a589d
saved
em230418 Dec 12, 2025
aa5c4f9
saved
em230418 Dec 12, 2025
7269412
saved
em230418 Dec 15, 2025
11fbd84
fixup! saved
em230418 Dec 18, 2025
f4dd38f
fixup! fixup! saved
em230418 Dec 19, 2025
c980cc4
saved
em230418 Dec 22, 2025
ff3222c
fixup! saved
em230418 Dec 24, 2025
4fc6006
fixup! fixup! saved
em230418 Dec 25, 2025
acb6bc2
Revert "fixup! fixup! saved"
em230418 Dec 26, 2025
710e198
Fixed typo, "accomodation" to "accommodation" (#1)
SecretAgentNull Jan 12, 2026
6a40209
[FIX] Updated "tg_website_event_sale_combo_tickets" docs (#2)
SecretAgentNull Jan 12, 2026
79c4f5f
Добавил Игоря
em230418 Jan 12, 2026
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
108 changes: 108 additions & 0 deletions tg_website_event_sale_combo_tickets/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
===============
Combo tickets
===============

Shuttle tickets - configuration
-------------------------------

- Go to Main menu -> Events -> Choose existing or add a new event

- In the "Tickets" tab add tickets, if none exist

- In the Questions tab:

* add generic questions like name, email

* add question with type "Selection" and "Is shuttle" checked.

For this question add answers and corresponding tickets.

If you already have prepared shuttle events with tickets, you can use "Generate shuttle tickets" button.

After pressing the "Generate shuttle tickets" button, a wizard will open. Press "Add a line", and a new window will open, allowing you to select events to add to the Answers list.

By checking the checkbox in the upper-left corner, you can select all events at once.

- Close question form

- Save

Shuttle tickets - usage
-----------------------

- Go to `/events`

- Click on "Get tickets" button in the event, that was used in the configuration above, then
choose one ticket

- Fill in registration data and select a shuttle

- Pay for the registration for the event, if required

- RESULT: You will have at least 2 registrations.
One is the event for which you have just registered.
Second one is the shuttle event, that is used in the shuttle ticket question

Accommodation - configuration
-----------------------------

- Go to Main menu -> Events -> Choose existing or add a new event

- In the "Tickets" tab add tickets, if none exists

- In the Questions tab:

* add generic questions like name, email

* add question with the type "Selection" and the "Is Accommodation" checked.

For this question add an answer and check the "Is Positive Accommodation Answer" checkbox.
Pressing the "Generate Answers" button will automatically generate a single answer named "Yes" with a "Is Positive Accommdation Answer" checkbox already set.

- Set the price for the ticket

- Save

- Go to Main menu -> Events -> Configuration -> Settings

- In "Shop - Combo Tickets" section set the "Accommodation Category" value

Accommodation - usage
---------------------

- Go to `/events`

- Click on "Get tickets" button in the event, that was used in the configuration above, then
choose one ticket with the question with the type "Selection" and "Is Accommodation" checked.

- Fill in answers to questions, in the question for accommodation, pick answer that was set as the "Is Positive Accommodation Answer".

- Click on "Go to payment"

- Click "Checkout"

- RESULT: in the payment page you will see "Choose accommodation" instead of the payment button.

After the "Choose accommodation" button is pressed, you will be redirected to the shop page with a preselected category configured earlier in the "configuration" step.

Once an accommodation product is added to the shopping cart, the order can be processed.

Credits
=======

Contributors
------------

* `Eugene Molotov <https://github.com/em230418>`__

* `Igor Makarenkov <https://github.com/SecretAgentNull>`__

Sponsors
--------

* `Tribal Gathering <https://www.tribalgathering.com/>`__

Maintainers
-----------

* `IT-Projects LLC <https://it-projects.info>`__
3 changes: 3 additions & 0 deletions tg_website_event_sale_combo_tickets/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import models
from . import controllers
from . import wizard
26 changes: 26 additions & 0 deletions tg_website_event_sale_combo_tickets/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": """Combo tickets""",
"version": "17.0.0.2.0",
"author": "IT-Projects LLC, Eugene Molotov",
"support": "it@it-projects.info",
"website": "https://github.com/it-projects-llc/tg-addons",
"license": "LGPL-3",
"depends": [
"website_event_sale",
"website_event_questions_by_ticket",
],
"data": [
"security/ir.model.access.csv",
"wizard/generate_shuttle_ticket_answers_views.xml",
"views/event_question_views.xml",
"views/templates.xml",
"views/res_config_settings_views.xml",
"views/event_event_views.xml",
],
"demo": [],
"assets": {
"web.assets_frontend": [
"tg_website_event_sale_combo_tickets/static/**/*",
]
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import main
61 changes: 61 additions & 0 deletions tg_website_event_sale_combo_tickets/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from copy import deepcopy

from odoo.http import request

from odoo.addons.website_event_sale.controllers.main import WebsiteEventSaleController
from odoo.addons.website_sale.controllers.main import WebsiteSale


class WebsiteEventSaleComboTicketsController(WebsiteEventSaleController):
def _process_attendees_form(self, event, form_details):
registrations = super()._process_attendees_form(event, form_details)
extra_registrations = []
for reg in registrations:
answers = reg.get("registration_answer_ids") or []
for answer in answers:
answer_id = answer[2].get("value_answer_id")
answer_record = request.env["event.question.answer"].browse(answer_id)
if answer_record.question_id.is_shuttle:
extra_reg = deepcopy(reg)
extra_reg.update(event_ticket_id=answer_record.shuttle_ticket.id)
extra_registrations.append(extra_reg)
return registrations + extra_registrations


class TGWebsiteSale(WebsiteSale):
def _get_shop_payment_values(self, order, **kwargs):
res = super()._get_shop_payment_values(order, **kwargs)

has_positive_accomodation_answers = False
accomodation_line = False
accomodation_category = order.company_id.accomodation_category
if not accomodation_category:
return res

for line in order.order_line:
has_positive_accomodation_answers = (
has_positive_accomodation_answers
or any(
line.registration_ids.registration_answer_choice_ids.filtered(
lambda x: x.question_id.is_accomodation
).mapped("value_answer_id.is_positive_accomodation_answer")
)
)

if (
accomodation_category
in line.product_id.public_categ_ids.parents_and_self
):
accomodation_line = line

if has_positive_accomodation_answers and not accomodation_line:
res.update(
{
"hide_payment_button": True,
"should_include_accomodation": True,
"accomodation_category_url": f"/shop/category/{accomodation_category.id}", # noqa: E501
"errors": [None],
}
)

return res
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def migrate(cr, installed_version):
cr.execute(
"ALTER TABLE event_question RENAME COLUMN is_shuttle_ticket TO is_shuttle"
)
6 changes: 6 additions & 0 deletions tg_website_event_sale_combo_tickets/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from . import event_question
from . import event_question_answer
from . import event_ticket
from . import res_config_settings
from . import res_company
from . import event_event
16 changes: 16 additions & 0 deletions tg_website_event_sale_combo_tickets/models/event_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from odoo import api, models


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

@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
records.question_ids._check_accomodation_answers()
return records

def write(self, vals):
res = super().write(vals)
self.question_ids._check_accomodation_answers()
return res
105 changes: 105 additions & 0 deletions tg_website_event_sale_combo_tickets/models/event_question.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class EventQuestion(models.Model):
_inherit = "event.question"

is_shuttle = fields.Boolean(
compute="_compute_is_shuttle", store=True, readonly=False
)

is_accomodation = fields.Boolean(
compute="_compute_is_accomodation", store=True, readonly=False
)

@api.depends("question_type")
def _compute_is_shuttle(self):
for record in self:
if record.question_type != "simple_choice" or record.is_accomodation:
record.is_shuttle = False

@api.depends("question_type")
def _compute_is_accomodation(self):
for record in self:
if record.question_type != "simple_choice" or record.is_shuttle:
record.is_accomodation = False

@api.constrains("is_shuttle", "is_accomodation")
def _check_shuttle_accomodation(self):
for record in self:
if record.is_shuttle and record.is_accomodation:
raise ValidationError(
_("Question cannot be both for shuttle and accomodation")
)

def _check_accomodation_answers(self, allow_empty=False):
for question in self.filtered("is_accomodation"):
flags = question.answer_ids.mapped("is_positive_accomodation_answer")
if not flags and allow_empty:
continue

has_positive_accomodation_answer = any(flags)
if not has_positive_accomodation_answer:
raise ValidationError(
_(
'Accomodation "%s" question should have positive answer',
question.title,
)
)

def _check_accomodation_category(self):
questions = self.filtered("is_accomodation")

for company in questions.mapped("event_id.company_id"):
if not company.accomodation_category:
raise ValidationError(_("Accomodation category is not set in settings"))

def action_generate_ticket_answers(self):
self._check_shuttle_accomodation()

if self.is_shuttle:
w = self.env["generate.shuttle.ticket.answers"].create(
{
"question": self.id,
}
)

return {
"type": "ir.actions.act_window",
"res_model": w._name,
"res_id": w.id,
"view_mode": "form",
"target": "new",
}

elif self.is_accomodation:
EQA = self.env["event.question.answer"].sudo()

has_positive_accomodation_answer = any(
self.answer_ids.mapped("is_positive_accomodation_answer")
)
if not has_positive_accomodation_answer:
EQA.create(
{
"name": _("Yes"),
"question_id": self.id,
"is_positive_accomodation_answer": True,
}
)

else:
raise NotImplementedError()

@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
records._check_accomodation_category()
records._check_accomodation_answers(allow_empty=True)
return records

def write(self, vals):
res = super().write(vals)
self._check_accomodation_category()
self._check_accomodation_answers(allow_empty=True)
return res
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from odoo import api, fields, models


class EventQuestionAnswer(models.Model):
_inherit = "event.question.answer"

shuttle_ticket = fields.Many2one("event.event.ticket")
shuttle_ticket_domain = fields.Binary(compute="_compute_shuttle_ticket_domain")
is_positive_accomodation_answer = fields.Boolean()

@api.depends("question_id.event_id")
def _compute_shuttle_ticket_domain(self):
for record in self:
record.shuttle_ticket_domain = [
("event_id.stage_id.pipe_end", "=", False),
]

@api.onchange("shuttle_ticket")
def _onchange_shuttle_ticket(self):
if not self.name and self.shuttle_ticket:
self.name = self.shuttle_ticket.name
14 changes: 14 additions & 0 deletions tg_website_event_sale_combo_tickets/models/event_ticket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from odoo import api, models


class EventTicket(models.Model):
_inherit = "event.event.ticket"

@api.depends_context("name_with_event_name")
def _compute_display_name(self):
if not self.env.context.get("name_with_event_name"):
return super()._compute_display_name()

for ticket in self:
event = ticket.event_id
ticket.display_name = f"{ticket.name} ({event.name})"
20 changes: 20 additions & 0 deletions tg_website_event_sale_combo_tickets/models/res_company.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from odoo import fields, models


class Company(models.Model):
_inherit = "res.company"

accomodation_category = fields.Many2one(
"product.public.category",
compute="_compute_accomodation_category",
compute_sudo=True,
)

def _compute_accomodation_category(self):
get_param = self.env["ir.config_parameter"].get_param
accomodation_category_id = int(get_param("tg.accomodation_category", 0))
if not accomodation_category_id:
accomodation_category_id = False

for record in self:
record.accomodation_category = accomodation_category_id
Loading