diff --git a/portal_event_tickets/README.rst b/portal_event_tickets/README.rst
new file mode 100644
index 0000000..a598537
--- /dev/null
+++ b/portal_event_tickets/README.rst
@@ -0,0 +1,154 @@
+=====================
+Customer Event Portal
+=====================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:26a1b1272c217905969b3d3a159e7dff8e4ce0d0fc8c18e9c227338d7fc32c84
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |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/licence-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-it--projects--llc%2Fwebsite--addons-lightgray.png?logo=github
+ :target: https://github.com/it-projects-llc/website-addons/tree/14.0/portal_event_tickets
+ :alt: it-projects-llc/website-addons
+
+|badge1| |badge2| |badge3|
+
+Allows to customers see tickets for events at Portal.
+
+- Only confirmed tickets with attendee_partner_id as current user are
+ shown
+
+Additional features:
+
+- Ticket transferring feature
+
+ - To decrease chance of transferring to a wrong email, partner with
+ the email must exist before transferring.
+
+ - New *When to Run* values for Email Schedule:
+
+ - transferring_started
+ - transferring_finished
+
+ - New attendee receives email with a link to finish ticket
+ transferring
+
+- Tracks changes in key registration fields (via ``tracking=True``)
+
+- Tickets can be changed to other products (including other tickets)
+
+ - When old ticket is canceled, a message with a reference to new
+ Sale Order is posted
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Configuration
+=============
+
+Ticket transferring configuration
+---------------------------------
+
+At event form:
+
+- Activate ``[x] Enable Ticket transferring``
+
+- At ``Email Schedule`` Tab add record:
+
+ - **Email To Send**: *Event: Transferring started*
+ - **Unit**: *Immediately*
+ - **When to Run**: *Transferring started*
+
+Ticket changing configuration
+-----------------------------
+
+At event form:
+
+- Activate ``[x] Enable Ticket changing``
+
+Usage
+=====
+
+- Open link ``/my``
+- RESULT: there is sections Tickets
+
+Ticket transferring
+-------------------
+
+Feature allows for attendees to transfer ticket ownership to another
+partner by email.
+
+- Login to portal as current ticket attendee
+- Select a ticket
+- Click button ``[Transfer to another person]``
+- Specify person's email. The partner must be already registered
+- Click ``[Confrim]``
+
+Now second person receives an email. If you use test deployment without
+mail servers, then you can find email at menu
+``[[ Settings ]] >> Technical >> Email >> Messages``.
+
+- Login to portal as new ticket attendee
+- Open then link at email
+- Fill the form
+- Click ``[Confrim]``
+- RESULT: Ticket has new owner
+
+Ticket changing
+---------------
+
+Feature allows to change the ticket to new ticket or product
+
+- Login to portal
+- Select a ticket
+- Click button ``[Upgrade / Change ticket]``
+- You are redirected to original event page. You can select new ticket
+ or navigate to ``/shop`` page and fill the cart
+- Follow checkout process
+- When the order is confirmed (e.g. after payment), old ticket is
+ canceled and new one is confirmed
+
+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
+-------
+
+* IT-Projects LLC
+
+Contributors
+------------
+
+- Ivan Yelizariev (https://github.com/yelizariev)
+- Alexandr Kolushov (https://github.com/KolushovAlexandr)
+- Eugene Molotov (https://github.com/em230418)
+- Victor Bykov (https://github.com/BykovVik)
+- Ilmir Karamov (https://github.com/ilmir-k)
+
+Maintainers
+-----------
+
+This module is part of the `it-projects-llc/website-addons `_ project on GitHub.
+
+You are welcome to contribute.
diff --git a/portal_event_tickets/__init__.py b/portal_event_tickets/__init__.py
new file mode 100644
index 0000000..91c5580
--- /dev/null
+++ b/portal_event_tickets/__init__.py
@@ -0,0 +1,2 @@
+from . import controllers
+from . import models
diff --git a/portal_event_tickets/__manifest__.py b/portal_event_tickets/__manifest__.py
new file mode 100644
index 0000000..bf2ca1a
--- /dev/null
+++ b/portal_event_tickets/__manifest__.py
@@ -0,0 +1,27 @@
+{
+ "name": """Customer Event Portal""",
+ "summary": """Allows to customers see their tickets for events at the Portal""",
+ "category": "Marketing",
+ "images": ["images/banner.jpg"],
+ "version": "14.0.1.0.0",
+ "author": "IT-Projects LLC",
+ "support": "apps@itpp.dev",
+ "website": "https://github.com/it-projects-llc/website-addons",
+ "license": "AGPL-3",
+ "depends": [
+ "portal",
+ "partner_event",
+ "website_event_sale",
+ "website_event_attendee_fields",
+ "website_sale_refund",
+ ],
+ "data": [
+ "views/portal_templates.xml",
+ "views/event_registration.xml",
+ "views/event_event.xml",
+ "data/mail_template_data.xml",
+ "views/assets.xml",
+ ],
+ "qweb": [],
+ "demo": ["data/res_users_demo.xml"],
+}
diff --git a/portal_event_tickets/controllers/__init__.py b/portal_event_tickets/controllers/__init__.py
new file mode 100644
index 0000000..12a7e52
--- /dev/null
+++ b/portal_event_tickets/controllers/__init__.py
@@ -0,0 +1 @@
+from . import main
diff --git a/portal_event_tickets/controllers/main.py b/portal_event_tickets/controllers/main.py
new file mode 100644
index 0000000..d90ddf8
--- /dev/null
+++ b/portal_event_tickets/controllers/main.py
@@ -0,0 +1,344 @@
+import logging
+
+from werkzeug.exceptions import Forbidden, NotFound
+
+from odoo import SUPERUSER_ID, _, http
+from odoo.exceptions import AccessError
+from odoo.http import request
+
+from odoo.addons.http_routing.models.ir_http import slug
+from odoo.addons.portal.controllers.portal import CustomerPortal
+from odoo.addons.website_event.controllers.main import WebsiteEventController
+from odoo.addons.website_sale.controllers.main import WebsiteSale
+
+_logger = logging.getLogger(__name__)
+
+
+class PortalEvent(CustomerPortal):
+ def _prepare_home_portal_values(self, counters):
+ values = super(PortalEvent, self)._prepare_home_portal_values(counters)
+ if "tickets_count" in counters:
+ domain = self._tickets_domain()
+ values["tickets_count"] = (
+ request.env["event.registration"].sudo().search_count(domain)
+ )
+ return values
+
+ def _tickets_domain(self, partner=None):
+ partner = partner or request.env.user.partner_id
+ return [
+ ("attendee_partner_id", "=", partner.id),
+ ("state", "=", "open"),
+ ]
+
+ @http.route()
+ def account(self, *args, **kw):
+ """Add sales documents to main account page"""
+ response = super(PortalEvent, self).account(*args, **kw)
+ domain = self._tickets_domain()
+ tickets_count = request.env["event.registration"].search_count(domain)
+
+ response.qcontext.update({"tickets_count": tickets_count})
+ return response
+
+ @http.route(
+ ["/my/tickets", "/my/tickets/page/"],
+ type="http",
+ auth="user",
+ website=True,
+ )
+ def portal_my_tickets(self, page=1, date_begin=None, date_end=None, **kw):
+ values = self._prepare_portal_layout_values()
+ Registration = request.env["event.registration"].sudo()
+
+ domain = self._tickets_domain()
+ if date_begin and date_end:
+ domain += [
+ ("create_date", ">", date_begin),
+ ("create_date", "<=", date_end),
+ ]
+
+ # count for pager
+ ticket_count = Registration.search_count(domain)
+ # make pager
+ pager = request.website.pager(
+ url="/my/tickets",
+ url_args={"date_begin": date_begin, "date_end": date_end},
+ total=ticket_count,
+ page=page,
+ step=self._items_per_page,
+ )
+ # search the count to display, according to the pager data
+ tickets = Registration.search(
+ domain, limit=self._items_per_page, offset=pager["offset"]
+ )
+ values.update(
+ {
+ "date": date_begin,
+ "page_name": "tickets",
+ "tickets": tickets,
+ "pager": pager,
+ "default_url": "/my/tickets",
+ }
+ )
+ return request.render("portal_event_tickets.portal_my_tickets", values)
+
+ def _has_ticket_access(self, ticket, to_update=False):
+ """Ticket must not be sudo`ed"""
+ env = ticket.env
+ if not ticket.exists():
+ _logger.info("ticket doesn't exist: %s", ticket)
+ return False
+
+ try:
+ # We check only ir.rule records, because ir.model.access actually
+ # doesn't allow portal user to read
+ ticket.check_access_rule("read")
+ except AccessError:
+ groups = env.user.groups_id.mapped("name")
+ _logger.info(
+ "Ticket access rights check is not passed! User groups: %s",
+ groups,
+ exc_info=True,
+ )
+ return False
+
+ if ticket.sudo().attendee_partner_id.id == ticket.env.user.partner_id.id:
+ return True
+
+ if to_update:
+ _logger.info(
+ "No an attendee %s cannot update ticket %s, which belongs to %s",
+ ticket.env.user.partner_id,
+ ticket,
+ ticket.attendee_partner_id,
+ )
+ # not an attendee, so cannot update
+ return False
+
+ return env.user.has_group("event.group_event_manager")
+
+ @http.route(["/my/tickets/"], type="http", auth="user", website=True)
+ def ticket_page(self, ticket=None, **kw):
+ values = self._prepare_portal_layout_values()
+ ticket = request.env["event.registration"].browse(ticket)
+ if not ticket or not ticket.exists():
+ raise NotFound()
+
+ if not self._has_ticket_access(ticket):
+ raise Forbidden()
+
+ ticket_sudo = ticket.sudo()
+ values.update({"page_name": "tickets", "ticket": ticket_sudo})
+ return request.render("portal_event_tickets.portal_ticket_page", values)
+
+ @http.route(
+ ["/my/tickets/pdf/"], type="http", auth="user", website=True
+ )
+ def portal_get_ticket(self, ticket_id=None, **kw):
+ ticket = request.env["event.registration"].browse(ticket_id)
+ report_template_for_portal = ticket.sudo().event_id.report_template_for_portal
+
+ if not self._has_ticket_access(ticket):
+ raise Forbidden()
+
+ registration_badge_template = (
+ report_template_for_portal.get_metadata()[0].get("xmlid")
+ if report_template_for_portal
+ else "event.report_event_registration_badge"
+ )
+
+ pdf = (
+ request.env.ref(registration_badge_template)
+ .with_user(SUPERUSER_ID)
+ ._render_qweb_pdf([ticket.id])[0]
+ )
+
+ pdfhttpheaders = [
+ ("Content-Type", "application/pdf"),
+ ("Content-Length", len(pdf)),
+ ("Content-Disposition", "attachment; filename=ticket.pdf;"),
+ ]
+ return request.make_response(pdf, headers=pdfhttpheaders)
+
+ @http.route(
+ ["/my/tickets/transfer"],
+ type="http",
+ auth="user",
+ methods=["GET"],
+ website=True,
+ )
+ def ticket_transfer_editor(self, **kw):
+ """Special controller to customize result messages"""
+ if not request.env.user.has_group("website.group_website_designer"):
+ return Forbidden()
+
+ values = self._prepare_portal_layout_values()
+ values.update({"editor_mode": True, "error": kw.get("error")})
+ return request.render("portal_event_tickets.portal_ticket_transfer", values)
+
+ @http.route(
+ ["/my/tickets/transfer"],
+ type="http",
+ auth="user",
+ methods=["POST"],
+ website=True,
+ )
+ def ticket_transfer(self, to_email, ticket_id, **kw):
+ values = self._prepare_portal_layout_values()
+
+ error = self._ticket_transfer(request.env, to_email, ticket_id)
+
+ values.update({"to_email": to_email, "error": error})
+ return request.render("portal_event_tickets.portal_ticket_transfer", values)
+
+ def _ticket_transfer(self, env, to_email, ticket_id):
+ """env is passed explicitly to use this method in ci tests"""
+ ticket = env["event.registration"].browse(int(ticket_id))
+ ticket.ensure_one()
+
+ if not self._has_ticket_access(ticket, to_update=True):
+ raise Forbidden()
+
+ ticket = ticket.sudo()
+
+ if not ticket.event_id.ticket_transferring:
+ raise Forbidden()
+
+ error = None
+
+ # Yes, error is None here
+ # but let's have correct indent for possible adding conditions.
+ if not error:
+ receiver = (
+ env["res.partner"]
+ .sudo()
+ .search([("email", "=ilike", to_email)], limit=1)
+ )
+
+ if not receiver:
+ error = "receiver_not_found"
+
+ if not error:
+ domain = [
+ ("attendee_partner_id", "=", receiver.id),
+ ("state", "not in", ["cancel"]),
+ ("event_id", "=", ticket.event_id.id),
+ ]
+ if env["event.registration"].sudo().search_count(domain):
+ error = "receiver_has_ticket"
+
+ if not error:
+ # do the transfer
+ ticket.transferring_started(receiver)
+
+ return error
+
+ @http.route(
+ ["/my/tickets/transfer/receive"],
+ type="http",
+ auth="user",
+ methods=["GET", "POST"],
+ website=True,
+ )
+ def ticket_transfer_receive(self, transfer_ticket=None, **kw):
+ if transfer_ticket:
+ ticket = request.env["event.registration"].browse(int(transfer_ticket))
+ else:
+ # Just take first available ticket. Mostly for unittests
+ # Use sudo as portal user doesn't have access
+ ticket = (
+ request.env["event.registration"]
+ .sudo()
+ .search(
+ [
+ ("attendee_partner_id", "=", request.env.user.partner_id.id),
+ ("is_transferring", "=", True),
+ ],
+ limit=1,
+ )
+ )
+ # sudo back to original user
+ ticket = ticket.with_user(request.env.user)
+
+ ticket.ensure_one()
+
+ if not self._has_ticket_access(ticket, to_update=True):
+ raise Forbidden()
+
+ # we can make sudo once access is checked
+ ticket = ticket.sudo()
+
+ if not ticket.event_id.ticket_transferring:
+ raise Forbidden()
+
+ values = self._prepare_portal_layout_values()
+ if request.httprequest.method == "GET":
+ tickets = WebsiteEventController()._process_tickets_form(
+ ticket.event_id, {f"nb_register-{ticket.event_ticket_id.id or 0}": 1}
+ )
+ values.update(
+ {
+ "transfer_ticket": ticket,
+ "tickets": tickets,
+ "event": ticket.event_id,
+ "availability_check": True,
+ }
+ )
+ return request.render(
+ "portal_event_tickets.portal_ticket_transfer_receive", values
+ )
+
+ # handle filled form
+
+ receiver = ticket.attendee_partner_id
+ registration = WebsiteEventController()._process_attendees_form(
+ ticket.event_id, kw
+ )[0]
+ registration["event_id"] = ticket.event_id.id
+ partner_vals = request.env["event.registration"]._prepare_partner(registration)
+ assert not partner_vals.get("email")
+
+ receiver.sudo().write(partner_vals)
+
+ ticket.sudo().transferring_finished()
+ return request.redirect("/my/tickets")
+
+ @http.route(
+ ["/my/tickets/change"], type="http", auth="user", methods=["POST"], website=True
+ )
+ def ticket_change(self, ticket_id, **kw):
+ ticket = request.env["event.registration"].browse(int(ticket_id))
+
+ if not self._has_ticket_access(ticket, to_update=True):
+ raise Forbidden()
+
+ if not ticket.event_id.ticket_changing:
+ raise Forbidden()
+
+ ticket = ticket.sudo()
+ line = ticket.sale_order_line_id
+ assert line
+ product = line.product_id
+
+ order = request.website.sale_get_order(force_create=True)
+ name = _("Ticket change: %s") % product.name
+ order.add_refund_line(line, name, 1)
+
+ # TODO: make redirection customizable
+ return request.redirect("/event/%s/register" % slug(ticket.event_id))
+
+
+class WebsiteSaleExtended(WebsiteSale):
+ @http.route()
+ def cart(self, **post):
+ response = super(WebsiteSaleExtended, self).cart(**post)
+ if post.get("total_is_negative"):
+ response.qcontext.update(
+ {
+ "warning_msg": _(
+ "Total amount is negative. Please add more tickets or products"
+ ),
+ }
+ )
+ return response
diff --git a/portal_event_tickets/data/mail_template_data.xml b/portal_event_tickets/data/mail_template_data.xml
new file mode 100644
index 0000000..b3bd935
--- /dev/null
+++ b/portal_event_tickets/data/mail_template_data.xml
@@ -0,0 +1,33 @@
+
+
+
+ Event: Transferring started
+
+ Finish ticket transferring
+ ${object.attendee_partner_id.id}
+
+
+ ${object.partner_id.lang}
+
+ Hello ${object.attendee_partner_id.name},
+
+ The ticket transferring was initiated. Open the link below to complete to process:
+
+
+ You
+
+
+
+ Best regards,
+
+ ]]>
+
+
diff --git a/portal_event_tickets/data/res_users_demo.xml b/portal_event_tickets/data/res_users_demo.xml
new file mode 100644
index 0000000..712ae42
--- /dev/null
+++ b/portal_event_tickets/data/res_users_demo.xml
@@ -0,0 +1,17 @@
+
+
+
+ portal1 name
+ portal1
+ portal1
+ portal1@example.com
+
+
+
+ portal2 name
+ portal2
+ portal2
+ portal2@example.com
+
+
+
diff --git a/portal_event_tickets/i18n/portal_event_tickets.pot b/portal_event_tickets/i18n/portal_event_tickets.pot
new file mode 100644
index 0000000..4b98328
--- /dev/null
+++ b/portal_event_tickets/i18n/portal_event_tickets.pot
@@ -0,0 +1,299 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * portal_event_tickets
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \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: portal_event_tickets
+#: model:mail.template,body_html:portal_event_tickets.email_template_signup
+msgid "\n"
+"\n"
+" \n"
+" Hello ${object.attendee_partner_id.name}, \n"
+"\n"
+" The ticket transferring was initiated. Open the link below to complete to process:\n"
+"\n"
+"
\n"
+" You \n"
+" \n"
+" \n"
+"
\n"
+" Best regards,
\n"
+"\n"
+" "
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Warning! Once confirmed this action cannot be undone"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "× "
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Event: "
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Ticket Type: "
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model,name:portal_event_tickets.model_event_registration
+msgid "Attendee"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,help:portal_event_tickets.field_event_event_ticket_changing
+msgid "Attendee can change ticket to new ticket or products"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,help:portal_event_tickets.field_event_event_ticket_transferring
+msgid "Attendee can transfer ticket to another partner"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Close"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Confirm"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Continue"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Email"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,field_description:portal_event_tickets.field_event_event_ticket_changing
+msgid "Enable Ticket Changing"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,field_description:portal_event_tickets.field_event_event_ticket_transferring
+msgid "Enable Ticket transferring"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model,name:portal_event_tickets.model_event_event
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_my_tickets
+msgid "Event"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:mail.template,subject:portal_event_tickets.email_template_signup
+msgid "Finish ticket transferring"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,field_description:portal_event_tickets.field_website_ticket_transfer_receiver_has_ticket
+msgid "Message ticket transfer: receiver already has ticket"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,field_description:portal_event_tickets.field_website_ticket_transfer_receiver_not_found
+msgid "Message ticket transfer: receiver not found"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,field_description:portal_event_tickets.field_website_ticket_transfer_success
+msgid "Message ticket transfer: success"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "My Tickets"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,field_description:portal_event_tickets.field_event_registration_origin_registration
+msgid "Original Ticket"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_transfer
+msgid "Partner already has ticket"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Print"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Receiver's Email"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model,name:portal_event_tickets.model_event_mail_registration
+msgid "Registration Mail Scheduler"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model,name:portal_event_tickets.model_sale_order
+msgid "Sales Order"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model,name:portal_event_tickets.model_sale_order_line
+msgid "Sales Order Line"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_my_home_event
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_my_tickets
+msgid "There are currently no tickets for your account."
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "This ticket can be changed to other tickets or products. Click continue , register new ticket (or fill the cart by other tickets\\products) and follow usual checkout process."
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.message_origin_link
+msgid "This ticket has been canceled from:"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Ticket #"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_my_tickets
+msgid "Ticket ID"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_my_tickets
+msgid "Ticket Type"
+msgstr ""
+
+#. module: portal_event_tickets
+#: code:addons/portal_event_tickets/controllers/main.py:279
+#, python-format
+msgid "Ticket change: %s"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,field_description:portal_event_tickets.field_event_registration_is_transferring
+msgid "Ticket in transferring"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Ticket transferring"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,help:portal_event_tickets.field_event_registration_is_transferring
+msgid "Ticket transferring is started, but not finished"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.view_event_registration_ticket_form
+msgid "Ticket transferring was started, but haven't finished!"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,field_description:portal_event_tickets.field_event_registration_was_transferred
+#: model:ir.model.fields,help:portal_event_tickets.field_event_registration_was_transferred
+msgid "Ticket was transferred"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,help:portal_event_tickets.field_event_registration_was_updated
+msgid "Ticket was transferred or updated"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model.fields,field_description:portal_event_tickets.field_event_registration_was_updated
+msgid "Ticket was updated"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_my_home_menu_event
+msgid "Tickets"
+msgstr ""
+
+#. module: portal_event_tickets
+#: code:addons/portal_event_tickets/controllers/main.py:292
+#, python-format
+msgid "Total amount is negative. Please add more tickets or products"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Transfer to another person"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_transfer
+msgid "Transfered succefully"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Upgrade / Change ticket"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_page
+msgid "Upgrade / change ticket"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_transfer
+msgid "Use one of following link to see page in a mode you need."
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model,name:portal_event_tickets.model_website
+msgid "Website"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_my_home_event
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_my_tickets
+msgid "Your Tickets"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.model,name:portal_event_tickets.model_event_mail
+msgid "event.mail"
+msgstr ""
+
+#. module: portal_event_tickets
+#: model:ir.ui.view,arch_db:portal_event_tickets.portal_ticket_transfer
+msgid "there is no partner with that email"
+msgstr ""
+
diff --git a/portal_event_tickets/images/banner.jpg b/portal_event_tickets/images/banner.jpg
new file mode 100644
index 0000000..1368e2c
Binary files /dev/null and b/portal_event_tickets/images/banner.jpg differ
diff --git a/portal_event_tickets/models/__init__.py b/portal_event_tickets/models/__init__.py
new file mode 100644
index 0000000..d4d5ed8
--- /dev/null
+++ b/portal_event_tickets/models/__init__.py
@@ -0,0 +1,5 @@
+from . import website
+from . import event_registration
+from . import event_mail
+from . import event
+from . import sale_order
diff --git a/portal_event_tickets/models/event.py b/portal_event_tickets/models/event.py
new file mode 100644
index 0000000..4d9ae2a
--- /dev/null
+++ b/portal_event_tickets/models/event.py
@@ -0,0 +1,38 @@
+from odoo import fields, models
+from odoo.http import request
+
+
+class Event(models.Model):
+ _inherit = "event.event"
+
+ report_template_for_portal = fields.Many2one(
+ "ir.actions.report", "Badge Template For Portal"
+ )
+
+ ticket_transferring = fields.Boolean(
+ "Enable Ticket transferring",
+ help="Attendee can transfer ticket to another partner",
+ default=True,
+ )
+
+ ticket_changing = fields.Boolean(
+ "Enable Ticket Changing",
+ help="Attendee can change ticket to new ticket or products",
+ default=True,
+ )
+
+ def check_partner_for_new_ticket(self, partner_id):
+ registration = self.partner_is_participating(partner_id)
+ if registration:
+ ticket_order_lines = registration.mapped("sale_order_line_id")
+
+ if request.website.sale_get_order():
+ cart_refund_lines = request.website.sale_get_order().order_line.mapped(
+ lambda r: r.refund_source_line_id
+ )
+ # all refund lines must be in ticket_order_lines
+ if all([line in cart_refund_lines for line in ticket_order_lines]):
+ # False means no errors
+ return False
+
+ return super(Event, self).check_partner_for_new_ticket(partner_id)
diff --git a/portal_event_tickets/models/event_mail.py b/portal_event_tickets/models/event_mail.py
new file mode 100644
index 0000000..4716e4b
--- /dev/null
+++ b/portal_event_tickets/models/event_mail.py
@@ -0,0 +1,106 @@
+# pylint: disable=api-one-deprecated
+from datetime import datetime
+
+from dateutil.relativedelta import relativedelta
+
+from odoo import api, fields, models, tools
+
+_INTERVALS = {
+ "hours": lambda interval: relativedelta(hours=interval),
+ "days": lambda interval: relativedelta(days=interval),
+ "weeks": lambda interval: relativedelta(days=7 * interval),
+ "months": lambda interval: relativedelta(months=interval),
+ "now": lambda interval: relativedelta(hours=0),
+}
+
+
+class EventMailScheduler(models.Model):
+ _inherit = "event.mail"
+
+ interval_type = fields.Selection(
+ selection_add=[
+ ("transferring_started", "Transferring started"),
+ ("transferring_finished", "Transferring finished"),
+ ],
+ ondelete={
+ "transferring_started": "cascade",
+ "transferring_finished": "cascade",
+ },
+ )
+
+ @api.depends(
+ "event_id.registration_ids.state",
+ "event_id.date_begin",
+ "interval_type",
+ "interval_unit",
+ "interval_nbr",
+ )
+ def _compute_scheduled_date(self):
+ for rself in self:
+ if rself.interval_type not in [
+ "transferring_started",
+ "transferring_finished",
+ ]:
+ super(EventMailScheduler, rself)._compute_scheduled_date()
+ continue
+
+ if rself.event_id.state not in ["confirm", "done"]:
+ rself.scheduled_date = False
+ else:
+ date, sign = rself.event_id.create_date, 1
+ rself.scheduled_date = datetime.strptime(
+ date, tools.DEFAULT_SERVER_DATETIME_FORMAT
+ ) + _INTERVALS[rself.interval_unit](sign * rself.interval_nbr)
+
+ def execute(self, registration=None):
+ for rself in self:
+ if rself.interval_type not in [
+ "transferring_started",
+ "transferring_finished",
+ ]:
+ super(EventMailScheduler, rself).execute()
+ continue
+
+ if registration:
+ rself.write(
+ {
+ "mail_registration_ids": [
+ (0, 0, {"registration_id": registration.id})
+ ]
+ }
+ )
+ # execute scheduler on registrations
+ rself.mail_registration_ids.filtered(
+ lambda reg: reg.scheduled_date
+ and reg.scheduled_date
+ <= datetime.strftime(
+ fields.datetime.now(), tools.DEFAULT_SERVER_DATETIME_FORMAT
+ )
+ ).execute()
+ return True
+
+
+class EventMailRegistration(models.Model):
+ _inherit = "event.mail.registration"
+
+ @api.depends(
+ "registration_id", "scheduler_id.interval_unit", "scheduler_id.interval_type"
+ )
+ def _compute_scheduled_date(self):
+ for rself in self:
+ if rself.scheduler_id.interval_type not in [
+ "transferring_started",
+ "transferring_finished",
+ ]:
+ super(EventMailRegistration, rself)._compute_scheduled_date()
+ continue
+
+ if rself.registration_id:
+ # date_open is not corresponded to its meaining,
+ # but keep because it's copy-pasted code
+ date_open_datetime = fields.datetime.now()
+ rself.scheduled_date = date_open_datetime + _INTERVALS[
+ rself.scheduler_id.interval_unit
+ ](rself.scheduler_id.interval_nbr)
+ else:
+ rself.scheduled_date = False
diff --git a/portal_event_tickets/models/event_registration.py b/portal_event_tickets/models/event_registration.py
new file mode 100644
index 0000000..bd8a23f
--- /dev/null
+++ b/portal_event_tickets/models/event_registration.py
@@ -0,0 +1,98 @@
+from odoo import api, fields, models
+
+
+class EventRegistration(models.Model):
+ _inherit = "event.registration"
+
+ # New fields
+ is_transferring = fields.Boolean(
+ "Ticket in transferring",
+ help="Ticket transferring is started, but not finished",
+ default=False,
+ )
+ was_transferred = fields.Boolean(
+ "Ticket was transferred",
+ help="Ticket was transferred",
+ default=False,
+ )
+ origin_registration = fields.Many2one(
+ "event.registration",
+ compute="_compute_origin_registration",
+ store=True,
+ string="Original Ticket",
+ tracking=True,
+ )
+ was_updated = fields.Boolean(
+ "Ticket was updated",
+ compute="_compute_was_updated",
+ help="Ticket was transferred or updated",
+ default=False,
+ )
+
+ # Updated fields
+ email = fields.Char(tracking=True)
+ phone = fields.Char(tracking=True)
+ name = fields.Char(tracking=True)
+
+ attendee_partner_id = fields.Many2one(tracking=True)
+ partner_id = fields.Many2one(tracking=True)
+ event_id = fields.Many2one(tracking=True)
+ event_ticket_id = fields.Many2one(tracking=True)
+
+ @api.depends("sale_order_id", "sale_order_id.order_line")
+ def _compute_origin_registration(self):
+ for r in self:
+ order = False
+ if r.sale_order_id:
+ refunded_lines = r.sale_order_id.order_line.filtered(
+ lambda x: x.refund_source_line_id
+ ).mapped("refund_source_line_id")
+ if refunded_lines:
+ order = refunded_lines[0].order_id
+ r.origin_registration = order and self.search(
+ [("state", "=", "cancel"), ("sale_order_id", "=", order.id)], limit=1
+ )
+
+ @api.depends("was_transferred", "origin_registration")
+ def _compute_was_updated(self):
+ for r in self:
+ r.was_updated = False
+ if r.was_transferred or r.origin_registration:
+ r.was_updated = True
+
+ def transferring_started(self, receiver):
+ self.ensure_one()
+ self.write(
+ {
+ "attendee_partner_id": receiver.id,
+ "email": receiver.email,
+ "name": receiver.name,
+ "phone": receiver.phone,
+ "is_transferring": True,
+ }
+ )
+
+ # trigger email sending
+ onsubscribe_schedulers = self.event_id.event_mail_ids.filtered(
+ lambda s: s.interval_type == "transferring_started"
+ )
+ onsubscribe_schedulers.execute(self) # self is a registration
+
+ def transferring_finished(self):
+ self.ensure_one()
+ receiver = self.attendee_partner_id
+ # Update name and phone in registration, because those may be changed
+ # Mark that transferring is finished
+ self.write(
+ {
+ "name": receiver.name,
+ "phone": receiver.phone,
+ "is_transferring": False,
+ "was_transferred": True,
+ }
+ )
+ # trigger email sending
+ onsubscribe_schedulers = self.event_id.event_mail_ids.filtered(
+ lambda s: s.interval_type == "transferring_finished"
+ )
+ onsubscribe_schedulers.execute(self) # self is a registration
diff --git a/portal_event_tickets/models/sale_order.py b/portal_event_tickets/models/sale_order.py
new file mode 100644
index 0000000..5a1a2e3
--- /dev/null
+++ b/portal_event_tickets/models/sale_order.py
@@ -0,0 +1,31 @@
+from odoo import models
+
+
+class SaleOrder(models.Model):
+ _inherit = "sale.order"
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+
+ def _cancel_line(self, origin=None):
+ res = super(SaleOrderLine, self)._cancel_line(origin=origin)
+
+ tickets = self.env["event.registration"].search(
+ [
+ ("sale_order_line_id", "in", self.ids),
+ ("attendee_partner_id", "=", origin.partner_id.id),
+ ("event_id", "=", self.event_id.id),
+ ]
+ )
+ tickets.action_cancel()
+
+ for t in tickets:
+ # post a message why it was canceled
+ t.message_post_with_view(
+ "portal_event_tickets.message_origin_link",
+ values={"origin": origin},
+ subtype_id=self.env.ref("mail.mt_note").id,
+ )
+
+ return res
diff --git a/portal_event_tickets/models/website.py b/portal_event_tickets/models/website.py
new file mode 100644
index 0000000..46599a6
--- /dev/null
+++ b/portal_event_tickets/models/website.py
@@ -0,0 +1,20 @@
+from odoo import fields, models
+
+
+class Website(models.Model):
+ _inherit = "website"
+
+ ticket_transfer_success = fields.Html(
+ "Message ticket transfer: success",
+ default="Ticket is transfered successfully. We will instruct receiver on further actions. ", # noqa: E501
+ )
+
+ ticket_transfer_receiver_not_found = fields.Html(
+ "Message ticket transfer: receiver not found",
+ default="""User with the email is not found. The user has to signup first. """, # noqa: E501
+ )
+
+ ticket_transfer_receiver_has_ticket = fields.Html(
+ "Message ticket transfer: receiver already has ticket",
+ default="""User already has the ticket. """,
+ )
diff --git a/portal_event_tickets/pyproject.toml b/portal_event_tickets/pyproject.toml
new file mode 100644
index 0000000..4231d0c
--- /dev/null
+++ b/portal_event_tickets/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/portal_event_tickets/readme/CONFIGURE.md b/portal_event_tickets/readme/CONFIGURE.md
new file mode 100644
index 0000000..ce27f9b
--- /dev/null
+++ b/portal_event_tickets/readme/CONFIGURE.md
@@ -0,0 +1,18 @@
+Ticket transferring configuration
+---------------------------------
+
+At event form:
+
+* Activate `[x] Enable Ticket transferring`
+* At `Email Schedule` Tab add record:
+
+ * **Email To Send**: *Event: Transferring started*
+ * **Unit**: *Immediately*
+ * **When to Run**: *Transferring started*
+
+Ticket changing configuration
+-----------------------------
+
+At event form:
+
+* Activate `[x] Enable Ticket changing`
diff --git a/portal_event_tickets/readme/CONTRIBUTORS.md b/portal_event_tickets/readme/CONTRIBUTORS.md
new file mode 100644
index 0000000..5ffabe4
--- /dev/null
+++ b/portal_event_tickets/readme/CONTRIBUTORS.md
@@ -0,0 +1,5 @@
+- Ivan Yelizariev (https://github.com/yelizariev)
+- Alexandr Kolushov (https://github.com/KolushovAlexandr)
+- Eugene Molotov (https://github.com/em230418)
+- Victor Bykov (https://github.com/BykovVik)
+- Ilmir Karamov (https://github.com/ilmir-k)
diff --git a/portal_event_tickets/readme/DESCRIPTION.md b/portal_event_tickets/readme/DESCRIPTION.md
new file mode 100644
index 0000000..ccdbca5
--- /dev/null
+++ b/portal_event_tickets/readme/DESCRIPTION.md
@@ -0,0 +1,21 @@
+Allows to customers see tickets for events at Portal.
+
+* Only confirmed tickets with attendee_partner_id as current user are shown
+
+Additional features:
+
+* Ticket transferring feature
+
+ - To decrease chance of transferring to a wrong email, partner with the email must exist before transferring.
+ - New *When to Run* values for Email Schedule:
+
+ * transferring_started
+ * transferring_finished
+
+ - New attendee receives email with a link to finish ticket transferring
+
+* Tracks changes in key registration fields (via `tracking=True`)
+
+* Tickets can be changed to other products (including other tickets)
+
+ - When old ticket is canceled, a message with a reference to new Sale Order is posted
diff --git a/portal_event_tickets/readme/USAGE.md b/portal_event_tickets/readme/USAGE.md
new file mode 100644
index 0000000..4a21b57
--- /dev/null
+++ b/portal_event_tickets/readme/USAGE.md
@@ -0,0 +1,33 @@
+* Open link `/my`
+* RESULT: there is sections Tickets
+
+Ticket transferring
+-------------------
+
+Feature allows for attendees to transfer ticket ownership to another partner by email.
+
+* Login to portal as current ticket attendee
+* Select a ticket
+* Click button `[Transfer to another person]`
+* Specify person's email. The partner must be already registered
+* Click `[Confrim]`
+
+Now second person receives an email. If you use test deployment without mail servers, then you can find email at menu `[[ Settings ]] >> Technical >> Email >> Messages`.
+
+* Login to portal as new ticket attendee
+* Open then link at email
+* Fill the form
+* Click `[Confrim]`
+* RESULT: Ticket has new owner
+
+Ticket changing
+---------------
+
+Feature allows to change the ticket to new ticket or product
+
+* Login to portal
+* Select a ticket
+* Click button `[Upgrade / Change ticket]`
+* You are redirected to original event page. You can select new ticket or navigate to `/shop` page and fill the cart
+* Follow checkout process
+* When the order is confirmed (e.g. after payment), old ticket is canceled and new one is confirmed
diff --git a/portal_event_tickets/static/description/icon.png b/portal_event_tickets/static/description/icon.png
new file mode 100644
index 0000000..5d32362
Binary files /dev/null and b/portal_event_tickets/static/description/icon.png differ
diff --git a/portal_event_tickets/static/description/index.html b/portal_event_tickets/static/description/index.html
new file mode 100644
index 0000000..4e019e2
--- /dev/null
+++ b/portal_event_tickets/static/description/index.html
@@ -0,0 +1,100 @@
+
+
+
+
Portal Event
+ Customer UI to see and manage tickets
+
+
+
+
+
+
+
+
+
+
Key features:
+
+
+
+
+ Customer can see all his tickets
+
+
+
+
+ Possiblity to transfer ticket to another person (optional)
+
+
+
+
+ Possiblity to change / upgrade ticket (optional)
+
+
+
+
+
+
+
+
+
+
+
+
New section in Portal
+
+
+
+
+
+
+
+
+
+
+
+
+ Tested on Odoo 11.0 community
+
+
+
+
+
+
diff --git a/portal_event_tickets/static/description/ticket.png b/portal_event_tickets/static/description/ticket.png
new file mode 100644
index 0000000..c1170db
Binary files /dev/null and b/portal_event_tickets/static/description/ticket.png differ
diff --git a/portal_event_tickets/static/src/js/portal.js b/portal_event_tickets/static/src/js/portal.js
new file mode 100644
index 0000000..c8491cc
--- /dev/null
+++ b/portal_event_tickets/static/src/js/portal.js
@@ -0,0 +1,37 @@
+odoo.define("portal_event_tickets.portal", function (require) {
+ "use strict";
+
+ var publicWidget = require("web.public.widget");
+
+ publicWidget.registry.transferTicketWidget = publicWidget.Widget.extend({
+ selector: "#transfer_ticket",
+
+ start: function () {
+ var def = this._super.apply(this, arguments);
+
+ var event_name = this.$el.data("event-name");
+ var $modal = $("#modal_attendees_registration");
+
+ /* Show form inline */
+ $modal.find("form").attr("action", "/my/tickets/transfer/receive");
+ $modal.removeClass("modal fade");
+
+ /* Remove Cancel button; update title */
+ var $submit = $modal.find("[type=submit]");
+ $submit.parent().empty().append($submit);
+ $submit.text("Confirm");
+
+ /* Remove Close button */
+ $modal.find(".close").remove();
+
+ /* Make email non-editable */
+ $modal.find("[name=1-email]").attr("disabled", "1");
+
+ /* Update title */
+ $modal
+ .find("h4.modal-title")
+ .html("Receive the ticket for " + event_name + " ");
+ return def;
+ },
+ });
+});
diff --git a/portal_event_tickets/static/src/js/ticket_transfer.tour.js b/portal_event_tickets/static/src/js/ticket_transfer.tour.js
new file mode 100644
index 0000000..7e1194f
--- /dev/null
+++ b/portal_event_tickets/static/src/js/ticket_transfer.tour.js
@@ -0,0 +1,40 @@
+odoo.define("portal_event_tickets.ticket_transfer_tour", function (require) {
+ "use strict";
+
+ var tour = require("web_tour.tour");
+
+ var options = {
+ test: true,
+ url: "/my/tickets/transfer/receive",
+ };
+
+ var tour_name = "ticket_transfer_receive";
+ tour.register(tour_name, options, [
+ {
+ content: "Fill attendees details",
+ extra_trigger: "input[name='1-function']",
+ trigger: "input[name='1-name']",
+ run: function () {
+ // Fill:
+ // * phone (optional)
+ // * country_id (mandatory)
+ // skip:
+ // * job position (optional)
+ $("input[name='1-phone']").val("111 111");
+ $("select[name='1-country_id']").val("1");
+ },
+ },
+ {
+ content: "Validate attendees details",
+ extra_trigger: "input[name='1-phone']",
+ trigger: 'button:contains("Confirm")',
+ },
+ {
+ content: "We are redirected to /my/tickets page",
+ trigger: ".breadcrumb-item:contains(Tickets)",
+ run: function () {
+ // It's needed to don't make a click on the link
+ },
+ },
+ ]);
+});
diff --git a/portal_event_tickets/tests/__init__.py b/portal_event_tickets/tests/__init__.py
new file mode 100644
index 0000000..14ea249
--- /dev/null
+++ b/portal_event_tickets/tests/__init__.py
@@ -0,0 +1,2 @@
+from . import test_ticket_transfer
+from . import test_ticket_pdf
diff --git a/portal_event_tickets/tests/common.py b/portal_event_tickets/tests/common.py
new file mode 100644
index 0000000..b28e955
--- /dev/null
+++ b/portal_event_tickets/tests/common.py
@@ -0,0 +1,128 @@
+from datetime import datetime, timedelta
+
+from odoo import fields
+from odoo.tests.common import HttpCase
+
+
+class TourCase(HttpCase):
+ def setUp(self):
+ super(TourCase, self).setUp()
+
+ # create Event
+ self.event = self.env["event.event"].create(
+ {
+ "name": "TestEvent",
+ "create_partner": True,
+ "date_begin": fields.Datetime.to_string(
+ datetime.today() + timedelta(days=1)
+ ),
+ "date_end": fields.Datetime.to_string(
+ datetime.today() + timedelta(days=15)
+ ),
+ "website_published": True,
+ }
+ )
+ self.ticket_type_1 = self.env.ref("event.event_0_ticket_1").copy(
+ {"event_id": self.event.id}
+ )
+ self.ticket_type_2 = self.env.ref("event.event_0_ticket_2").copy(
+ {"event_id": self.event.id}
+ )
+
+ self.event.write(
+ {
+ "attendee_field_ids": [
+ (
+ 6,
+ 0,
+ [
+ self.env.ref(
+ "website_event_attendee_fields.attendee_field_name"
+ ).id,
+ self.env.ref(
+ "website_event_attendee_fields.attendee_field_email"
+ ).id,
+ self.env.ref(
+ "website_event_attendee_fields.attendee_field_phone"
+ ).id,
+ self.env.ref(
+ "website_event_attendee_fields.attendee_field_country_id"
+ ).id,
+ self.env.ref(
+ "website_event_attendee_fields.attendee_field_function"
+ ).id,
+ ],
+ )
+ ]
+ }
+ )
+
+ # create Portal User
+ self.user_portal1 = self.env.ref("portal_event_tickets.user_portal1")
+
+ sale_order, self.ticket1 = self._create_ticket(
+ ticket_type=self.ticket_type_1,
+ partner=self.user_portal1.partner_id,
+ event=self.event,
+ )
+ sale_order.action_confirm()
+
+ self.user_portal2 = self.env.ref("portal_event_tickets.user_portal2")
+
+ def _create_ticket(self, ticket_type, partner, event):
+ product = ticket_type.product_id
+
+ # I create a sale order
+ sale_order = self.env["sale.order"].create(
+ {
+ "partner_id": partner.id,
+ "note": "Invoice after delivery",
+ }
+ )
+ sale_order.onchange_partner_id()
+
+ # In the sale order I add some sale order lines. i choose event product
+ sale_order_line = self.env["sale.order.line"].create(
+ {
+ "product_id": product.id,
+ # we set price_unit to 0
+ # to confirm registration via registration.editor
+ "price_unit": 0,
+ "product_uom": self.env.ref("uom.product_uom_unit").id,
+ "product_uom_qty": 1.0,
+ "order_id": sale_order.id,
+ "name": "sale order line",
+ "event_id": event.id,
+ "event_ticket_id": ticket_type.id,
+ }
+ )
+
+ # In the event registration I add some attendee detail lines.
+ # I choose event product
+ register_person = self.env["registration.editor"].create(
+ {
+ "sale_order_id": sale_order.id,
+ "event_registration_ids": [
+ (
+ 0,
+ 0,
+ {
+ "event_id": event.id,
+ "name": partner.name,
+ "email": partner.email,
+ "sale_order_line_id": sale_order_line.id,
+ },
+ )
+ ],
+ }
+ )
+
+ # I click apply to create attendees
+ register_person.action_make_registration()
+
+ return (
+ sale_order,
+ self.env["event.registration"].search(
+ [("sale_order_id", "=", sale_order.id)]
+ ),
+ )
diff --git a/portal_event_tickets/tests/test_ticket_pdf.py b/portal_event_tickets/tests/test_ticket_pdf.py
new file mode 100644
index 0000000..b7df33a
--- /dev/null
+++ b/portal_event_tickets/tests/test_ticket_pdf.py
@@ -0,0 +1,19 @@
+from odoo.tests.common import tagged
+
+from .common import TourCase
+
+
+@tagged("-at_install", "post_install")
+class TicketPDF(TourCase):
+ def test_ticket_access_pdf(self):
+ self.assertEqual(
+ self.ticket1.attendee_partner_id,
+ self.user_portal1.partner_id,
+ "Wrong attendee_partner_id value before the test",
+ )
+
+ login = self.user_portal1.login
+ self.authenticate(login, login)
+
+ r = self.url_open(f"/my/tickets/pdf/{self.ticket1.id}", allow_redirects=False)
+ self.assertEqual(r.status_code, 200)
diff --git a/portal_event_tickets/tests/test_ticket_transfer.py b/portal_event_tickets/tests/test_ticket_transfer.py
new file mode 100644
index 0000000..446c943
--- /dev/null
+++ b/portal_event_tickets/tests/test_ticket_transfer.py
@@ -0,0 +1,35 @@
+from odoo.tests.common import tagged
+
+from ..controllers.main import PortalEvent
+from .common import TourCase
+
+
+@tagged("-at_install", "post_install")
+class TicketTransfer(TourCase):
+ def test_ticket_transfer_tour(self):
+ """user_portal1 transfers his ticket1 to user_portal2"""
+
+ self.assertEqual(
+ self.ticket1.attendee_partner_id,
+ self.user_portal1.partner_id,
+ "Wrong attendee_partner_id value before the test",
+ )
+
+ # user_portal1 transfers ticket to user_portal2
+ env = self.env["res.users"].with_user(self.user_portal1).env
+ PortalEvent()._ticket_transfer(env, self.user_portal2.email, self.ticket1.id)
+
+ # user_portal2 click on the link in email
+ self.start_tour("/", "ticket_transfer_receive", login=self.user_portal2.login)
+
+ self.assertEqual(
+ self.ticket1.state,
+ "open",
+ "Ticket doesn't have state Confirmed after transfering",
+ )
+
+ self.assertEqual(
+ self.ticket1.attendee_partner_id,
+ self.user_portal2.partner_id,
+ "Ticket Attendee was not changed",
+ )
diff --git a/portal_event_tickets/views/assets.xml b/portal_event_tickets/views/assets.xml
new file mode 100644
index 0000000..ef6ecad
--- /dev/null
+++ b/portal_event_tickets/views/assets.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/portal_event_tickets/views/event_event.xml b/portal_event_tickets/views/event_event.xml
new file mode 100644
index 0000000..462d7e6
--- /dev/null
+++ b/portal_event_tickets/views/event_event.xml
@@ -0,0 +1,14 @@
+
+
+
+ Events (Singup)
+ event.event
+
+
+
+
+
+
+
+
+
diff --git a/portal_event_tickets/views/event_registration.xml b/portal_event_tickets/views/event_registration.xml
new file mode 100644
index 0000000..ffceec6
--- /dev/null
+++ b/portal_event_tickets/views/event_registration.xml
@@ -0,0 +1,32 @@
+
+
+
+ event.registration.form.inherit
+ event.registration
+
+
+
+
+
+
+
+
+
+ Ticket transferring was started, but haven't finished!
+
+
+
+
+ event.registration.tree
+ event.registration
+
+
+
+
+
+
+
+
diff --git a/portal_event_tickets/views/portal_templates.xml b/portal_event_tickets/views/portal_templates.xml
new file mode 100644
index 0000000..91b4e97
--- /dev/null
+++ b/portal_event_tickets/views/portal_templates.xml
@@ -0,0 +1,354 @@
+
+
+
+
+ o_portal container mt-3 mb-2
+
+
+
+
+
+
+ Tickets
+
+
+
+
+
+
+
+
+
+ There are currently no invoices and payments for your account.
+
+
+
+
+
+ Ticket ID
+ Event
+ Ticket Type
+
+
+
+
+
+ #
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Use one of following link to see page in a mode you need.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This ticket has been canceled from:
+
+
+
+
+ ,
+
+
+
+