Initialize fork and rebrand app to event_manager
CI / Server (push) Has been cancelled
Linters / Frappe Linter (push) Has been cancelled
Linters / Vulnerable Dependency Check (push) Has been cancelled
UI Tests / Playwright E2E Tests (push) Has been cancelled

This commit is contained in:
2026-05-11 09:56:57 +02:00
parent f82bb803ac
commit 786cbc724f
500 changed files with 41152 additions and 2 deletions
+9
View File
@@ -0,0 +1,9 @@
__version__ = "0.0.1"
import os
if os.environ.get("CI"):
import frappe
from frappe.tests.utils import toggle_test_mode
toggle_test_mode(True)
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
import frappe
from frappe.utils import cint, md_to_html
from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys
@frappe.whitelist(allow_guest=True) # nosemgrep: frappe-semgrep-rules.rules.security.guest-whitelisted-method
def get_login_context(redirect_to: str | None = None):
context = {
"disable_signup": frappe.get_website_settings("disable_signup"),
"disable_user_pass_login": frappe.get_system_settings("disable_user_pass_login"),
"login_with_email_link": frappe.get_system_settings("login_with_email_link"),
"login_banner": md_to_html(raw_banner)
if (raw_banner := frappe.db.get_single_value("Pohodex Event Manager Settings", "login_banner"))
else None,
"provider_logins": [],
}
if not redirect_to:
redirect_to = frappe.utils.get_url("/dashboard")
social_login_keys = frappe.get_all(
"Social Login Key",
filters={"enable_social_login": 1},
fields=["name", "provider_name", "icon", "client_id", "base_url"],
)
for provider in social_login_keys:
if provider.client_id and provider.base_url and get_oauth_keys(provider.name):
context["provider_logins"].append(
{
"name": provider.name,
"provider_name": provider.provider_name,
"icon": provider.icon or "",
"auth_url": get_oauth2_authorize_url(provider.name, redirect_to),
}
)
return context
+344
View File
@@ -0,0 +1,344 @@
from functools import lru_cache
import frappe
from frappe import _
from frappe.geo.country_info import get_all as get_all_countries
from frappe.model import DEFAULT_FIELDS, display_fieldtypes
from frappe.utils import get_datetime, now_datetime, today
from frappe.utils.data import cstr, sbool
LAYOUT_FIELDTYPES = set(display_fieldtypes)
EVENT_PROPOSAL_EXCLUDE_FIELDS = DEFAULT_FIELDS | {
"naming_series",
"amended_from",
"host",
"host_company",
"host_company_logo",
"additional_notes",
"status",
"submitted_by",
}
STANDARD_EXCLUDE_FIELDS = DEFAULT_FIELDS | {
"additional_fields",
"event",
"section_break_additional",
"submitted_by",
"status",
}
@frappe.whitelist(allow_guest=True) # nosemgrep: frappe-semgrep-rules.rules.security.guest-whitelisted-method
def get_dial_codes() -> list:
return _get_dial_codes()
@lru_cache(maxsize=1)
def _get_dial_codes() -> list:
data = get_all_countries()
codes = []
seen = set()
for country in sorted(data):
info = data[country]
isd = info.get("isd", "")
code = (info.get("code") or "").upper()
if isd and isd not in seen:
codes.append({"country": country, "code": code, "dial_code": isd})
seen.add(isd)
return codes
def get_form_fields(doctype: str, exclude_fields: set) -> list:
meta = frappe.get_meta(doctype)
fields = []
for df in meta.fields:
if df.fieldname in exclude_fields:
continue
if df.fieldtype in LAYOUT_FIELDTYPES:
continue
if df.hidden:
continue
if df.read_only:
continue
default_value = df.default
if default_value:
if default_value == "Today":
default_value = today()
elif default_value == "Now":
default_value = cstr(now_datetime())
elif default_value.startswith("eval:") or default_value.startswith("%"):
default_value = None
field_data = {
"fieldname": df.fieldname,
"fieldtype": df.fieldtype,
"label": df.label or df.fieldname,
"options": df.options,
"reqd": df.reqd,
"default": default_value,
"description": df.description,
}
if df.fieldtype == "Link" and df.options:
link_values = frappe.get_all(
df.options,
fields=["name"],
limit_page_length=0,
order_by="name asc",
)
field_data["link_options"] = [d.name for d in link_values]
if df.fieldtype == "Table" and df.options:
child_meta = frappe.get_meta(df.options)
child_fields = []
for child_df in child_meta.fields:
if child_df.fieldtype in LAYOUT_FIELDTYPES:
continue
if child_df.hidden:
continue
child_fields.append(
{
"fieldname": child_df.fieldname,
"fieldtype": child_df.fieldtype,
"label": child_df.label or child_df.fieldname,
"options": child_df.options,
"reqd": child_df.reqd,
}
)
field_data["child_fields"] = child_fields
fields.append(field_data)
return fields
def validate_custom_form(event_route: str, form_route: str):
event_name = frappe.get_cached_value("Pohodex Event Manager Event", {"route": event_route}, "name")
if not event_name:
frappe.throw(_("Event not found"), frappe.DoesNotExistError)
event_doc = frappe.get_cached_doc("Pohodex Event Manager Event", event_name)
if not event_doc.is_published:
frappe.throw(_("Event not found"), frappe.DoesNotExistError)
form_row = None
for row in event_doc.custom_forms:
if row.route == form_route and row.publish:
form_row = row
break
if not form_row:
frappe.throw(_("This form is not available for this event"), frappe.DoesNotExistError)
return event_doc, form_row
def get_auto_set_fields(form_doctype: str):
meta = frappe.get_meta(form_doctype)
auto_set = {}
for df in meta.fields:
if df.fieldname == "event" and df.fieldtype == "Link":
auto_set["event"] = "from_route"
elif df.fieldname == "submitted_by" and df.fieldtype == "Link":
auto_set["submitted_by"] = "session_user"
return auto_set
@frappe.whitelist(allow_guest=True) # nosemgrep: frappe-semgrep-rules.rules.security.guest-whitelisted-method
def get_custom_form_data(event_route: str, form_route: str) -> dict:
event_doc, form_row = validate_custom_form(event_route, form_route)
form_doctype = form_row.form_doctype
allow_guest_submission = sbool(event_doc.allow_guest_booking)
if not allow_guest_submission and frappe.session.user == "Guest":
frappe.throw(_("Please log in to submit this form"), frappe.AuthenticationError)
event_data = {
"name": event_doc.name,
"title": event_doc.title,
"route": event_doc.route,
"banner_image": event_doc.banner_image,
"start_date": event_doc.start_date,
"end_date": event_doc.end_date,
"start_time": event_doc.start_time,
"end_time": event_doc.end_time,
"time_zone": event_doc.time_zone,
"venue": event_doc.venue,
"medium": event_doc.medium,
"short_description": event_doc.short_description,
}
closed = False
if form_row.auto_close_at and get_datetime(form_row.auto_close_at) < now_datetime():
closed = True
if closed:
return {
"form_fields": [],
"custom_fields": [],
"form_title": form_doctype,
"event": event_data,
"closed": True,
"closed_title": form_row.closed_title or _("Submissions Closed"),
"closed_message": form_row.closed_message or _("Submissions for this form have closed."),
"success_title": "",
"success_message": "",
}
auto_set = get_auto_set_fields(form_doctype)
exclude_fields = STANDARD_EXCLUDE_FIELDS | set(auto_set.keys())
form_fields = get_form_fields(form_doctype, exclude_fields)
form_doctype_meta = frappe.get_meta(form_doctype)
custom_fields = []
if form_doctype_meta.has_field("additional_fields"):
custom_fields = frappe.get_all(
"Pohodex Event Manager Custom Field",
filters={
"event": event_doc.name,
"applied_to": "Custom Form",
"custom_form_doctype": form_doctype,
"enabled": 1,
},
fields=[
"label",
"fieldname",
"fieldtype",
"options",
"mandatory",
"placeholder",
"default_value",
"order",
],
order_by="order asc",
)
return {
"form_fields": form_fields,
"custom_fields": custom_fields,
"form_title": form_doctype_meta.name,
"event": event_data,
"closed": False,
"closed_title": form_row.closed_title or _("Submissions Closed"),
"closed_message": form_row.closed_message or _("Submissions for this form have closed."),
"success_title": form_row.success_title or _("Thank you!"),
"success_message": form_row.success_message or "",
}
@frappe.whitelist(allow_guest=True) # nosemgrep: frappe-semgrep-rules.rules.security.guest-whitelisted-method
def submit_custom_form(
event_route: str, form_route: str, data: dict | str, custom_fields_data: dict | str | None = None
) -> None:
event_doc, form_row = validate_custom_form(event_route, form_route)
form_doctype = form_row.form_doctype
if not event_doc.allow_guest_booking and frappe.session.user == "Guest":
frappe.throw(_("Please login to submit this form"), frappe.AuthenticationError)
if form_row.auto_close_at and get_datetime(form_row.auto_close_at) < now_datetime():
frappe.throw(_("Submissions for this form have closed."))
data = frappe.parse_json(data) or {}
custom_fields_data = frappe.parse_json(custom_fields_data) or {}
auto_set = get_auto_set_fields(form_doctype)
exclude_fields = STANDARD_EXCLUDE_FIELDS | set(auto_set.keys())
doc_data = {"doctype": form_doctype}
for field, source in auto_set.items():
if source == "from_route":
doc_data[field] = event_doc.name
elif source == "session_user":
doc_data[field] = frappe.session.user
allowed_fieldnames = {f["fieldname"] for f in get_form_fields(form_doctype, exclude_fields)}
for fieldname, value in data.items():
if fieldname in allowed_fieldnames:
doc_data[fieldname] = value
meta = frappe.get_meta(form_doctype)
for df in meta.fields:
if df.fieldtype == "Table" and df.fieldname not in exclude_fields:
if df.fieldname in data and isinstance(data[df.fieldname], list):
doc_data[df.fieldname] = data[df.fieldname]
doc = frappe.get_doc(doc_data)
if custom_fields_data and meta.has_field("additional_fields"):
custom_field_definitions = frappe.get_all(
"Pohodex Event Manager Custom Field",
filters={
"event": event_doc.name,
"applied_to": "Custom Form",
"custom_form_doctype": form_doctype,
"enabled": 1,
},
fields=["fieldname", "label", "fieldtype"],
)
allowed_custom = {cf["fieldname"]: cf for cf in custom_field_definitions}
for fieldname, value in custom_fields_data.items():
if fieldname in allowed_custom and value not in (None, ""):
cf = allowed_custom[fieldname]
doc.append(
"additional_fields",
{
"label": cf["label"],
"fieldname": fieldname,
"fieldtype": cf["fieldtype"],
"value": cstr(value),
},
)
doc.insert(ignore_permissions=True)
def validate_event_proposal_settings():
settings = frappe.get_cached_doc("Pohodex Event Manager Settings")
if not settings.accept_event_proposals:
frappe.throw(_("Event Proposals are not being accepted"), frappe.DoesNotExistError)
if not settings.allow_guest_event_proposals and frappe.session.user == "Guest":
frappe.throw(_("Please log in to submit a proposal"), frappe.AuthenticationError)
return settings
@frappe.whitelist(allow_guest=True) # nosemgrep: frappe-semgrep-rules.rules.security.guest-whitelisted-method
def get_event_proposal_form_data() -> dict:
settings = validate_event_proposal_settings()
form_fields = get_form_fields("Event Proposal", EVENT_PROPOSAL_EXCLUDE_FIELDS)
return {
"form_fields": form_fields,
"form_title": _("Event Proposal"),
"banner_title": settings.event_proposal_banner_title or _("Propose an Event"),
"success_title": settings.event_proposal_success_title or _("Thank you!"),
"success_message": settings.event_proposal_success_message or "",
}
@frappe.whitelist(allow_guest=True) # nosemgrep: frappe-semgrep-rules.rules.security.guest-whitelisted-method
def submit_event_proposal(data: dict | str) -> None:
validate_event_proposal_settings()
data = frappe.parse_json(data) or {}
doc_data = {"doctype": "Event Proposal"}
if frappe.session.user != "Guest":
doc_data["submitted_by"] = frappe.session.user
allowed_fieldnames = {
f["fieldname"] for f in get_form_fields("Event Proposal", EVENT_PROPOSAL_EXCLUDE_FIELDS)
}
for fieldname, value in data.items():
if fieldname in allowed_fieldnames:
doc_data[fieldname] = value
meta = frappe.get_meta("Event Proposal")
for df in meta.fields:
if df.fieldtype == "Table" and df.fieldname not in EVENT_PROPOSAL_EXCLUDE_FIELDS:
if df.fieldname in data and isinstance(data[df.fieldname], list):
doc_data[df.fieldname] = data[df.fieldname]
frappe.get_doc(doc_data).insert(ignore_permissions=True)
View File
@@ -0,0 +1,15 @@
// Copyright (c) 2025, BWH Studios and contributors
// For license information, please see license.txt
frappe.ui.form.on("Pohodex Event Manager Custom Field", {
refresh(frm) {
frm.set_query("custom_form_doctype", function () {
return {
filters: {
istable: 0,
issingle: 0,
},
};
});
},
});
@@ -0,0 +1,144 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-11-01 11:29:38.327158",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enabled",
"event",
"label",
"fieldname",
"mandatory",
"placeholder",
"default_value",
"column_break_fpgn",
"applied_to",
"custom_form_doctype",
"offline_payment_method",
"fieldtype",
"options",
"order"
],
"fields": [
{
"fieldname": "event",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Event",
"options": "Pohodex Event Manager Event",
"reqd": 1
},
{
"fieldname": "column_break_fpgn",
"fieldtype": "Column Break"
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"reqd": 1
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"label": "Name"
},
{
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "Data\nCheck\nSmall Text\nPhone\nEmail\nSelect\nDate\nNumber\nMulti Select\nRating\nAttach\nAttach Image",
"reqd": 1
},
{
"fieldname": "options",
"fieldtype": "Small Text",
"label": "Options"
},
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled?"
},
{
"default": "0",
"fieldname": "mandatory",
"fieldtype": "Check",
"label": "Mandatory?"
},
{
"fieldname": "placeholder",
"fieldtype": "Data",
"label": "Placeholder"
},
{
"default": "1",
"fieldname": "order",
"fieldtype": "Int",
"label": "Order",
"non_negative": 1
},
{
"fieldname": "default_value",
"fieldtype": "Data",
"label": "Default Value"
},
{
"default": "Booking",
"fieldname": "applied_to",
"fieldtype": "Select",
"label": "Applied To",
"options": "Booking\nTicket\nOffline Payment Form\nCustom Form"
},
{
"depends_on": "eval:doc.applied_to === 'Custom Form'",
"fieldname": "custom_form_doctype",
"fieldtype": "Link",
"label": "Custom Form DocType",
"mandatory_depends_on": "eval:doc.applied_to === 'Custom Form'",
"options": "DocType"
},
{
"depends_on": "eval:doc.applied_to === 'Offline Payment Form'",
"fieldname": "offline_payment_method",
"fieldtype": "Link",
"label": "Offline Payment Method",
"mandatory_depends_on": "eval:doc.applied_to === 'Offline Payment Form'",
"options": "Offline Payment Method"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-02-06 15:26:27.195623",
"modified_by": "Administrator",
"module": "Pohodex Event Manager",
"name": "Pohodex Event Manager Custom Field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "label"
}
@@ -0,0 +1,72 @@
# Copyright (c) 2025, BWH Studios and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class BuzzCustomField(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
applied_to: DF.Literal["Booking", "Ticket", "Offline Payment Form"]
default_value: DF.Data | None
enabled: DF.Check
event: DF.Link
fieldname: DF.Data | None
fieldtype: DF.Literal[
"Data", "Check", "Small Text", "Phone", "Email", "Select", "Date", "Number", "Multi Select"
]
label: DF.Data
mandatory: DF.Check
options: DF.SmallText | None
order: DF.Int
placeholder: DF.Data | None
# end: auto-generated types
def validate(self):
if not self.fieldname:
self.fieldname = frappe.scrub(self.label)
def on_update(self):
if self.applied_to == "Custom Form" and self.custom_form_doctype:
self.create_additional_fields_if_missing()
def create_additional_fields_if_missing(self):
meta = frappe.get_meta(self.custom_form_doctype)
if meta.has_field("additional_fields"):
return
frappe.get_doc(
{
"doctype": "Custom Field",
"dt": self.custom_form_doctype,
"fieldname": "section_break_additional",
"label": "Additional Fields",
"fieldtype": "Section Break",
}
).insert(ignore_permissions=True)
frappe.get_doc(
{
"doctype": "Custom Field",
"dt": self.custom_form_doctype,
"fieldname": "additional_fields",
"label": "Additional Fields",
"fieldtype": "Table",
"options": "Additional Field",
"insert_after": "section_break_additional",
}
).insert(ignore_permissions=True)
frappe.msgprint(
_("Added 'Additional Fields' table to {0}").format(self.custom_form_doctype),
alert=True,
)
@@ -0,0 +1,20 @@
# Copyright (c) 2025, BWH Studios and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestBuzzCustomField(IntegrationTestCase):
"""
Integration tests for BuzzCustomField.
Use this class for testing interactions between multiple components.
"""
pass
@@ -0,0 +1,3 @@
frappe.ready(function () {
// bind events here
});
@@ -0,0 +1,135 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 0,
"allow_incomplete": 0,
"allow_multiple": 1,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"anonymous": 0,
"apply_document_permissions": 0,
"button_label": "Submit",
"condition_json": "[]",
"creation": "2025-10-09 18:53:54.552643",
"currency": "INR",
"doc_type": "Sponsorship Enquiry",
"docstatus": 0,
"doctype": "Web Form",
"hide_footer": 1,
"hide_navbar": 1,
"idx": 0,
"is_standard": 1,
"list_columns": [],
"login_required": 1,
"max_attachment_size": 5,
"modified": "2025-10-30 16:52:36.708858",
"modified_by": "Administrator",
"module": "Pohodex Event Manager",
"name": "apply-for-sponsorship",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
"route": "apply-for-sponsorship",
"show_attachments": 0,
"show_list": 1,
"show_sidebar": 0,
"success_url": "/dashboard/account/sponsorships",
"title": "Apply for Sponsorship",
"web_form_fields": [
{
"allow_read_on_all_link_options": 1,
"fieldname": "event",
"fieldtype": "Link",
"hidden": 0,
"label": "Event",
"max_length": 0,
"max_value": 0,
"options": "Pohodex Event Manager Event",
"precision": "",
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "company_name",
"fieldtype": "Data",
"hidden": 0,
"label": "Company Name",
"max_length": 0,
"max_value": 0,
"precision": "",
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "company_logo",
"fieldtype": "Attach Image",
"hidden": 0,
"label": "Company Logo",
"max_length": 0,
"max_value": 0,
"precision": "",
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "column_break_fhgg",
"fieldtype": "Column Break",
"hidden": 0,
"max_length": 0,
"max_value": 0,
"precision": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "website",
"fieldtype": "Data",
"hidden": 0,
"label": "Website",
"max_length": 0,
"max_value": 0,
"options": "URL",
"precision": "",
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 1,
"fieldname": "country",
"fieldtype": "Link",
"hidden": 0,
"label": "Country",
"max_length": 0,
"max_value": 0,
"options": "Country",
"precision": "",
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "phone",
"fieldtype": "Phone",
"hidden": 0,
"label": "Phone Number",
"max_length": 0,
"max_value": 0,
"precision": "",
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
}
]
}
@@ -0,0 +1,6 @@
import frappe
def get_context(context):
# do your magic here
pass
@@ -0,0 +1,3 @@
frappe.ready(function () {
// bind events here
});
@@ -0,0 +1,133 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 1,
"allow_edit": 0,
"allow_incomplete": 0,
"allow_multiple": 1,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"anonymous": 0,
"apply_document_permissions": 0,
"button_label": "Submit",
"condition_json": "[]",
"creation": "2025-10-09 18:57:22.187906",
"currency": "INR",
"doc_type": "Talk Proposal",
"docstatus": 0,
"doctype": "Web Form",
"hide_footer": 1,
"hide_navbar": 1,
"idx": 0,
"introduction_text": "<div class=\"ql-editor read-mode\"><p>Apply for giving a talk at the event.</p></div>",
"is_standard": 1,
"list_columns": [],
"login_required": 1,
"max_attachment_size": 0,
"modified": "2025-10-28 17:42:31.607629",
"modified_by": "Administrator",
"module": "Pohodex Event Manager",
"name": "propose-a-talk",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
"route": "propose-a-talk",
"show_attachments": 0,
"show_list": 1,
"show_sidebar": 0,
"title": "Propose a Talk",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 0,
"label": "Title of your talk",
"max_length": 0,
"max_value": 0,
"precision": "",
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "column_break_esac",
"fieldtype": "Column Break",
"hidden": 0,
"max_length": 0,
"max_value": 0,
"precision": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 1,
"fieldname": "event",
"fieldtype": "Link",
"hidden": 0,
"label": "Event",
"max_length": 0,
"max_value": 0,
"options": "Pohodex Event Manager Event",
"precision": "",
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "section_break_yqfb",
"fieldtype": "Section Break",
"hidden": 0,
"max_length": 0,
"max_value": 0,
"precision": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "description",
"fieldtype": "Text Editor",
"hidden": 0,
"label": "Description",
"max_length": 0,
"max_value": 0,
"precision": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "speakers",
"fieldtype": "Table",
"hidden": 0,
"label": "Speakers",
"max_length": 0,
"max_value": 0,
"options": "Proposal Speaker",
"precision": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "phone",
"fieldtype": "Phone",
"hidden": 0,
"label": "Phone Number",
"max_length": 0,
"max_value": 0,
"precision": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
}
]
}
@@ -0,0 +1,6 @@
import frappe
def get_context(context):
# do your magic here
pass
@@ -0,0 +1,29 @@
{
"app": "Pohodex Event Manager",
"charts": [],
"content": "[]",
"creation": "2026-03-05 19:10:56.484378",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"hide_custom": 0,
"idx": 0,
"indicator_color": "green",
"is_hidden": 0,
"label": "Pohodex Event Manager",
"link_type": "DocType",
"links": [],
"modified": "2026-03-05 19:24:18.378896",
"modified_by": "Administrator",
"module": "Pohodex Event Manager",
"name": "Pohodex Event Manager",
"number_cards": [],
"owner": "Administrator",
"public": 1,
"quick_lists": [],
"roles": [],
"sequence_id": 0.0,
"shortcuts": [],
"title": "Pohodex Event Manager",
"type": "Workspace"
}
@@ -0,0 +1,8 @@
// Copyright (c) 2026, BWH Studios and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Pohodex Event Manager Campaign", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,97 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "prompt",
"creation": "2026-01-23 14:27:04.947771",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enabled",
"title",
"event",
"qr_code",
"column_break_onhk",
"description"
],
"fields": [
{
"description": "Will be shown to the user",
"fieldname": "description",
"fieldtype": "Markdown Editor",
"in_list_view": 1,
"label": "Description",
"reqd": 1
},
{
"fieldname": "event",
"fieldtype": "Link",
"label": "Event",
"options": "Pohodex Event Manager Event"
},
{
"fieldname": "column_break_onhk",
"fieldtype": "Column Break"
},
{
"fieldname": "qr_code",
"fieldtype": "Attach Image",
"label": "QR Code",
"read_only": 1
},
{
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled?"
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
}
],
"grid_page_length": 50,
"image_field": "qr_code",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-01-23 17:23:01.011762",
"modified_by": "Administrator",
"module": "Pohodex Event Manager Marketing",
"name": "Pohodex Event Manager Campaign",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Event Manager",
"select": 1,
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,50 @@
# Copyright (c) 2026, BWH Studios and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from event_manager.utils import generate_qr_code_file, is_app_installed
class BuzzCampaign(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
description: DF.MarkdownEditor
enabled: DF.Check
event: DF.Link | None
qr_code: DF.AttachImage | None
title: DF.Data
# end: auto-generated types
def before_save(self):
if not self.enabled:
return
previous = self.get_doc_before_save()
name_changed = previous and previous.name != self.name
if not self.qr_code or name_changed:
self.generate_qr_code()
def validate(self):
self.validate_crm_installed()
def validate_crm_installed(self):
if self.enabled and not is_app_installed("crm"):
frappe.throw(_("Please install Frappe CRM to use campaigns feature"))
def generate_qr_code(self):
register_url = f"{frappe.utils.get_url()}/dashboard/register-interest/{self.name}"
self.qr_code = generate_qr_code_file(
doc=self,
data=register_url,
field_name="qr_code",
file_prefix="campaign-qr-code",
)
@@ -0,0 +1,20 @@
# Copyright (c) 2026, BWH Studios and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestBuzzCampaign(IntegrationTestCase):
"""
Integration tests for BuzzCampaign.
Use this class for testing interactions between multiple components.
"""
pass
View File
+21
View File
@@ -0,0 +1,21 @@
{
"app": "event_manager",
"bg_color": "blue",
"creation": "2026-03-05 19:10:56.544723",
"docstatus": 0,
"doctype": "Desktop Icon",
"hidden": 0,
"icon_type": "Link",
"idx": 0,
"label": "Pohodex Event Manager",
"link_to": "Pohodex Event Manager",
"link_type": "Workspace Sidebar",
"modified": "2026-03-05 19:32:36.529508",
"modified_by": "Administrator",
"name": "Pohodex Event Manager",
"owner": "Administrator",
"restrict_removal": 0,
"roles": [],
"sidebar": "Pohodex Event Manager",
"standard": 1
}
View File
View File
@@ -0,0 +1,11 @@
// Copyright (c) 2025, BWH Studios and contributors
// For license information, please see license.txt
frappe.ui.form.on("Additional Event Page", {
refresh(frm) {
if (frm.doc.is_published) {
frappe.db.get_value("Pohodex Event Manager Event", frm.doc.event, "route").then(({ message }) => {
frm.add_web_link(`/events/${message.route}/${frm.doc.route}`);
});
}
},
});
@@ -0,0 +1,85 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-09-25 18:54:07.782051",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"title",
"route",
"column_break_gagp",
"event",
"is_published",
"section_break_pifr",
"content"
],
"fields": [
{
"fieldname": "event",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Event",
"options": "Pohodex Event Manager Event",
"reqd": 1
},
{
"fieldname": "content",
"fieldtype": "Text Editor",
"label": "Content",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"reqd": 1
},
{
"fieldname": "route",
"fieldtype": "Data",
"label": "Route"
},
{
"fieldname": "column_break_gagp",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_pifr",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "is_published",
"fieldtype": "Check",
"label": "Is Published?"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-12-01 16:45:14.211143",
"modified_by": "Administrator",
"module": "Events",
"name": "Additional Event Page",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1
}
@@ -0,0 +1,42 @@
# Copyright (c) 2025, BWH Studios and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class AdditionalEventPage(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
content: DF.TextEditor
event: DF.Link
is_published: DF.Check
route: DF.Data | None
title: DF.Data
# end: auto-generated types
def validate(self):
self.validate_route()
self.validate_duplicate()
def validate_route(self):
if self.is_published and not self.route:
self.route = frappe.website.utils.cleanup_page_name(self.title).replace("_", "-")
def validate_duplicate(self):
if not self.route:
return
if frappe.db.exists(
"Additional Event Page",
{"route": self.route, "event": self.event, "name": ["!=", self.name]},
):
frappe.throw(
frappe._("An Additional Event Page with the same route already exists for this event.")
)
@@ -0,0 +1,20 @@
# Copyright (c) 2025, BWH Studios and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestAdditionalEventPage(IntegrationTestCase):
"""
Integration tests for AdditionalEventPage.
Use this class for testing interactions between multiple components.
"""
pass
@@ -0,0 +1,527 @@
// Copyright (c) 2025, BWH Studios and contributors
// For license information, please see license.txt
const FIELD_LABELS = {
category: __("Category"),
host: __("Host"),
banner_image: __("Banner Image"),
short_description: __("Short Description"),
about: __("About"),
medium: __("Medium"),
venue: __("Venue"),
allow_guest_booking: __("Allow Guest Booking"),
guest_verification_method: __("Guest Verification Method"),
time_zone: __("Time Zone"),
send_ticket_email: __("Send Ticket Email"),
apply_tax: __("Tax Settings"),
tax_label: __("Tax Label"),
tax_percentage: __("Tax Percentage"),
ticket_email_template: __("Ticket Email Template"),
ticket_print_format: __("Ticket Print Format"),
auto_send_pitch_deck: __("Auto Send Pitch Deck"),
sponsor_deck_email_template: __("Sponsor Deck Email Template"),
sponsor_deck_reply_to: __("Sponsor Deck Reply To"),
sponsor_deck_cc: __("Sponsor Deck CC"),
sponsor_deck_attachments: __("Sponsor Deck Attachments"),
payment_gateways: __("Payment Gateways"),
ticket_types: __("Ticket Types"),
add_ons: __("Add-ons"),
custom_fields: __("Custom Fields"),
};
function get_field_label(field) {
return FIELD_LABELS[field] || field;
}
function render_save_template_field_group(fields, doc) {
let html = "";
for (let field of fields) {
let value = doc[field];
let has_value = value !== null && value !== undefined && value !== "" && value !== 0;
if (Array.isArray(value)) {
has_value = value.length > 0;
}
let label = get_field_label(field);
html += `
<div class="col-md-6 mb-2">
<label class="d-flex align-items-center">
<input type="checkbox" class="template-option mr-2" data-option="${field}" ${
has_value ? "checked" : "disabled"
}>
${label}
${!has_value ? '<span class="text-muted ml-1">(' + __("Not set") + ")</span>" : ""}
</label>
</div>
`;
}
return html;
}
function render_save_template_options(dialog, frm) {
let html = "";
let doc = frm.doc;
let buttons_html = `
<div class="mb-3">
<button class="btn btn-default btn-xs select-all-btn">${__("Select All")}</button>
<button class="btn btn-default btn-xs unselect-all-btn">${__("Unselect All")}</button>
</div>
`;
dialog.get_field("select_buttons").$wrapper.html(buttons_html);
// Event Details
html += '<div class="template-section mt-3">';
html += `<h6 class="text-muted">${__("Event Details")}</h6>`;
html += '<div class="row">';
html += render_save_template_field_group(
[
"category",
"host",
"banner_image",
"short_description",
"about",
"medium",
"venue",
"allow_guest_booking",
"guest_verification_method",
"time_zone",
],
doc
);
html += "</div></div>";
// Ticketing Settings
html += '<div class="template-section mt-3">';
html += `<h6 class="text-muted">${__("Ticketing Settings")}</h6>`;
html += '<div class="row">';
html += render_save_template_field_group(
[
"send_ticket_email",
"apply_tax",
"tax_label",
"tax_percentage",
"ticket_email_template",
"ticket_print_format",
],
doc
);
html += "</div></div>";
// Sponsorship Settings
html += '<div class="template-section mt-3">';
html += `<h6 class="text-muted">${__("Sponsorship Settings")}</h6>`;
html += '<div class="row">';
html += render_save_template_field_group(
[
"auto_send_pitch_deck",
"sponsor_deck_email_template",
"sponsor_deck_reply_to",
"sponsor_deck_cc",
"sponsor_deck_attachments",
],
doc
);
html += "</div></div>";
// Related Documents
html += '<div class="template-section mt-4" id="related-docs-section">';
html += `<h6 class="text-muted">${__("Related Documents")}</h6>`;
html += '<div class="row">';
let pg_count = doc.payment_gateways ? doc.payment_gateways.length : 0;
html += `
<div class="col-md-6 mb-2">
<label class="d-flex align-items-center">
<input type="checkbox" class="template-option mr-2" data-option="payment_gateways" ${
pg_count > 0 ? "checked" : ""
} ${pg_count === 0 ? "disabled" : ""}>
${__("Payment Gateways")} ${
pg_count > 0
? `<span class="text-muted ml-1">(${pg_count})</span>`
: '<span class="text-muted ml-1">(' + __("None") + ")</span>"
}
</label>
</div>
`;
html += `
<div class="col-md-6 mb-2" id="ticket-types-option">
<span class="text-muted">${__("Loading...")}</span>
</div>
<div class="col-md-6 mb-2" id="add-ons-option">
<span class="text-muted">${__("Loading...")}</span>
</div>
<div class="col-md-6 mb-2" id="custom-fields-option">
<span class="text-muted">${__("Loading...")}</span>
</div>
`;
html += "</div></div>";
dialog.get_field("field_options").$wrapper.html(html);
let $wrapper = dialog.get_field("field_options").$wrapper;
const linked_doctypes = [
{
id: "ticket-types-option",
doctype: "Event Ticket Type",
option: "ticket_types",
label: __("Ticket Types"),
},
{
id: "add-ons-option",
doctype: "Ticket Add-on",
option: "add_ons",
label: __("Add-ons"),
},
{
id: "custom-fields-option",
doctype: "Pohodex Event Manager Custom Field",
option: "custom_fields",
label: __("Custom Fields"),
},
];
for (let item of linked_doctypes) {
frappe.call({
method: "frappe.client.get_count",
args: { doctype: item.doctype, filters: { event: doc.name } },
callback: function (r) {
let count = r.message || 0;
$wrapper.find(`#${item.id}`).html(`
<label class="d-flex align-items-center">
<input type="checkbox" class="template-option mr-2" data-option="${item.option}" ${
count > 0 ? "checked" : ""
} ${count === 0 ? "disabled" : ""}>
${item.label} ${
count > 0
? `<span class="text-muted ml-1">(${count})</span>`
: '<span class="text-muted ml-1">(' + __("None") + ")</span>"
}
</label>
`);
},
});
}
dialog
.get_field("select_buttons")
.$wrapper.find(".select-all-btn")
.on("click", function () {
dialog
.get_field("field_options")
.$wrapper.find(".template-option:not(:disabled)")
.prop("checked", true);
});
dialog
.get_field("select_buttons")
.$wrapper.find(".unselect-all-btn")
.on("click", function () {
dialog
.get_field("field_options")
.$wrapper.find(".template-option")
.prop("checked", false);
});
}
function show_save_as_template_dialog(frm) {
let dialog = new frappe.ui.Dialog({
title: __("Save Event as Template"),
fields: [
{
fieldtype: "Data",
fieldname: "template_name",
label: __("Template Name"),
reqd: 1,
default: frm.doc.title + " Template",
},
{
fieldtype: "Section Break",
label: __("Select What to Include"),
},
{
fieldtype: "HTML",
fieldname: "select_buttons",
},
{
fieldtype: "HTML",
fieldname: "field_options",
},
],
size: "large",
primary_action_label: __("Save Template"),
primary_action: function (values) {
let options = {};
dialog
.get_field("field_options")
.$wrapper.find(".template-option:checked")
.each(function () {
options[$(this).data("option")] = 1;
});
frappe.call({
method: "event_manager.events.doctype.event_template.event_template.create_template_from_event",
args: {
event_name: frm.doc.name,
template_name: values.template_name,
options: JSON.stringify(options),
},
freeze: true,
freeze_message: __("Creating Template..."),
callback: function (r) {
if (r.message) {
dialog.hide();
frappe.show_alert({
message: __("Template {0} created successfully", [r.message]),
indicator: "green",
});
frappe.set_route("Form", "Event Template", r.message);
}
},
});
},
});
render_save_template_options(dialog, frm);
dialog.show();
}
frappe.ui.form.on("Pohodex Event Manager Event Form", {
copy_to_clipboard(frm, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
const url = `${window.location.origin}/dashboard/events/${frm.doc.route}/forms/${row.route}`;
navigator.clipboard.writeText(url);
frappe.show_alert({ message: __("Link copied!"), indicator: "green" });
},
});
frappe.ui.form.on("Pohodex Event Manager Event", {
refresh(frm) {
frm.fields_dict.time_zone.set_data(getZoomSupportedTimezones());
if (frm.doc.route && frm.doc.is_published) {
frm.add_web_link(`/events/${frm.doc.route}`);
}
if (frm.doc.route) {
frm.add_web_link(`/dashboard/book-tickets/${frm.doc.route}`, "View Registration Page");
}
if (!frm.is_new()) {
frm.add_web_link(`/dashboard/check-in/${frm.doc.name}`, __("Open Check-in"));
}
const button_label = frm.doc.is_published ? __("Unpublish") : __("Publish");
frm.add_custom_button(button_label, () => {
frm.set_value("is_published", !frm.doc.is_published);
frm.save();
});
frm.set_query("track", "schedule", (doc, cdt, cdn) => {
return {
filters: {
event: doc.name,
},
};
});
frm.set_query("default_ticket_type", (doc) => {
return {
filters: {
event: doc.name,
is_published: 1,
},
};
});
// Save as Template button
if (!frm.is_new()) {
frm.add_custom_button(
__("Save as Template"),
function () {
show_save_as_template_dialog(frm);
},
__("Actions")
);
}
frm.trigger("add_zoom_custom_actions");
},
add_zoom_custom_actions(frm) {
const installed_apps = frappe.boot.app_data.map((app) => app.app_name);
if (!installed_apps.includes("zoom_integration") || frm.doc.category != "Webinars") {
return;
}
if (frm.doc.zoom_webinar) {
frm.add_custom_button(__("View Webinar on Zoom"), () => {
window.open(`https://zoom.us/webinar/${frm.doc.zoom_webinar}`, "_blank");
});
return;
}
const btn = frm.add_custom_button(__("Create Webinar on Zoom"), () => {
frm.call({
doc: frm.doc,
method: "create_webinar_on_zoom",
btn,
freeze: true,
}).then(({ message }) => {
frm.layout.tabs.find((t) => t.label == "Zoom Integration").set_active();
});
});
},
category(frm) {
if (!frm.is_new()) return;
if (frm.doc.category === "Webinars") {
frm.set_value("attach_email_ticket", 0);
} else {
frm.set_value("attach_email_ticket", 1);
}
},
});
function getZoomSupportedTimezones() {
return [
"Pacific/Midway",
"Pacific/Pago_Pago",
"Pacific/Honolulu",
"America/Anchorage",
"America/Vancouver",
"America/Los_Angeles",
"America/Tijuana",
"America/Edmonton",
"America/Denver",
"America/Phoenix",
"America/Mazatlan",
"America/Winnipeg",
"America/Regina",
"America/Chicago",
"America/Mexico_City",
"America/Guatemala",
"America/El_Salvador",
"America/Managua",
"America/Costa_Rica",
"America/Montreal",
"America/New_York",
"America/Indianapolis",
"America/Panama",
"America/Bogota",
"America/Lima",
"America/Halifax",
"America/Puerto_Rico",
"America/Caracas",
"America/Santiago",
"America/St_Johns",
"America/Montevideo",
"America/Araguaina",
"America/Argentina/Buenos_Aires",
"America/Godthab",
"America/Sao_Paulo",
"Atlantic/Azores",
"Canada/Atlantic",
"Atlantic/Cape_Verde",
"UTC",
"Etc/Greenwich",
"Europe/Belgrade",
"CET",
"Atlantic/Reykjavik",
"Europe/Dublin",
"Europe/London",
"Europe/Lisbon",
"Africa/Casablanca",
"Africa/Nouakchott",
"Europe/Oslo",
"Europe/Copenhagen",
"Europe/Brussels",
"Europe/Berlin",
"Europe/Helsinki",
"Europe/Amsterdam",
"Europe/Rome",
"Europe/Stockholm",
"Europe/Vienna",
"Europe/Luxembourg",
"Europe/Paris",
"Europe/Zurich",
"Europe/Madrid",
"Africa/Bangui",
"Africa/Algiers",
"Africa/Tunis",
"Africa/Harare",
"Africa/Nairobi",
"Europe/Warsaw",
"Europe/Prague",
"Europe/Budapest",
"Europe/Sofia",
"Europe/Istanbul",
"Europe/Athens",
"Europe/Bucharest",
"Asia/Nicosia",
"Asia/Beirut",
"Asia/Damascus",
"Asia/Jerusalem",
"Asia/Amman",
"Africa/Tripoli",
"Africa/Cairo",
"Africa/Johannesburg",
"Europe/Moscow",
"Asia/Baghdad",
"Asia/Kuwait",
"Asia/Riyadh",
"Asia/Bahrain",
"Asia/Qatar",
"Asia/Aden",
"Asia/Tehran",
"Africa/Khartoum",
"Africa/Djibouti",
"Africa/Mogadishu",
"Asia/Dubai",
"Asia/Muscat",
"Asia/Baku",
"Asia/Kabul",
"Asia/Yekaterinburg",
"Asia/Tashkent",
"Asia/Calcutta",
"Asia/Kathmandu",
"Asia/Novosibirsk",
"Asia/Almaty",
"Asia/Dacca",
"Asia/Krasnoyarsk",
"Asia/Dhaka",
"Asia/Bangkok",
"Asia/Saigon",
"Asia/Jakarta",
"Asia/Irkutsk",
"Asia/Shanghai",
"Asia/Hong_Kong",
"Asia/Taipei",
"Asia/Kuala_Lumpur",
"Asia/Singapore",
"Australia/Perth",
"Asia/Yakutsk",
"Asia/Seoul",
"Asia/Tokyo",
"Australia/Darwin",
"Australia/Adelaide",
"Asia/Vladivostok",
"Pacific/Port_Moresby",
"Australia/Brisbane",
"Australia/Sydney",
"Australia/Hobart",
"Asia/Magadan",
"SST",
"Pacific/Noumea",
"Asia/Kamchatka",
"Pacific/Fiji",
"Pacific/Auckland",
"Asia/Kolkata",
"Europe/Kiev",
"America/Tegucigalpa",
"Pacific/Apia",
];
}
@@ -0,0 +1,637 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "autoincrement",
"creation": "2025-07-19 11:28:39.634220",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"title",
"category",
"free_webinar",
"medium",
"column_break_cxqh",
"banner_image",
"host",
"venue",
"section_break_naqe",
"start_date",
"start_time",
"time_zone",
"column_break_cjby",
"end_date",
"end_time",
"section_break_zukm",
"short_description",
"about",
"tab_2_tab",
"schedule",
"website_tab",
"is_published",
"route",
"default_ticket_type",
"column_break_ndtl",
"external_registration_page",
"registration_url",
"meta_image",
"card_image",
"section_break_rmtj",
"allow_guest_booking",
"column_break_lzcw",
"guest_verification_method",
"auto_closures_section",
"registrations_close_at",
"section_break_kwlt",
"featured_speakers",
"payments_tab",
"section_break_owtc",
"payment_gateways",
"tax_settings_section",
"apply_tax",
"tax_inclusive",
"column_break_gehk",
"tax_label",
"tax_percentage",
"sponsorships_tab",
"show_sponsorship_section",
"automations_section",
"auto_send_pitch_deck",
"section_break_edar",
"sponsor_deck_email_template",
"sponsor_deck_reply_to",
"column_break_awpd",
"sponsor_deck_cc",
"section_break_mieu",
"sponsor_deck_attachments",
"customisations_tab",
"send_ticket_email",
"section_break_uoke",
"attach_calendar_invite",
"ticket_email_template",
"column_break_ukql",
"attach_email_ticket",
"ticket_print_format",
"talks_section",
"allow_editing_talks_after_acceptance",
"custom_forms_tab",
"forms_section",
"custom_forms",
"connections_tab",
"proposal"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "column_break_cxqh",
"fieldtype": "Column Break"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date",
"reqd": 1
},
{
"fieldname": "category",
"fieldtype": "Link",
"label": "Category",
"options": "Event Category",
"reqd": 1
},
{
"depends_on": "eval:doc.medium!=\"Online\"",
"fieldname": "venue",
"fieldtype": "Link",
"label": "Venue",
"mandatory_depends_on": "eval:doc.medium!=\"Online\"",
"options": "Event Venue"
},
{
"default": "In Person",
"fieldname": "medium",
"fieldtype": "Select",
"label": "Medium",
"options": "In Person\nOnline"
},
{
"fieldname": "section_break_naqe",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_cjby",
"fieldtype": "Column Break"
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date"
},
{
"fieldname": "section_break_zukm",
"fieldtype": "Section Break"
},
{
"fieldname": "short_description",
"fieldtype": "Small Text",
"label": "Short Description"
},
{
"description": "Description of the event",
"fieldname": "about",
"fieldtype": "Text Editor",
"label": "About"
},
{
"fieldname": "start_time",
"fieldtype": "Time",
"label": "Start Time",
"reqd": 1
},
{
"fieldname": "end_time",
"fieldtype": "Time",
"label": "End Time",
"reqd": 1
},
{
"fieldname": "host",
"fieldtype": "Link",
"label": "Host",
"options": "Event Host",
"reqd": 1
},
{
"fieldname": "time_zone",
"fieldtype": "Autocomplete",
"label": "Time Zone"
},
{
"fieldname": "banner_image",
"fieldtype": "Attach Image",
"label": "Banner Image"
},
{
"default": "0",
"fieldname": "is_published",
"fieldtype": "Check",
"label": "Is Published?"
},
{
"fieldname": "tab_2_tab",
"fieldtype": "Tab Break",
"label": "Schedule"
},
{
"fieldname": "schedule",
"fieldtype": "Table",
"label": "Schedule",
"options": "Schedule Item"
},
{
"fieldname": "payments_tab",
"fieldtype": "Tab Break",
"label": "Payments"
},
{
"description": "Used by Frappe Builder",
"fieldname": "route",
"fieldtype": "Data",
"label": "Route",
"no_copy": 1,
"unique": 1
},
{
"fieldname": "meta_image",
"fieldtype": "Attach Image",
"label": "Meta Image"
},
{
"default": "0",
"fieldname": "external_registration_page",
"fieldtype": "Check",
"label": "External Registration Page?"
},
{
"depends_on": "eval:doc.external_registration_page==1",
"fieldname": "registration_url",
"fieldtype": "Data",
"label": "Registration URL",
"mandatory_depends_on": "eval:doc.external_registration_page==1"
},
{
"fieldname": "website_tab",
"fieldtype": "Tab Break",
"label": "Website"
},
{
"fieldname": "column_break_ndtl",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.send_ticket_email && doc.attach_email_ticket;",
"fieldname": "ticket_print_format",
"fieldtype": "Link",
"label": "Ticket Print Format",
"link_filters": "[[\"Print Format\",\"doc_type\",\"=\",\"Event Ticket\"]]",
"options": "Print Format"
},
{
"fieldname": "forms_section",
"fieldtype": "Section Break"
},
{
"fieldname": "custom_forms",
"fieldtype": "Table",
"label": "Custom Forms",
"options": "Pohodex Event Manager Event Form"
},
{
"fieldname": "talks_section",
"fieldtype": "Section Break",
"label": "Talk Settings"
},
{
"default": "0",
"description": "When enabled, speakers can edit their talk title and description after acceptance",
"fieldname": "allow_editing_talks_after_acceptance",
"fieldtype": "Check",
"label": "Allow Editing Talks After Acceptance"
},
{
"depends_on": "eval:doc.send_ticket_email;",
"fieldname": "ticket_email_template",
"fieldtype": "Link",
"label": "Ticket Email Template",
"options": "Email Template"
},
{
"fieldname": "customisations_tab",
"fieldtype": "Tab Break",
"label": "Customisations"
},
{
"fieldname": "column_break_ukql",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_kwlt",
"fieldtype": "Section Break"
},
{
"fieldname": "featured_speakers",
"fieldtype": "Table",
"label": "Featured Speakers",
"options": "Event Featured Speaker"
},
{
"fieldname": "sponsorships_tab",
"fieldtype": "Tab Break",
"label": "Sponsorships"
},
{
"default": "1",
"description": "Show or hide the sponsorship section on the event website",
"fieldname": "show_sponsorship_section",
"fieldtype": "Check",
"label": "Show Sponsorship Section on Website"
},
{
"fieldname": "automations_section",
"fieldtype": "Section Break",
"label": "Automations"
},
{
"default": "1",
"fieldname": "auto_send_pitch_deck",
"fieldtype": "Check",
"label": "Auto Send Pitch Deck?"
},
{
"fieldname": "section_break_edar",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_awpd",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_mieu",
"fieldtype": "Section Break"
},
{
"depends_on": "eval:doc.auto_send_pitch_deck==true",
"description": "Default template will be used if not set",
"fieldname": "sponsor_deck_email_template",
"fieldtype": "Link",
"label": "Email Template",
"options": "Email Template"
},
{
"depends_on": "eval:doc.auto_send_pitch_deck==true",
"fieldname": "sponsor_deck_cc",
"fieldtype": "Small Text",
"label": "CC"
},
{
"depends_on": "eval:doc.auto_send_pitch_deck==true",
"fieldname": "sponsor_deck_reply_to",
"fieldtype": "Data",
"label": "Reply To"
},
{
"depends_on": "eval:doc.auto_send_pitch_deck==true",
"fieldname": "sponsor_deck_attachments",
"fieldtype": "Table",
"label": "Attachments",
"options": "Sponsorship Deck Item"
},
{
"description": "Will be selected by default in the booking form",
"fieldname": "default_ticket_type",
"fieldtype": "Link",
"label": "Default Ticket Type",
"options": "Event Ticket Type"
},
{
"default": "0",
"depends_on": "eval:doc.category==\"Webinars\"",
"fieldname": "free_webinar",
"fieldtype": "Check",
"label": "Free Webinar?"
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections"
},
{
"fieldname": "proposal",
"fieldtype": "Link",
"label": "Proposal",
"options": "Event Proposal",
"read_only": 1
},
{
"fieldname": "section_break_owtc",
"fieldtype": "Section Break"
},
{
"fieldname": "payment_gateways",
"fieldtype": "Table",
"label": "Payment Gateways",
"options": "Event Payment Gateway"
},
{
"fieldname": "tax_settings_section",
"fieldtype": "Section Break",
"label": "Tax Settings"
},
{
"default": "0",
"fieldname": "apply_tax",
"fieldtype": "Check",
"label": "Apply Tax on Bookings?"
},
{
"default": "0",
"depends_on": "eval:doc.apply_tax==1",
"description": "When enabled, ticket prices are treated as tax-inclusive. Tax is back-calculated from the price instead of added on top.",
"fieldname": "tax_inclusive",
"fieldtype": "Check",
"label": "Tax Inclusive"
},
{
"default": "GST",
"depends_on": "eval:doc.apply_tax==1",
"description": "Label displayed to customers (e.g., GST, VAT, Sales Tax)",
"fieldname": "tax_label",
"fieldtype": "Data",
"label": "Tax Label",
"mandatory_depends_on": "eval:doc.apply_tax==1"
},
{
"default": "18",
"depends_on": "eval:doc.apply_tax==1",
"description": "Tax rate to apply on bookings",
"fieldname": "tax_percentage",
"fieldtype": "Percent",
"label": "Tax Percentage",
"mandatory_depends_on": "eval:doc.apply_tax==1"
},
{
"fieldname": "card_image",
"fieldtype": "Attach Image",
"label": "Card Image"
},
{
"default": "1",
"depends_on": "eval:doc.send_ticket_email",
"fieldname": "attach_calendar_invite",
"fieldtype": "Check",
"label": "Attach Calendar Invite"
},
{
"default": "0",
"fieldname": "allow_guest_booking",
"fieldtype": "Check",
"label": "Allow Guest Booking"
},
{
"default": "None",
"depends_on": "eval:doc.allow_guest_booking",
"description": "How to verify guest identity before booking",
"fieldname": "guest_verification_method",
"fieldtype": "Select",
"label": "Guest Verification Method",
"options": "None\nEmail OTP\nPhone OTP"
},
{
"default": "1",
"depends_on": "eval:doc.send_ticket_email;",
"fieldname": "attach_email_ticket",
"fieldtype": "Check",
"label": "Attach Email Ticket"
},
{
"fieldname": "section_break_uoke",
"fieldtype": "Section Break"
},
{
"default": "1",
"fieldname": "send_ticket_email",
"fieldtype": "Check",
"label": "Send Ticket Email"
},
{
"fieldname": "section_break_rmtj",
"fieldtype": "Section Break",
"label": "Guest Booking"
},
{
"fieldname": "column_break_gehk",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_lzcw",
"fieldtype": "Column Break"
},
{
"fieldname": "auto_closures_section",
"fieldtype": "Section Break",
"label": "Auto Closures"
},
{
"fieldname": "registrations_close_at",
"fieldtype": "Datetime",
"label": "Registrations Close At"
},
{
"fieldname": "custom_forms_tab",
"fieldtype": "Tab Break",
"label": "Forms"
}
],
"grid_page_length": 50,
"image_field": "banner_image",
"index_web_pages_for_search": 1,
"links": [
{
"group": "Sponsorship",
"link_doctype": "Event Sponsor",
"link_fieldname": "event"
},
{
"group": "Sponsorship",
"link_doctype": "Sponsorship Tier",
"link_fieldname": "event"
},
{
"group": "Ticketing",
"link_doctype": "Event Ticket Type",
"link_fieldname": "event"
},
{
"group": "Ticketing",
"link_doctype": "Ticket Add-on",
"link_fieldname": "event"
},
{
"group": "Ticketing",
"link_doctype": "Event Booking",
"link_fieldname": "event"
},
{
"group": "Ticketing",
"link_doctype": "Event Ticket",
"link_fieldname": "event"
},
{
"group": "Proposals",
"link_doctype": "Sponsorship Enquiry",
"link_fieldname": "event"
},
{
"group": "Proposals",
"link_doctype": "Talk Proposal",
"link_fieldname": "event"
},
{
"group": "General",
"link_doctype": "Event Track",
"link_fieldname": "event"
},
{
"group": "General",
"link_doctype": "Event Check In",
"link_fieldname": "event"
},
{
"group": "Ticketing",
"link_doctype": "Pohodex Event Manager Coupon Code",
"link_fieldname": "event"
},
{
"group": "General",
"link_doctype": "Additional Event Page",
"link_fieldname": "event"
},
{
"group": "Ticketing",
"link_doctype": "Offline Payment Method",
"link_fieldname": "event"
},
{
"group": "Customisations",
"link_doctype": "Pohodex Event Manager Custom Field",
"link_fieldname": "event"
}
],
"modified": "2026-03-23 17:37:37.770911",
"modified_by": "Administrator",
"module": "Events",
"name": "Pohodex Event Manager Event",
"naming_rule": "Autoincrement",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Event Manager",
"select": 1,
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"select": 1,
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Pohodex Event Manager User",
"select": 1,
"share": 1
}
],
"row_format": "Dynamic",
"search_fields": "start_date",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1
}
@@ -0,0 +1,342 @@
# Copyright (c) 2025, BWH Studios and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import append_number_if_name_exists
from frappe.utils.data import get_time, time_diff_in_seconds
from event_manager.utils import only_if_app_installed
class BuzzEvent(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from event_manager.events.doctype.buzz_event_form.buzz_event_form import BuzzEventForm
from event_manager.events.doctype.event_featured_speaker.event_featured_speaker import EventFeaturedSpeaker
from event_manager.events.doctype.event_payment_gateway.event_payment_gateway import EventPaymentGateway
from event_manager.events.doctype.schedule_item.schedule_item import ScheduleItem
from event_manager.proposals.doctype.sponsorship_deck_item.sponsorship_deck_item import SponsorshipDeckItem
about: DF.TextEditor | None
allow_editing_talks_after_acceptance: DF.Check
allow_guest_booking: DF.Check
apply_tax: DF.Check
attach_calendar_invite: DF.Check
attach_email_ticket: DF.Check
auto_send_pitch_deck: DF.Check
banner_image: DF.AttachImage | None
card_image: DF.AttachImage | None
category: DF.Link
custom_forms: DF.Table[BuzzEventForm]
default_ticket_type: DF.Link | None
end_date: DF.Date | None
end_time: DF.Time
external_registration_page: DF.Check
featured_speakers: DF.Table[EventFeaturedSpeaker]
free_webinar: DF.Check
guest_verification_method: DF.Literal["None", "Email OTP", "Phone OTP"]
host: DF.Link
is_published: DF.Check
medium: DF.Literal["In Person", "Online"]
meta_image: DF.AttachImage | None
name: DF.Int | None
payment_gateways: DF.Table[EventPaymentGateway]
proposal: DF.Link | None
registration_url: DF.Data | None
registrations_close_at: DF.Datetime | None
route: DF.Data | None
schedule: DF.Table[ScheduleItem]
send_ticket_email: DF.Check
short_description: DF.SmallText | None
show_sponsorship_section: DF.Check
sponsor_deck_attachments: DF.Table[SponsorshipDeckItem]
sponsor_deck_cc: DF.SmallText | None
sponsor_deck_email_template: DF.Link | None
sponsor_deck_reply_to: DF.Data | None
start_date: DF.Date
start_time: DF.Time
tax_inclusive: DF.Check
tax_label: DF.Data | None
tax_percentage: DF.Percent
ticket_email_template: DF.Link | None
ticket_print_format: DF.Link | None
time_zone: DF.Autocomplete | None
title: DF.Data
venue: DF.Link | None
# end: auto-generated types
def validate(self):
self.validate_dates()
self.validate_schedule()
self.validate_route()
self.validate_tax_settings()
self.validate_guest_verification_config()
def validate_schedule(self):
end_date = self.end_date or self.start_date
for item in self.schedule:
if item.date < self.start_date or item.date > end_date:
frappe.throw(
frappe._("<b>Schedule</b> row #{0}: <b>Date</b> must be within event dates").format(
item.idx
)
)
if time_diff_in_seconds(item.end_time, item.start_time) <= 0:
frappe.throw(
frappe._(
"<b>Schedule</b> row #{0}: <b>End Time</b> must be after <b>Start Time</b>"
).format(item.idx)
)
if (
item.date == self.start_date
and self.start_time
and get_time(item.start_time) < get_time(self.start_time)
):
frappe.throw(
frappe._(
"<b>Schedule</b> row #{0}: <b>Start Time</b> cannot be before event start time"
).format(item.idx)
)
if item.date == end_date and self.end_time and get_time(item.end_time) > get_time(self.end_time):
frappe.throw(
frappe._(
"<b>Schedule</b> row #{0}: <b>End Time</b> cannot be after event end time"
).format(item.idx)
)
def validate_dates(self):
self.validate_from_to_dates("start_date", "end_date")
if (
(not self.end_date or self.start_date == self.end_date)
and self.start_time
and self.end_time
and time_diff_in_seconds(self.end_time, self.start_time) <= 0
):
frappe.throw(frappe._("<b>End Time</b> must be after <b>Start Time</b>"))
def validate_tax_settings(self):
"""Set default tax values when tax is enabled."""
if self.apply_tax:
if not self.tax_label:
self.tax_label = "GST"
if not self.tax_percentage:
self.tax_percentage = 18
def validate_route(self):
if self.is_published and not self.route:
route = frappe.website.utils.cleanup_page_name(self.title).replace("_", "-")
self.route = append_number_if_name_exists("Pohodex Event Manager Event", route, fieldname="route")
def validate_guest_verification_config(self):
"""Ensure email/SMS is configured when OTP verification is enabled."""
if frappe.in_test or not self.allow_guest_booking:
return
if self.guest_verification_method == "Email OTP":
has_email = frappe.db.exists("Email Account", {"default_outgoing": 1, "enable_outgoing": 1})
if not has_email:
frappe.throw(
frappe._(
"Please configure an outgoing Email Account before enabling Email OTP verification."
),
title=frappe._("Email Not Configured"),
)
@frappe.whitelist()
def after_insert(self):
self.create_default_records()
def create_default_records(self):
records = [
{"doctype": "Sponsorship Tier", "title": "Normal"},
{"doctype": "Event Ticket Type", "title": "Normal"},
]
for record in records:
frappe.get_doc({**record, "event": self.name}).insert(ignore_permissions=True)
default_forms = [
{"form_doctype": "Event Feedback", "route": "feedback"},
{"form_doctype": "Talk Proposal", "route": "propose-talk"},
{"form_doctype": "Sponsorship Enquiry", "route": "enquire-sponsorship"},
]
for form in default_forms:
self.append("custom_forms", form)
self.save(ignore_permissions=True)
@frappe.whitelist()
@only_if_app_installed("zoom_integration", raise_exception=True)
def create_webinar_on_zoom(self):
if not self.end_time:
frappe.throw(frappe._("End time is needed for Zoom Webinar creation"))
zoom_webinar = frappe.get_doc(
{
"doctype": "Zoom Webinar",
"title": self.title,
"date": self.start_date,
"start_time": self.start_time,
"duration": int(time_diff_in_seconds(self.end_time, self.start_time)),
"timezone": self.time_zone,
"template": frappe.get_cached_doc("Pohodex Event Manager Settings").default_webinar_template,
}
).insert()
self.db_set("zoom_webinar", zoom_webinar.name)
return zoom_webinar
def on_update(self):
self.update_zoom_webinar()
@only_if_app_installed("zoom_integration")
def update_zoom_webinar(self):
if not self.zoom_webinar:
return
if (
self.has_value_changed("start_date")
or self.has_value_changed("end_time")
or self.has_value_changed("start_time")
or self.has_value_changed("time_zone")
):
webinar = frappe.get_doc("Zoom Webinar", self.zoom_webinar)
webinar.update(
{
"date": self.start_date,
"start_time": self.start_time,
"duration": int(time_diff_in_seconds(self.end_time, self.start_time)),
"timezone": self.time_zone,
}
)
webinar.save()
@frappe.whitelist()
def create_from_template(template_name: str, options: str, additional_fields: str = "{}") -> str:
"""
Create a new Pohodex Event Manager Event from a template.
Args:
template_name: Name of the Event Template
options: JSON string of what to copy (e.g., {"category": 1, "ticket_types": 1, ...})
additional_fields: JSON string of additional field values for mandatory fields not in template
Returns:
New Pohodex Event Manager Event document name
"""
if not frappe.has_permission("Event Template", "read"):
frappe.throw(_("You don't have permission to use templates"))
if not frappe.has_permission("Pohodex Event Manager Event", "create"):
frappe.throw(_("You don't have permission to create events"))
template = frappe.get_doc("Event Template", template_name)
options = frappe.parse_json(options)
additional_fields = frappe.parse_json(additional_fields)
# Create new event with required fields
event = frappe.new_doc("Pohodex Event Manager Event")
event.title = f"New Event from {template.template_name}"
event.start_date = frappe.utils.today()
event.start_time = "09:00:00"
event.end_time = "18:00:00"
# Apply additional fields first (these are mandatory fields provided by user)
for field, value in additional_fields.items():
if value:
event.set(field, value)
# Field mapping for direct copy
field_map = {
"category": "category",
"host": "host",
"banner_image": "banner_image",
"short_description": "short_description",
"about": "about",
"medium": "medium",
"venue": "venue",
"allow_guest_booking": "allow_guest_booking",
"guest_verification_method": "guest_verification_method",
"time_zone": "time_zone",
"send_ticket_email": "send_ticket_email",
"ticket_email_template": "ticket_email_template",
"ticket_print_format": "ticket_print_format",
"apply_tax": "apply_tax",
"tax_inclusive": "tax_inclusive",
"tax_label": "tax_label",
"tax_percentage": "tax_percentage",
"auto_send_pitch_deck": "auto_send_pitch_deck",
"sponsor_deck_email_template": "sponsor_deck_email_template",
"sponsor_deck_reply_to": "sponsor_deck_reply_to",
"sponsor_deck_cc": "sponsor_deck_cc",
}
for option_key, field_name in field_map.items():
if options.get(option_key):
event.set(field_name, template.get(field_name))
# Copy child tables
if options.get("payment_gateways"):
for pg in template.payment_gateways:
event.append("payment_gateways", {"payment_gateway": pg.payment_gateway})
if options.get("sponsor_deck_attachments"):
for attachment in template.sponsor_deck_attachments:
event.append("sponsor_deck_attachments", {"file": attachment.file})
event.insert()
# Create linked documents (Ticket Types, Add-ons, Custom Fields)
if options.get("ticket_types"):
for tt in template.template_ticket_types:
ticket_type = frappe.new_doc("Event Ticket Type")
ticket_type.event = event.name
ticket_type.title = tt.title
ticket_type.price = tt.price
ticket_type.currency = tt.currency
ticket_type.is_published = tt.is_published
ticket_type.max_tickets_available = tt.max_tickets_available
ticket_type.auto_unpublish_after = tt.auto_unpublish_after
ticket_type.insert()
if options.get("add_ons"):
for addon in template.template_add_ons:
add_on = frappe.new_doc("Ticket Add-on")
add_on.event = event.name
add_on.title = addon.title
add_on.price = addon.price
add_on.currency = addon.currency
add_on.description = addon.description
add_on.user_selects_option = addon.user_selects_option
add_on.options = addon.options
add_on.enabled = addon.enabled
add_on.insert()
if options.get("custom_fields"):
for cf in template.template_custom_fields:
custom_field = frappe.new_doc("Pohodex Event Manager Custom Field")
custom_field.event = event.name
custom_field.label = cf.label
custom_field.fieldname = cf.fieldname
custom_field.fieldtype = cf.fieldtype
custom_field.options = cf.options
custom_field.applied_to = cf.applied_to
custom_field.enabled = cf.enabled
custom_field.mandatory = cf.mandatory
custom_field.placeholder = cf.placeholder
custom_field.default_value = cf.default_value
custom_field.order = cf.order
custom_field.insert()
return event.name
@@ -0,0 +1,379 @@
// Field groups for template options
const TEMPLATE_FIELD_GROUPS = {
event_details: {
label: __("Event Details"),
fields: [
"category",
"host",
"banner_image",
"short_description",
"about",
"medium",
"venue",
"allow_guest_booking",
"guest_verification_method",
"time_zone",
],
},
ticketing_settings: {
label: __("Ticketing Settings"),
fields: [
"send_ticket_email",
"apply_tax",
"tax_label",
"tax_percentage",
"ticket_email_template",
"ticket_print_format",
],
},
sponsorship_settings: {
label: __("Sponsorship Settings"),
fields: [
"auto_send_pitch_deck",
"sponsor_deck_email_template",
"sponsor_deck_reply_to",
"sponsor_deck_cc",
"sponsor_deck_attachments",
],
},
};
const MANDATORY_FIELDS = ["category", "host"];
const FIELD_LABELS = {
category: __("Category"),
host: __("Host"),
banner_image: __("Banner Image"),
short_description: __("Short Description"),
about: __("About"),
medium: __("Medium"),
venue: __("Venue"),
allow_guest_booking: __("Allow Guest Booking"),
guest_verification_method: __("Guest Verification Method"),
time_zone: __("Time Zone"),
send_ticket_email: __("Send Ticket Email"),
apply_tax: __("Tax Settings"),
tax_label: __("Tax Label"),
tax_percentage: __("Tax Percentage"),
ticket_email_template: __("Ticket Email Template"),
ticket_print_format: __("Ticket Print Format"),
auto_send_pitch_deck: __("Auto Send Pitch Deck"),
sponsor_deck_email_template: __("Sponsor Deck Email Template"),
sponsor_deck_reply_to: __("Sponsor Deck Reply To"),
sponsor_deck_cc: __("Sponsor Deck CC"),
sponsor_deck_attachments: __("Sponsor Deck Attachments"),
payment_gateways: __("Payment Gateways"),
ticket_types: __("Ticket Types"),
add_ons: __("Add-ons"),
custom_fields: __("Custom Fields"),
};
function get_field_label(field) {
return FIELD_LABELS[field] || field;
}
function render_field_group(group_key, template) {
let group = TEMPLATE_FIELD_GROUPS[group_key];
let html = '<div class="template-section mt-3">';
html += `<h6 class="text-muted">${group.label}</h6>`;
html += '<div class="row">';
for (let field of group.fields) {
let value = template[field];
let has_value = value !== null && value !== undefined && value !== "" && value !== 0;
if (Array.isArray(value)) {
has_value = value.length > 0;
}
let label = get_field_label(field);
html += `
<div class="col-md-6 mb-2">
<label class="d-flex align-items-center">
<input type="checkbox" class="template-option mr-2" data-option="${field}" ${
has_value ? "checked" : "disabled"
}>
${label}
${!has_value ? '<span class="text-muted ml-1">(' + __("Not set") + ")</span>" : ""}
</label>
</div>
`;
}
html += "</div></div>";
return html;
}
function render_related_documents(template) {
let html = '<div class="template-section mt-4">';
html += `<h6 class="text-muted">${__("Related Documents")}</h6>`;
html += '<div class="row">';
const related_items = [
{ key: "payment_gateways", label: __("Payment Gateways"), data_key: "payment_gateways" },
{
key: "ticket_types",
label: __("Ticket Types"),
data_key: "template_ticket_types",
},
{ key: "add_ons", label: __("Add-ons"), data_key: "template_add_ons" },
{
key: "custom_fields",
label: __("Custom Fields"),
data_key: "template_custom_fields",
},
];
for (let item of related_items) {
let count = template[item.data_key] ? template[item.data_key].length : 0;
html += `
<div class="col-md-6 mb-2">
<label class="d-flex align-items-center">
<input type="checkbox" class="template-option mr-2" data-option="${item.key}" ${
count > 0 ? "checked" : ""
} ${count === 0 ? "disabled" : ""}>
${item.label} ${
count > 0
? `<span class="text-muted ml-1">(${count})</span>`
: '<span class="text-muted ml-1">(' + __("None") + ")</span>"
}
</label>
</div>
`;
}
html += "</div></div>";
return html;
}
function update_mandatory_fields_visibility(dialog, template) {
let missing_fields = [];
for (let field of MANDATORY_FIELDS) {
let template_has_value = template[field] && template[field] !== "";
let checkbox = dialog
.get_field("field_options")
.$wrapper.find(`.template-option[data-option="${field}"]`);
let is_checked = checkbox.is(":checked");
if (!template_has_value || !is_checked) {
missing_fields.push(field);
dialog.get_field(field).df.hidden = 0;
dialog.get_field(field).df.reqd = 1;
dialog.get_field(field).refresh();
} else {
dialog.get_field(field).df.hidden = 1;
dialog.get_field(field).df.reqd = 0;
dialog.get_field(field).refresh();
}
}
if (missing_fields.length > 0) {
dialog.get_field("missing_fields_section").df.hidden = 0;
dialog.get_field("missing_fields_section").refresh();
dialog
.get_field("missing_fields_info")
.$wrapper.html(
`<p class="text-muted small">${__(
"The following required fields are not set in the template or not selected. Please fill them in:"
)}</p>`
);
} else {
dialog.get_field("missing_fields_section").df.hidden = 1;
dialog.get_field("missing_fields_section").refresh();
dialog.get_field("missing_fields_info").$wrapper.html("");
}
}
function bind_select_buttons(dialog) {
dialog
.get_field("select_buttons")
.$wrapper.find(".select-all-btn")
.on("click", function () {
dialog
.get_field("field_options")
.$wrapper.find(".template-option:not(:disabled)")
.prop("checked", true);
update_mandatory_fields_visibility(dialog, dialog.template_data);
});
dialog
.get_field("select_buttons")
.$wrapper.find(".unselect-all-btn")
.on("click", function () {
dialog
.get_field("field_options")
.$wrapper.find(".template-option")
.prop("checked", false);
update_mandatory_fields_visibility(dialog, dialog.template_data);
});
}
function render_template_options(dialog, template) {
let buttons_html = `
<div class="mb-3">
<button class="btn btn-default btn-xs select-all-btn">${__("Select All")}</button>
<button class="btn btn-default btn-xs unselect-all-btn">${__("Unselect All")}</button>
</div>
`;
dialog.get_field("select_buttons").$wrapper.html(buttons_html);
let html = "";
html += render_field_group("event_details", template);
html += render_field_group("ticketing_settings", template);
html += render_field_group("sponsorship_settings", template);
html += render_related_documents(template);
dialog.get_field("field_options").$wrapper.html(html);
dialog.template_data = template;
update_mandatory_fields_visibility(dialog, template);
bind_select_buttons(dialog);
dialog.get_field("field_options").$wrapper.on("change", ".template-option", function () {
update_mandatory_fields_visibility(dialog, dialog.template_data);
});
}
function on_template_selected(dialog) {
let template_name = dialog.get_value("template");
if (!template_name) {
dialog.get_field("field_options").$wrapper.html("");
dialog.get_field("select_buttons").$wrapper.html("");
return;
}
frappe.call({
method: "frappe.client.get",
args: {
doctype: "Event Template",
name: template_name,
},
callback: function (r) {
if (r.message) {
render_template_options(dialog, r.message);
}
},
});
}
function create_event_from_template(dialog, values) {
let template_name = values.template;
let options = {};
dialog
.get_field("field_options")
.$wrapper.find(".template-option:checked")
.each(function () {
options[$(this).data("option")] = 1;
});
let additional_fields = {};
for (let field of MANDATORY_FIELDS) {
let field_obj = dialog.get_field(field);
if (!field_obj.df.hidden && values[field]) {
additional_fields[field] = values[field];
}
}
frappe.call({
method: "event_manager.events.doctype.buzz_event.buzz_event.create_from_template",
args: {
template_name: template_name,
options: JSON.stringify(options),
additional_fields: JSON.stringify(additional_fields),
},
freeze: true,
freeze_message: __("Creating Event..."),
callback: function (r) {
if (r.message) {
dialog.hide();
frappe.show_alert({
message: __("Event created successfully"),
indicator: "green",
});
frappe.set_route("Form", "Pohodex Event Manager Event", r.message);
}
},
});
}
function show_create_from_template_dialog() {
let dialog = new frappe.ui.Dialog({
title: __("Create Event from Template"),
fields: [
{
fieldtype: "Link",
fieldname: "template",
label: __("Select Template"),
options: "Event Template",
reqd: 1,
change: function () {
on_template_selected(dialog);
},
},
{
fieldtype: "Section Break",
fieldname: "missing_fields_section",
label: __("Required Fields"),
depends_on: "eval:doc.template",
hidden: 1,
},
{
fieldtype: "HTML",
fieldname: "missing_fields_info",
},
{
fieldtype: "Link",
fieldname: "category",
label: __("Category"),
options: "Event Category",
hidden: 1,
},
{
fieldtype: "Column Break",
},
{
fieldtype: "Link",
fieldname: "host",
label: __("Host"),
options: "Event Host",
hidden: 1,
},
{
fieldtype: "Section Break",
fieldname: "options_section",
label: __("Select What to Copy"),
depends_on: "eval:doc.template",
},
{
fieldtype: "HTML",
fieldname: "select_buttons",
depends_on: "eval:doc.template",
},
{
fieldtype: "HTML",
fieldname: "field_options",
depends_on: "eval:doc.template",
},
],
size: "large",
primary_action_label: __("Create Event"),
primary_action: function (values) {
create_event_from_template(dialog, values);
},
});
dialog.show();
}
frappe.listview_settings["Pohodex Event Manager Event"] = {
onload: function (listview) {
if (frappe.perm.has_perm("Event Template", 0, "read")) {
listview.page.add_inner_button(__("Create from Template"), function () {
show_create_from_template_dialog();
});
}
},
};
@@ -0,0 +1,731 @@
# Copyright (c) 2025, BWH Studios and Contributors
# See license.txt
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
import frappe
from frappe.tests.utils import FrappeTestCase
from event_manager.api import are_registrations_closed
from event_manager.events.doctype.buzz_event.buzz_event import create_from_template
from event_manager.events.doctype.event_template.event_template import create_template_from_event
class TestBuzzEvent(FrappeTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.create_test_fixtures()
@classmethod
def create_test_fixtures(cls):
if not frappe.db.exists("Event Category", "Test Category"):
frappe.get_doc({"doctype": "Event Category", "category_name": "Test Category"}).insert(
ignore_permissions=True
)
if not frappe.db.exists("Event Host", "Test Host"):
frappe.get_doc({"doctype": "Event Host", "host_name": "Test Host"}).insert(
ignore_permissions=True
)
def tearDown(self):
frappe.db.rollback()
# ==================== Schedule Validation Tests ====================
def _make_event_with_schedule(self, schedule_overrides, **event_overrides):
"""Helper to create a Pohodex Event Manager Event with a single schedule item for validation tests."""
event_defaults = {
"doctype": "Pohodex Event Manager Event",
"title": "Schedule Test Event",
"category": "Test Category",
"host": "Test Host",
"start_date": "2026-03-05",
"end_date": "2026-03-06",
"start_time": "9:00:00",
"end_time": "18:00:00",
}
event_defaults.update(event_overrides)
event = frappe.get_doc(event_defaults)
# Directly call validate_schedule instead of insert to avoid
# needing linked Event Track records in the test database
for row in schedule_overrides:
event.append("schedule", row)
return event
def test_schedule_start_time_after_event_start_is_valid(self):
"""Schedule at 11:00 should be valid when event starts at 9:00 (regression: string comparison bug)"""
event = self._make_event_with_schedule(
[{"date": "2026-03-05", "start_time": "11:00:00", "end_time": "12:00:00"}]
)
# Should not raise
event.validate_schedule()
def test_schedule_start_time_before_event_start_is_rejected(self):
"""Schedule at 08:00 should be rejected when event starts at 9:00"""
event = self._make_event_with_schedule(
[{"date": "2026-03-05", "start_time": "08:00:00", "end_time": "08:30:00"}]
)
with self.assertRaises(frappe.exceptions.ValidationError):
event.validate_schedule()
def test_schedule_end_time_after_event_end_is_rejected(self):
"""Schedule ending at 19:00 should be rejected when event ends at 18:00"""
event = self._make_event_with_schedule(
[{"date": "2026-03-06", "start_time": "17:00:00", "end_time": "19:00:00"}]
)
with self.assertRaises(frappe.exceptions.ValidationError):
event.validate_schedule()
def test_schedule_end_time_before_event_end_is_valid(self):
"""Schedule ending at 16:30 should be valid when event ends at 18:00"""
event = self._make_event_with_schedule(
[{"date": "2026-03-06", "start_time": "16:00:00", "end_time": "16:30:00"}]
)
# Should not raise
event.validate_schedule()
# ==================== Create from Template Tests ====================
def test_create_from_template_copies_direct_fields(self):
"""Test that direct fields are copied from template to event"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Direct Fields Template",
"category": "Test Category",
"host": "Test Host",
"medium": "Online",
"about": "About text",
"short_description": "Short desc",
"time_zone": "Asia/Kolkata",
"allow_guest_booking": 1,
"guest_verification_method": "Email OTP",
"send_ticket_email": 1,
"apply_tax": 1,
"tax_label": "GST",
"tax_percentage": 18,
}
)
template.insert()
options = {
"category": 1,
"host": 1,
"medium": 1,
"about": 1,
"short_description": 1,
"time_zone": 1,
"allow_guest_booking": 1,
"guest_verification_method": 1,
"send_ticket_email": 1,
"apply_tax": 1,
"tax_label": 1,
"tax_percentage": 1,
}
event_name = create_from_template(template.name, frappe.as_json(options))
event = frappe.get_doc("Pohodex Event Manager Event", event_name)
self.assertEqual(event.category, "Test Category")
self.assertEqual(event.host, "Test Host")
self.assertEqual(event.medium, "Online")
self.assertEqual(event.about, "About text")
self.assertEqual(event.short_description, "Short desc")
self.assertEqual(event.time_zone, "Asia/Kolkata")
self.assertEqual(event.allow_guest_booking, 1)
self.assertEqual(event.guest_verification_method, "Email OTP")
self.assertEqual(event.send_ticket_email, 1)
self.assertEqual(event.apply_tax, 1)
self.assertEqual(event.tax_label, "GST")
self.assertEqual(event.tax_percentage, 18)
def test_create_from_template_respects_unselected_options(self):
"""Test that unselected options are not copied"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Selective Template",
"category": "Test Category",
"host": "Test Host",
"medium": "In Person",
"about": "Should not appear",
"apply_tax": 1,
"tax_percentage": 18,
}
)
template.insert()
options = {"category": 1, "host": 1, "medium": 0, "about": 0, "apply_tax": 0}
event_name = create_from_template(template.name, frappe.as_json(options))
event = frappe.get_doc("Pohodex Event Manager Event", event_name)
self.assertEqual(event.category, "Test Category")
self.assertEqual(event.host, "Test Host")
self.assertFalse(event.about)
self.assertFalse(event.apply_tax)
def test_create_from_template_additional_fields_override(self):
"""Test that additional_fields override template values"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Override Template",
"category": "Test Category",
"host": "Test Host",
}
)
template.insert()
# Don't copy category from template, provide via additional_fields
options = {"host": 1}
additional_fields = {"category": "Test Category"}
event_name = create_from_template(
template.name, frappe.as_json(options), frappe.as_json(additional_fields)
)
event = frappe.get_doc("Pohodex Event Manager Event", event_name)
self.assertEqual(event.category, "Test Category")
self.assertEqual(event.host, "Test Host")
def test_create_from_template_creates_ticket_types(self):
"""Test that ticket types are created as linked documents"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Ticket Template",
"category": "Test Category",
"host": "Test Host",
"template_ticket_types": [
{
"title": "Early Bird",
"price": 500,
"currency": "INR",
"is_published": 1,
"max_tickets_available": 100,
},
{"title": "Regular", "price": 1000, "currency": "INR", "is_published": 1},
],
}
)
template.insert()
options = {"category": 1, "host": 1, "ticket_types": 1}
event_name = create_from_template(template.name, frappe.as_json(options))
ticket_types = frappe.get_all(
"Event Ticket Type",
filters={"event": event_name, "title": ["in", ["Early Bird", "Regular"]]},
fields=["title", "price", "max_tickets_available"],
order_by="price",
)
self.assertEqual(len(ticket_types), 2)
self.assertEqual(ticket_types[0].title, "Early Bird")
self.assertEqual(ticket_types[0].price, 500)
self.assertEqual(ticket_types[0].max_tickets_available, 100)
self.assertEqual(ticket_types[1].title, "Regular")
self.assertEqual(ticket_types[1].price, 1000)
def test_create_from_template_creates_add_ons(self):
"""Test that add-ons are created as linked documents"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "AddOn Template",
"category": "Test Category",
"host": "Test Host",
"template_add_ons": [
{
"title": "Workshop Access",
"price": 2000,
"currency": "INR",
"enabled": 1,
"user_selects_option": 1,
"options": "Morning\nAfternoon",
}
],
}
)
template.insert()
options = {"category": 1, "host": 1, "add_ons": 1}
event_name = create_from_template(template.name, frappe.as_json(options))
add_ons = frappe.get_all(
"Ticket Add-on",
filters={"event": event_name},
fields=["title", "price", "user_selects_option", "options"],
)
self.assertEqual(len(add_ons), 1)
self.assertEqual(add_ons[0].title, "Workshop Access")
self.assertEqual(add_ons[0].price, 2000)
self.assertEqual(add_ons[0].user_selects_option, 1)
self.assertEqual(add_ons[0].options, "Morning\nAfternoon")
def test_create_from_template_creates_custom_fields(self):
"""Test that custom fields are created as linked documents"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "CustomField Template",
"category": "Test Category",
"host": "Test Host",
"template_custom_fields": [
{
"label": "Company",
"fieldname": "company",
"fieldtype": "Data",
"applied_to": "Booking",
"mandatory": 1,
"enabled": 1,
"placeholder": "Enter company name",
}
],
}
)
template.insert()
options = {"category": 1, "host": 1, "custom_fields": 1}
event_name = create_from_template(template.name, frappe.as_json(options))
custom_fields = frappe.get_all(
"Pohodex Event Manager Custom Field",
filters={"event": event_name},
fields=["label", "fieldtype", "mandatory", "placeholder"],
)
self.assertEqual(len(custom_fields), 1)
self.assertEqual(custom_fields[0].label, "Company")
self.assertEqual(custom_fields[0].fieldtype, "Data")
self.assertEqual(custom_fields[0].mandatory, 1)
self.assertEqual(custom_fields[0].placeholder, "Enter company name")
def test_create_from_template_skips_linked_docs_when_unselected(self):
"""Test that linked docs are not created when options are 0"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Skip Linked Template",
"category": "Test Category",
"host": "Test Host",
"template_ticket_types": [
{"title": "Skipped", "price": 100, "currency": "INR", "is_published": 1}
],
"template_add_ons": [
{"title": "Skipped Addon", "price": 50, "currency": "INR", "enabled": 1}
],
"template_custom_fields": [
{
"label": "Skipped Field",
"fieldname": "skipped",
"fieldtype": "Data",
"applied_to": "Booking",
"enabled": 1,
}
],
}
)
template.insert()
options = {"category": 1, "host": 1, "ticket_types": 0, "add_ons": 0, "custom_fields": 0}
event_name = create_from_template(template.name, frappe.as_json(options))
self.assertEqual(
len(frappe.get_all("Event Ticket Type", filters={"event": event_name, "title": "Skipped"})), 0
)
self.assertEqual(len(frappe.get_all("Ticket Add-on", filters={"event": event_name})), 0)
self.assertEqual(len(frappe.get_all("Pohodex Event Manager Custom Field", filters={"event": event_name})), 0)
def test_create_from_template_sets_default_title_and_date(self):
"""Test that event gets a default title and today's date"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Defaults Template",
"category": "Test Category",
"host": "Test Host",
}
)
template.insert()
options = {"category": 1, "host": 1}
event_name = create_from_template(template.name, frappe.as_json(options))
event = frappe.get_doc("Pohodex Event Manager Event", event_name)
self.assertIn("Defaults Template", event.title)
self.assertEqual(str(event.start_date), frappe.utils.today())
def test_create_from_template_copies_sponsorship_settings(self):
"""Test that sponsorship settings are copied"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Sponsor Template",
"category": "Test Category",
"host": "Test Host",
"auto_send_pitch_deck": 1,
"sponsor_deck_reply_to": "test@example.com",
"sponsor_deck_cc": "cc@example.com",
}
)
template.insert()
options = {
"category": 1,
"host": 1,
"auto_send_pitch_deck": 1,
"sponsor_deck_reply_to": 1,
"sponsor_deck_cc": 1,
}
event_name = create_from_template(template.name, frappe.as_json(options))
event = frappe.get_doc("Pohodex Event Manager Event", event_name)
self.assertEqual(event.auto_send_pitch_deck, 1)
self.assertEqual(event.sponsor_deck_reply_to, "test@example.com")
self.assertEqual(event.sponsor_deck_cc, "cc@example.com")
# ==================== Save as Template Tests ====================
def test_save_event_as_template_all_options(self):
"""Test saving an event as template with all field options"""
event = frappe.get_doc(
{
"doctype": "Pohodex Event Manager Event",
"title": "Full Save Event",
"category": "Test Category",
"host": "Test Host",
"start_date": frappe.utils.today(),
"start_time": "09:00:00",
"end_time": "18:00:00",
"medium": "Online",
"about": "Full event description",
"apply_tax": 1,
"tax_label": "GST",
"tax_percentage": 18,
}
)
event.insert()
# Create linked docs
frappe.get_doc(
{
"doctype": "Event Ticket Type",
"event": event.name,
"title": "Gold",
"price": 5000,
"currency": "INR",
"is_published": 1,
}
).insert()
frappe.get_doc(
{
"doctype": "Ticket Add-on",
"event": event.name,
"title": "Parking",
"price": 200,
"currency": "INR",
"enabled": 1,
}
).insert()
frappe.get_doc(
{
"doctype": "Pohodex Event Manager Custom Field",
"event": event.name,
"label": "Designation",
"fieldname": "designation",
"fieldtype": "Data",
"applied_to": "Booking",
"enabled": 1,
}
).insert()
options = {
"category": 1,
"host": 1,
"medium": 1,
"about": 1,
"apply_tax": 1,
"tax_label": 1,
"tax_percentage": 1,
"ticket_types": 1,
"add_ons": 1,
"custom_fields": 1,
}
template_name = create_template_from_event(
str(event.name), "Full Save Template", frappe.as_json(options)
)
template = frappe.get_doc("Event Template", template_name)
self.assertEqual(template.category, "Test Category")
self.assertEqual(template.host, "Test Host")
self.assertEqual(template.medium, "Online")
self.assertEqual(template.about, "Full event description")
self.assertEqual(template.apply_tax, 1)
self.assertEqual(template.tax_percentage, 18)
# Check linked docs (ticket types include default "Normal" created on event insert)
gold_tickets = [t for t in template.template_ticket_types if t.title == "Gold"]
self.assertEqual(len(gold_tickets), 1)
self.assertEqual(gold_tickets[0].price, 5000)
self.assertEqual(len(template.template_add_ons), 1)
self.assertEqual(template.template_add_ons[0].title, "Parking")
self.assertEqual(len(template.template_custom_fields), 1)
self.assertEqual(template.template_custom_fields[0].label, "Designation")
def test_save_event_as_template_partial(self):
"""Test saving event as template with partial options"""
event = frappe.get_doc(
{
"doctype": "Pohodex Event Manager Event",
"title": "Partial Save Event",
"category": "Test Category",
"host": "Test Host",
"start_date": frappe.utils.today(),
"start_time": "09:00:00",
"end_time": "18:00:00",
"medium": "In Person",
"about": "Included",
"apply_tax": 1,
"tax_percentage": 18,
}
)
event.insert()
options = {"category": 1, "about": 1, "medium": 0, "apply_tax": 0}
template_name = create_template_from_event(
str(event.name), "Partial Save Template", frappe.as_json(options)
)
template = frappe.get_doc("Event Template", template_name)
self.assertEqual(template.category, "Test Category")
self.assertEqual(template.about, "Included")
self.assertFalse(template.host)
self.assertFalse(template.apply_tax)
# ==================== Round Trip Test ====================
def test_round_trip_preserves_data(self):
"""Test Event -> Template -> Event preserves all data"""
original = frappe.get_doc(
{
"doctype": "Pohodex Event Manager Event",
"title": "Round Trip Event",
"category": "Test Category",
"host": "Test Host",
"start_date": frappe.utils.today(),
"start_time": "09:00:00",
"end_time": "18:00:00",
"medium": "Online",
"about": "Round trip description",
"apply_tax": 1,
"tax_label": "Service Tax",
"tax_percentage": 12,
}
)
original.insert()
frappe.get_doc(
{
"doctype": "Event Ticket Type",
"event": original.name,
"title": "Platinum",
"price": 10000,
"currency": "INR",
"is_published": 1,
"max_tickets_available": 25,
}
).insert()
# Event -> Template
all_options = {
"category": 1,
"host": 1,
"medium": 1,
"about": 1,
"apply_tax": 1,
"tax_label": 1,
"tax_percentage": 1,
"ticket_types": 1,
}
template_name = create_template_from_event(
str(original.name), "Round Trip Template", frappe.as_json(all_options)
)
# Template -> New Event
new_event_name = create_from_template(template_name, frappe.as_json(all_options))
new_event = frappe.get_doc("Pohodex Event Manager Event", new_event_name)
self.assertEqual(new_event.category, original.category)
self.assertEqual(new_event.host, original.host)
self.assertEqual(new_event.medium, original.medium)
self.assertEqual(new_event.about, original.about)
self.assertEqual(new_event.tax_label, original.tax_label)
self.assertEqual(new_event.tax_percentage, original.tax_percentage)
platinum_tickets = frappe.get_all(
"Event Ticket Type",
filters={"event": new_event_name, "title": "Platinum"},
fields=["price", "max_tickets_available"],
)
self.assertEqual(len(platinum_tickets), 1)
self.assertEqual(platinum_tickets[0].price, 10000)
self.assertEqual(platinum_tickets[0].max_tickets_available, 25)
# ==================== Permission Tests ====================
def test_create_from_template_requires_template_read_permission(self):
"""Test that creating from template requires read permission on Event Template"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Perm Test Template",
"category": "Test Category",
"host": "Test Host",
}
)
template.insert()
# Create a user without Event Template read permission
frappe.set_user("Guest")
try:
with self.assertRaises(frappe.exceptions.ValidationError):
create_from_template(template.name, frappe.as_json({"category": 1, "host": 1}))
finally:
frappe.set_user("Administrator")
def test_save_as_template_requires_create_permission(self):
"""Test that saving as template requires create permission on Event Template"""
event = frappe.get_doc(
{
"doctype": "Pohodex Event Manager Event",
"title": "Perm Event",
"category": "Test Category",
"host": "Test Host",
"start_date": frappe.utils.today(),
"start_time": "09:00:00",
"end_time": "18:00:00",
}
)
event.insert()
frappe.set_user("Guest")
try:
with self.assertRaises(frappe.exceptions.ValidationError):
create_template_from_event(str(event.name), "Perm Template", frappe.as_json({"category": 1}))
finally:
frappe.set_user("Administrator")
class TestRegistrationsClosed(FrappeTestCase):
"""Tests for the are_registrations_closed function with timezone handling."""
def _make_event(self, registrations_close_at=None, time_zone=None):
"""Create a minimal event _dict for testing (no DB insert needed)."""
return frappe._dict(
registrations_close_at=registrations_close_at,
time_zone=time_zone,
)
def test_no_close_at_returns_false(self):
"""When registrations_close_at is not set, registrations are open."""
event = self._make_event()
self.assertFalse(are_registrations_closed(event))
def test_future_close_at_returns_false(self):
"""When close_at is in the future, registrations are open."""
fake_now = datetime(2026, 6, 15, 10, 0, 0)
event = self._make_event(
registrations_close_at="2026-06-15 12:00:00", # 2 hours after fake_now
time_zone="UTC",
)
with patch("event_manager.api.get_datetime_in_timezone", return_value=fake_now):
self.assertFalse(are_registrations_closed(event))
def test_past_close_at_returns_true(self):
"""When close_at is in the past, registrations are closed."""
fake_now = datetime(2026, 6, 15, 14, 0, 0)
event = self._make_event(
registrations_close_at="2026-06-15 12:00:00", # 2 hours before fake_now
time_zone="UTC",
)
with patch("event_manager.api.get_datetime_in_timezone", return_value=fake_now):
self.assertTrue(are_registrations_closed(event))
def test_timezone_ahead_of_utc_closes_earlier(self):
"""An event in Asia/Kolkata (UTC+5:30) should close before the same wall-clock time in UTC.
If it's 14:00 UTC, that's 19:30 IST.
A close_at of 18:00 (naive, in event tz) is already past in IST but not in UTC.
"""
# Simulate 19:30 IST (= 14:00 UTC)
fake_ist_now = datetime(2026, 6, 15, 19, 30, 0, tzinfo=timezone(timedelta(hours=5, minutes=30)))
event = self._make_event(
registrations_close_at="2026-06-15 18:00:00", # 18:00 in event tz (IST)
time_zone="Asia/Kolkata",
)
with patch("event_manager.api.get_datetime_in_timezone", return_value=fake_ist_now):
# 19:30 IST > 18:00 IST → closed
self.assertTrue(are_registrations_closed(event))
def test_timezone_behind_utc_stays_open_longer(self):
"""An event in US/Pacific (UTC-7) should stay open longer than the same wall-clock in UTC.
If it's 23:00 UTC on June 15, that's 16:00 PDT on June 15.
A close_at of 18:00 (naive, in event tz) is still in the future in PDT.
"""
# Simulate 16:00 PDT (= 23:00 UTC)
fake_pdt_now = datetime(2026, 6, 15, 16, 0, 0, tzinfo=timezone(timedelta(hours=-7)))
event = self._make_event(
registrations_close_at="2026-06-15 18:00:00", # 18:00 in event tz (PDT)
time_zone="US/Pacific",
)
with patch("event_manager.api.get_datetime_in_timezone", return_value=fake_pdt_now):
# 16:00 PDT < 18:00 PDT → still open
self.assertFalse(are_registrations_closed(event))
def test_same_close_time_different_timezones(self):
"""Same UTC instant, same close_at string — different result depending on event timezone.
At 2026-06-15 17:30 UTC:
- Asia/Kolkata: 23:00 IST → 23:00 > 18:00 → closed
- US/Pacific: 10:30 PDT → 10:30 < 18:00 → open
"""
close_at = "2026-06-15 18:00:00"
event_ist = self._make_event(registrations_close_at=close_at, time_zone="Asia/Kolkata")
event_pdt = self._make_event(registrations_close_at=close_at, time_zone="US/Pacific")
# 17:30 UTC = 23:00 IST
fake_ist_now = datetime(2026, 6, 15, 23, 0, 0, tzinfo=timezone(timedelta(hours=5, minutes=30)))
with patch("event_manager.api.get_datetime_in_timezone", return_value=fake_ist_now):
self.assertTrue(are_registrations_closed(event_ist))
# 17:30 UTC = 10:30 PDT
fake_pdt_now = datetime(2026, 6, 15, 10, 30, 0, tzinfo=timezone(timedelta(hours=-7)))
with patch("event_manager.api.get_datetime_in_timezone", return_value=fake_pdt_now):
self.assertFalse(are_registrations_closed(event_pdt))
def test_falls_back_to_system_timezone_when_event_tz_not_set(self):
"""When event has no time_zone, system timezone is used."""
fake_now = datetime(2026, 6, 15, 14, 0, 0)
event = self._make_event(
registrations_close_at="2026-06-15 13:00:00", # 1 hour before fake_now
time_zone=None,
)
with patch("event_manager.api.get_datetime_in_timezone", return_value=fake_now):
self.assertTrue(are_registrations_closed(event))
@@ -0,0 +1,105 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2026-03-20 00:00:00",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"form_doctype",
"route",
"copy_to_clipboard",
"publish",
"column_break_main",
"auto_close_at",
"section_break_success",
"success_title",
"success_message",
"section_break_closed",
"closed_title",
"closed_message"
],
"fields": [
{
"fieldname": "form_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "DocType",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "route",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Route",
"reqd": 1
},
{
"default": "0",
"fieldname": "publish",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Publish"
},
{
"fieldname": "column_break_main",
"fieldtype": "Column Break"
},
{
"fieldname": "auto_close_at",
"fieldtype": "Datetime",
"label": "Auto Close At"
},
{
"fieldname": "section_break_success",
"fieldtype": "Section Break",
"label": "Success"
},
{
"fieldname": "success_title",
"fieldtype": "Data",
"label": "Success Title"
},
{
"fieldname": "success_message",
"fieldtype": "Markdown Editor",
"label": "Success Message"
},
{
"fieldname": "section_break_closed",
"fieldtype": "Section Break",
"label": "Closed"
},
{
"fieldname": "closed_title",
"fieldtype": "Data",
"label": "Closed Title"
},
{
"fieldname": "closed_message",
"fieldtype": "Small Text",
"label": "Closed Message"
},
{
"fieldname": "copy_to_clipboard",
"fieldtype": "Button",
"in_list_view": 1,
"label": "Copy link to clipboard"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-04-14 19:33:17.779162",
"modified_by": "Administrator",
"module": "Events",
"name": "Pohodex Event Manager Event Form",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,38 @@
import frappe
from frappe import _
from frappe.model.document import Document
class BuzzEventForm(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
auto_close_at: DF.Datetime | None
closed_message: DF.SmallText | None
closed_title: DF.Data | None
form_doctype: DF.Link
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
publish: DF.Check
route: DF.Data
success_message: DF.MarkdownEditor | None
success_title: DF.Data | None
# end: auto-generated types
def validate(self):
self.validate_unique_route()
def validate_unique_route(self):
for row in self.parentdoc.custom_forms:
if row.name != self.name and row.route == self.route:
frappe.throw(
_("Duplicate route '{0}' in custom forms. Each form must have a unique route.").format(
self.route
)
)
@@ -0,0 +1,8 @@
// Copyright (c) 2025, BWH Studios and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Pohodex Event Manager Settings", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,251 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-08-17 22:08:28.579289",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"general_section",
"support_email",
"ticketing_section",
"allow_transfer_ticket_before_event_start_days",
"column_break_ehsr",
"allow_add_ons_change_before_event_start_days",
"column_break_hagy",
"allow_ticket_cancellation_request_before_event_start_days",
"proposals_tab",
"event_proposals_section",
"accept_event_proposals",
"event_proposal_banner_title",
"column_break_lxjh",
"allow_guest_event_proposals",
"success_section",
"event_proposal_success_title",
"event_proposal_success_message",
"login_tab",
"login_banner_section",
"login_banner",
"communications_tab",
"ticketing_emails_section",
"default_ticket_email_template",
"sponsorship_emails_section",
"auto_send_pitch_deck",
"sponsor_email_settings_section",
"default_sponsor_deck_email_template",
"default_sponsor_deck_reply_to",
"column_break_sponsor",
"default_sponsor_deck_cc",
"section_break_vtep",
"custom_fields_go_after_this"
],
"fields": [
{
"fieldname": "ticketing_section",
"fieldtype": "Section Break",
"label": "Ticketing"
},
{
"default": "7",
"fieldname": "allow_transfer_ticket_before_event_start_days",
"fieldtype": "Int",
"label": "Allow Transfer Ticket Before Event Start (Days)",
"non_negative": 1
},
{
"default": "7",
"fieldname": "allow_add_ons_change_before_event_start_days",
"fieldtype": "Int",
"label": "Allow Add Ons Change Before Event Start (Days)",
"non_negative": 1
},
{
"default": "7",
"fieldname": "allow_ticket_cancellation_request_before_event_start_days",
"fieldtype": "Int",
"label": "Allow Ticket Cancellation Request Before Event Start (Days)",
"non_negative": 1
},
{
"fieldname": "column_break_ehsr",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_hagy",
"fieldtype": "Column Break"
},
{
"fieldname": "general_section",
"fieldtype": "Section Break",
"label": "General"
},
{
"description": "Will be linked in emails, etc.",
"fieldname": "support_email",
"fieldtype": "Data",
"label": "Support Email",
"options": "Email"
},
{
"fieldname": "proposals_tab",
"fieldtype": "Tab Break",
"label": "Proposals"
},
{
"fieldname": "event_proposals_section",
"fieldtype": "Section Break",
"label": "Event Proposals"
},
{
"default": "0",
"fieldname": "accept_event_proposals",
"fieldtype": "Check",
"label": "Accept Event Proposals"
},
{
"default": "0",
"depends_on": "eval:doc.accept_event_proposals",
"fieldname": "allow_guest_event_proposals",
"fieldtype": "Check",
"label": "Allow Guest Submission"
},
{
"depends_on": "eval:doc.accept_event_proposals",
"fieldname": "event_proposal_banner_title",
"fieldtype": "Data",
"label": "Banner Title"
},
{
"depends_on": "eval:doc.accept_event_proposals",
"fieldname": "event_proposal_success_title",
"fieldtype": "Data",
"label": "Success Title"
},
{
"depends_on": "eval:doc.accept_event_proposals",
"fieldname": "event_proposal_success_message",
"fieldtype": "Markdown Editor",
"label": "Success Message"
},
{
"fieldname": "login_tab",
"fieldtype": "Tab Break",
"label": "Login"
},
{
"fieldname": "login_banner_section",
"fieldtype": "Section Break",
"label": "Login Banner Config"
},
{
"description": "Promotional message shown in the login/signup modal. Supports Markdown.",
"fieldname": "login_banner",
"fieldtype": "Markdown Editor",
"label": "Login Banner"
},
{
"fieldname": "communications_tab",
"fieldtype": "Tab Break",
"label": "Communications"
},
{
"fieldname": "ticketing_emails_section",
"fieldtype": "Section Break",
"label": "Ticketing"
},
{
"description": "Default template for ticket confirmation emails. Can be overridden per event.",
"fieldname": "default_ticket_email_template",
"fieldtype": "Link",
"label": "Default Ticket Email Template",
"options": "Email Template"
},
{
"fieldname": "sponsorship_emails_section",
"fieldtype": "Section Break",
"label": "Sponsorships"
},
{
"default": "0",
"fieldname": "auto_send_pitch_deck",
"fieldtype": "Check",
"label": "Auto Send Pitch Deck"
},
{
"depends_on": "eval:doc.auto_send_pitch_deck",
"fieldname": "sponsor_email_settings_section",
"fieldtype": "Section Break"
},
{
"depends_on": "eval:doc.auto_send_pitch_deck",
"description": "Default template for sponsorship pitch deck emails. Can be overridden per event.",
"fieldname": "default_sponsor_deck_email_template",
"fieldtype": "Link",
"label": "Default Email Template",
"mandatory_depends_on": "eval:doc.auto_send_pitch_deck",
"options": "Email Template"
},
{
"depends_on": "eval:doc.auto_send_pitch_deck",
"fieldname": "default_sponsor_deck_reply_to",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Default Reply To",
"mandatory_depends_on": "eval:doc.auto_send_pitch_deck",
"options": "Email"
},
{
"fieldname": "column_break_sponsor",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.auto_send_pitch_deck",
"fieldname": "default_sponsor_deck_cc",
"fieldtype": "Small Text",
"label": "Default CC"
},
{
"fieldname": "section_break_vtep",
"fieldtype": "Section Break"
},
{
"fieldname": "custom_fields_go_after_this",
"fieldtype": "HTML",
"label": "Custom Fields Go After This"
},
{
"fieldname": "column_break_lxjh",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.accept_event_proposals",
"fieldname": "success_section",
"fieldtype": "Section Break",
"label": "Success"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-29 09:46:29.455686",
"modified_by": "Administrator",
"module": "Events",
"name": "Pohodex Event Manager Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,45 @@
# Copyright (c) 2025, BWH Studios and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class BuzzSettings(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
accept_event_proposals: DF.Check
allow_add_ons_change_before_event_start_days: DF.Int
allow_guest_event_proposals: DF.Check
allow_ticket_cancellation_request_before_event_start_days: DF.Int
allow_transfer_ticket_before_event_start_days: DF.Int
auto_send_pitch_deck: DF.Check
default_sponsor_deck_cc: DF.SmallText | None
default_sponsor_deck_email_template: DF.Link | None
default_sponsor_deck_reply_to: DF.Data | None
default_ticket_email_template: DF.Link | None
event_proposal_banner_title: DF.Data | None
event_proposal_success_message: DF.MarkdownEditor | None
event_proposal_success_title: DF.Data | None
login_banner: DF.MarkdownEditor | None
support_email: DF.Data | None
# end: auto-generated types
def validate(self):
"""Validate the settings."""
self.validate_transfer_days()
def validate_transfer_days(self):
"""Validate that transfer days is a reasonable value."""
if self.allow_transfer_ticket_before_event_start_days is not None:
if self.allow_transfer_ticket_before_event_start_days < 0:
frappe.throw(_("Allow Transfer Ticket Before Event Start Days cannot be negative."))
elif self.allow_transfer_ticket_before_event_start_days > 365:
frappe.throw(_("Allow Transfer Ticket Before Event Start Days cannot be more than 365 days."))
@@ -0,0 +1,20 @@
# Copyright (c) 2025, BWH Studios and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestBuzzSettings(IntegrationTestCase):
"""
Integration tests for BuzzSettings.
Use this class for testing interactions between multiple components.
"""
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2025, BWH Studios and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Event Category", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,103 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "prompt",
"creation": "2025-07-19 11:30:07.885513",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enabled",
"slug",
"description",
"column_break_mrmh",
"banner_image",
"meta_image",
"icon_svg"
],
"fields": [
{
"fieldname": "icon_svg",
"fieldtype": "Code",
"label": "Icon SVG",
"options": "HTML"
},
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
},
{
"fieldname": "banner_image",
"fieldtype": "Attach Image",
"label": "Banner Image"
},
{
"fieldname": "slug",
"fieldtype": "Data",
"label": "Slug",
"unique": 1
},
{
"fieldname": "column_break_mrmh",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "meta_image",
"fieldtype": "Attach Image",
"label": "Meta Image"
}
],
"grid_page_length": 50,
"image_field": "banner_image",
"index_web_pages_for_search": 1,
"links": [
{
"link_doctype": "Pohodex Event Manager Coupon Code",
"link_fieldname": "event_category"
}
],
"modified": "2026-05-08 12:38:57.139602",
"modified_by": "Administrator",
"module": "Events",
"name": "Event Category",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Event Manager",
"select": 1,
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,30 @@
# Copyright (c) 2025, BWH Studios and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class EventCategory(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
banner_image: DF.AttachImage | None
description: DF.SmallText | None
enabled: DF.Check
icon_svg: DF.Code | None
meta_image: DF.AttachImage | None
slug: DF.Data | None
# end: auto-generated types
def validate(self):
if not self.slug:
self.set_slug()
def set_slug(self):
self.slug = frappe.website.utils.cleanup_page_name(self.name).replace("_", "-")
@@ -0,0 +1,20 @@
# Copyright (c) 2025, BWH Studios and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestEventCategory(IntegrationTestCase):
"""
Integration tests for EventCategory.
Use this class for testing interactions between multiple components.
"""
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2025, BWH Studios and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Event Check In", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,110 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-07-29 20:00:04.578374",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"event",
"date",
"column_break_fxzb",
"ticket",
"section_break_tt1x",
"amended_from"
],
"fields": [
{
"fieldname": "section_break_tt1x",
"fieldtype": "Section Break"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Event Check In",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"fetch_from": "ticket.event",
"fieldname": "event",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Event",
"options": "Pohodex Event Manager Event",
"reqd": 1
},
{
"fieldname": "ticket",
"fieldtype": "Link",
"label": "Ticket",
"options": "Event Ticket",
"reqd": 1
},
{
"fieldname": "column_break_fxzb",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"label": "Date"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-10-18 16:51:30.061975",
"modified_by": "Administrator",
"module": "Events",
"name": "Event Check In",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Event Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Frontdesk Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,25 @@
# Copyright (c) 2025, BWH Studios and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class EventCheckIn(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
amended_from: DF.Link | None
date: DF.Date | None
event: DF.Link
ticket: DF.Link
# end: auto-generated types
def before_insert(self):
if not self.date:
self.date = frappe.utils.today()
@@ -0,0 +1,20 @@
# Copyright (c) 2025, BWH Studios and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestEventCheckIn(IntegrationTestCase):
"""
Integration tests for EventCheckIn.
Use this class for testing interactions between multiple components.
"""
pass
@@ -0,0 +1,35 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-09-25 19:04:14.697421",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"speaker"
],
"fields": [
{
"fieldname": "speaker",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Speaker",
"options": "Speaker Profile",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-09-25 19:04:45.486029",
"modified_by": "Administrator",
"module": "Events",
"name": "Event Featured Speaker",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,23 @@
# Copyright (c) 2025, BWH Studios and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class EventFeaturedSpeaker(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
speaker: DF.Link
# end: auto-generated types
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2025, BWH Studios and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Event Feedback", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,70 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-08-14 14:10:08.833301",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"event",
"how_can_we_improve",
"section_break_additional",
"additional_fields"
],
"fields": [
{
"fieldname": "event",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Event",
"options": "Pohodex Event Manager Event",
"reqd": 1
},
{
"fieldname": "how_can_we_improve",
"fieldtype": "Small Text",
"label": "How can we improve?"
},
{
"fieldname": "section_break_additional",
"fieldtype": "Section Break"
},
{
"fieldname": "additional_fields",
"fieldtype": "Table",
"label": "Additional Fields",
"options": "Additional Field"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-14 14:11:07.484890",
"modified_by": "Administrator",
"module": "Events",
"name": "Event Feedback",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"if_owner": 1,
"read": 1,
"role": "Pohodex Event Manager User"
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,21 @@
# Copyright (c) 2025, BWH Studios and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class EventFeedback(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
event: DF.Link
how_can_we_improve: DF.SmallText | None
# end: auto-generated types
pass
@@ -0,0 +1,20 @@
# Copyright (c) 2025, BWH Studios and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestEventFeedback(IntegrationTestCase):
"""
Integration tests for EventFeedback.
Use this class for testing interactions between multiple components.
"""
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2025, BWH Studios and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Event Host", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,105 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "prompt",
"creation": "2025-07-19 11:36:30.869780",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"logo",
"country",
"social_media_links",
"column_break_udkj",
"by_line",
"address",
"section_break_hgnb",
"about"
],
"fields": [
{
"allow_in_quick_entry": 1,
"fieldname": "logo",
"fieldtype": "Attach Image",
"label": "Logo"
},
{
"fieldname": "column_break_udkj",
"fieldtype": "Column Break"
},
{
"allow_in_quick_entry": 1,
"fieldname": "by_line",
"fieldtype": "Data",
"label": "By Line"
},
{
"fieldname": "section_break_hgnb",
"fieldtype": "Section Break"
},
{
"fieldname": "about",
"fieldtype": "Text Editor",
"label": "About"
},
{
"allow_in_quick_entry": 1,
"fieldname": "country",
"fieldtype": "Link",
"label": "Country",
"options": "Country"
},
{
"fieldname": "address",
"fieldtype": "Small Text",
"label": "Address"
},
{
"fieldname": "social_media_links",
"fieldtype": "Table",
"label": "Social Media Links",
"options": "Social Media Link"
}
],
"grid_page_length": 50,
"image_field": "logo",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-26 12:24:58.729143",
"modified_by": "Administrator",
"module": "Events",
"name": "Event Host",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Event Manager",
"select": 1,
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,27 @@
# Copyright (c) 2025, BWH Studios and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class EventHost(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from event_manager.events.doctype.social_media_link.social_media_link import SocialMediaLink
about: DF.TextEditor | None
address: DF.SmallText | None
by_line: DF.Data | None
country: DF.Link | None
logo: DF.AttachImage | None
social_media_links: DF.Table[SocialMediaLink]
# end: auto-generated types
pass
@@ -0,0 +1,20 @@
# Copyright (c) 2025, BWH Studios and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestEventHost(IntegrationTestCase):
"""
Integration tests for EventHost.
Use this class for testing interactions between multiple components.
"""
pass
@@ -0,0 +1,36 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-12-22 14:26:23.528630",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"payment_gateway"
],
"fields": [
{
"fieldname": "payment_gateway",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Payment Gateway",
"options": "Payment Gateway",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-12-22 14:27:25.962536",
"modified_by": "Administrator",
"module": "Events",
"name": "Event Payment Gateway",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,23 @@
# Copyright (c) 2025, BWH Studios and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class EventPaymentGateway(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
payment_gateway: DF.Link
# end: auto-generated types
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2025, BWH Studios and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Event Sponsor", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,118 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-07-19 12:48:18.436949",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"company_name",
"company_logo",
"website",
"column_break_ozou",
"event",
"tier",
"country",
"additional_section",
"enquiry"
],
"fields": [
{
"fieldname": "column_break_ozou",
"fieldtype": "Column Break"
},
{
"fieldname": "event",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Event",
"options": "Pohodex Event Manager Event",
"reqd": 1
},
{
"fieldname": "company_name",
"fieldtype": "Data",
"label": "Company Name",
"reqd": 1
},
{
"fieldname": "tier",
"fieldtype": "Link",
"label": "Tier",
"options": "Sponsorship Tier",
"reqd": 1
},
{
"fieldname": "company_logo",
"fieldtype": "Attach Image",
"label": "Company Logo",
"reqd": 1
},
{
"fieldname": "additional_section",
"fieldtype": "Section Break",
"label": "Additional"
},
{
"fieldname": "enquiry",
"fieldtype": "Link",
"label": "Enquiry",
"options": "Sponsorship Enquiry"
},
{
"fieldname": "website",
"fieldtype": "Data",
"label": "Website",
"options": "URL"
},
{
"fieldname": "country",
"fieldtype": "Link",
"label": "Country",
"options": "Country"
}
],
"grid_page_length": 50,
"image_field": "company_logo",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-10-28 16:18:05.658346",
"modified_by": "Administrator",
"module": "Events",
"name": "Event Sponsor",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Event Manager",
"select": 1,
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "company_name",
"track_changes": 1
}
@@ -0,0 +1,32 @@
# Copyright (c) 2025, BWH Studios and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class EventSponsor(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
company_logo: DF.AttachImage
company_name: DF.Data
country: DF.Link | None
enquiry: DF.Link | None
event: DF.Link
tier: DF.Link
website: DF.Data | None
# end: auto-generated types
def validate(self):
already_exists = frappe.db.exists(
"Event Sponsor", {"event": self.event, "enquiry": self.enquiry, "name": ("!=", self.name)}
)
if already_exists:
frappe.throw(frappe._("Sponsor for this enquiry already exists!"))
@@ -0,0 +1,46 @@
# Copyright (c) 2025, BWH Studios and Contributors
# See license.txt
import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestEventSponsor(IntegrationTestCase):
"""
Integration tests for EventSponsor.
Use this class for testing interactions between multiple components.
"""
def test_enquiry_to_sponsor_flow(self):
test_event = frappe.get_doc("Pohodex Event Manager Event", {"route": "test-route"})
test_sponsorship_tier = frappe.get_doc(
{
"doctype": "Sponsorship Tier",
"event": test_event.name,
"title": "Super Platinum",
"price": 1000,
"currency": "INR",
}
).insert()
test_enquiry = frappe.get_doc(
{
"doctype": "Sponsorship Enquiry",
"event": test_event.name,
"company_name": "Test Studios",
"company_logo": "https://buildwithhussain.com/files/youtube2.png",
"tier": test_sponsorship_tier.name,
}
).insert()
# "Payment Success trigger"
test_enquiry.on_payment_authorized("Completed")
self.assertEqual(test_enquiry.status, "Paid")
self.assertTrue(frappe.db.exists("Event Sponsor", {"enquiry": test_enquiry.name}))
@@ -0,0 +1,8 @@
// Copyright (c) 2025, BWH Studios and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Event Talk", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,132 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "autoincrement",
"creation": "2025-07-19 12:15:38.420883",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"title",
"submitted_by",
"column_break_npnb",
"proposal",
"event",
"section_break_rnda",
"speakers",
"section_break_nrdk",
"description"
],
"fields": [
{
"fetch_from": "proposal.title",
"fetch_if_empty": 1,
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "section_break_rnda",
"fieldtype": "Section Break"
},
{
"fieldname": "speakers",
"fieldtype": "Table",
"label": "Speakers",
"options": "Talk Speaker"
},
{
"fetch_from": "proposal.submitted_by",
"fieldname": "submitted_by",
"fieldtype": "Link",
"label": "Submitted By",
"options": "User",
"reqd": 1
},
{
"fieldname": "column_break_npnb",
"fieldtype": "Column Break"
},
{
"fieldname": "proposal",
"fieldtype": "Link",
"label": "Proposal",
"options": "Talk Proposal"
},
{
"fetch_from": "proposal.event",
"fieldname": "event",
"fieldtype": "Link",
"label": "Event",
"options": "Pohodex Event Manager Event",
"reqd": 1
},
{
"fieldname": "section_break_nrdk",
"fieldtype": "Section Break"
},
{
"fetch_from": "proposal.description",
"fetch_if_empty": 1,
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-01-14 12:38:51.583900",
"modified_by": "Administrator",
"module": "Events",
"name": "Event Talk",
"naming_rule": "Autoincrement",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Event Manager",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Pohodex Event Manager User",
"select": 1,
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
@@ -0,0 +1,30 @@
# Copyright (c) 2025, BWH Studios and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class EventTalk(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from event_manager.events.doctype.talk_speaker.talk_speaker import TalkSpeaker
description: DF.TextEditor | None
event: DF.Link
name: DF.Int | None
proposal: DF.Link | None
speakers: DF.Table[TalkSpeaker]
submitted_by: DF.Link
title: DF.Data
# end: auto-generated types
def validate(self):
if frappe.db.exists("Event Talk", {"proposal": self.proposal, "name": ["!=", self.name]}):
frappe.throw("Talk already created for this proposal!")
@@ -0,0 +1,20 @@
# Copyright (c) 2025, BWH Studios and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestEventTalk(IntegrationTestCase):
"""
Integration tests for EventTalk.
Use this class for testing interactions between multiple components.
"""
pass
@@ -0,0 +1,2 @@
# Copyright (c) 2025, Frappe Technologies and contributors
# For license information, please see license.txt
@@ -0,0 +1,327 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:template_name",
"creation": "2025-12-31 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"template_name",
"category",
"medium",
"column_break_main",
"banner_image",
"host",
"venue",
"allow_guest_booking",
"guest_verification_method",
"section_break_details",
"time_zone",
"short_description",
"about",
"payments_tab",
"section_break_payments",
"payment_gateways",
"tax_settings_section",
"apply_tax",
"tax_label",
"tax_percentage",
"sponsorships_tab",
"automations_section",
"auto_send_pitch_deck",
"section_break_sponsor",
"sponsor_deck_reply_to",
"sponsor_deck_email_template",
"column_break_sponsor",
"sponsor_deck_cc",
"section_break_attachments",
"sponsor_deck_attachments",
"customisations_tab",
"send_ticket_email",
"ticket_email_template",
"column_break_custom",
"ticket_print_format",
"ticket_types_tab",
"template_ticket_types",
"add_ons_tab",
"template_add_ons",
"custom_fields_tab",
"template_custom_fields"
],
"fields": [
{
"fieldname": "template_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Template Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "category",
"fieldtype": "Link",
"label": "Category",
"options": "Event Category"
},
{
"default": "In Person",
"fieldname": "medium",
"fieldtype": "Select",
"label": "Medium",
"options": "In Person\nOnline"
},
{
"fieldname": "column_break_main",
"fieldtype": "Column Break"
},
{
"fieldname": "banner_image",
"fieldtype": "Attach Image",
"label": "Banner Image"
},
{
"fieldname": "host",
"fieldtype": "Link",
"label": "Host",
"options": "Event Host"
},
{
"depends_on": "eval:doc.medium!=\"Online\"",
"fieldname": "venue",
"fieldtype": "Link",
"label": "Venue",
"options": "Event Venue"
},
{
"default": "0",
"fieldname": "allow_guest_booking",
"fieldtype": "Check",
"label": "Allow Guest Booking"
},
{
"default": "Email OTP",
"depends_on": "eval:doc.allow_guest_booking",
"description": "How to verify guest identity before booking",
"fieldname": "guest_verification_method",
"fieldtype": "Select",
"label": "Guest Verification Method",
"options": "None\nEmail OTP\nPhone OTP"
},
{
"fieldname": "section_break_details",
"fieldtype": "Section Break"
},
{
"fieldname": "time_zone",
"fieldtype": "Autocomplete",
"label": "Time Zone"
},
{
"fieldname": "short_description",
"fieldtype": "Small Text",
"label": "Short Description"
},
{
"fieldname": "about",
"fieldtype": "Text Editor",
"label": "About"
},
{
"fieldname": "payments_tab",
"fieldtype": "Tab Break",
"label": "Payments"
},
{
"fieldname": "section_break_payments",
"fieldtype": "Section Break"
},
{
"fieldname": "payment_gateways",
"fieldtype": "Table",
"label": "Payment Gateways",
"options": "Event Payment Gateway"
},
{
"fieldname": "tax_settings_section",
"fieldtype": "Section Break",
"label": "Tax Settings"
},
{
"default": "0",
"fieldname": "apply_tax",
"fieldtype": "Check",
"label": "Apply Tax on Bookings?"
},
{
"default": "GST",
"depends_on": "eval:doc.apply_tax==1",
"fieldname": "tax_label",
"fieldtype": "Data",
"label": "Tax Label"
},
{
"default": "18",
"depends_on": "eval:doc.apply_tax==1",
"fieldname": "tax_percentage",
"fieldtype": "Percent",
"label": "Tax Percentage"
},
{
"fieldname": "sponsorships_tab",
"fieldtype": "Tab Break",
"label": "Sponsorships"
},
{
"fieldname": "automations_section",
"fieldtype": "Section Break",
"label": "Automations"
},
{
"default": "0",
"fieldname": "auto_send_pitch_deck",
"fieldtype": "Check",
"label": "Auto Send Pitch Deck?"
},
{
"fieldname": "section_break_sponsor",
"fieldtype": "Section Break"
},
{
"depends_on": "eval:doc.auto_send_pitch_deck==true",
"fieldname": "sponsor_deck_reply_to",
"fieldtype": "Data",
"label": "Reply To"
},
{
"depends_on": "eval:doc.auto_send_pitch_deck==true",
"fieldname": "sponsor_deck_email_template",
"fieldtype": "Link",
"label": "Email Template",
"options": "Email Template"
},
{
"fieldname": "column_break_sponsor",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.auto_send_pitch_deck==true",
"fieldname": "sponsor_deck_cc",
"fieldtype": "Small Text",
"label": "CC"
},
{
"fieldname": "section_break_attachments",
"fieldtype": "Section Break"
},
{
"depends_on": "eval:doc.auto_send_pitch_deck==true",
"fieldname": "sponsor_deck_attachments",
"fieldtype": "Table",
"label": "Attachments",
"options": "Sponsorship Deck Item"
},
{
"fieldname": "customisations_tab",
"fieldtype": "Tab Break",
"label": "Customisations"
},
{
"default": "1",
"fieldname": "send_ticket_email",
"fieldtype": "Check",
"label": "Send Ticket Email"
},
{
"fieldname": "ticket_email_template",
"fieldtype": "Link",
"label": "Ticket Email Template",
"options": "Email Template"
},
{
"fieldname": "column_break_custom",
"fieldtype": "Column Break"
},
{
"fieldname": "ticket_print_format",
"fieldtype": "Link",
"label": "Ticket Print Format",
"link_filters": "[[\"Print Format\",\"doc_type\",\"=\",\"Event Ticket\"]]",
"options": "Print Format"
},
{
"fieldname": "ticket_types_tab",
"fieldtype": "Tab Break",
"label": "Ticket Types"
},
{
"fieldname": "template_ticket_types",
"fieldtype": "Table",
"label": "Ticket Types",
"options": "Event Template Ticket Type"
},
{
"fieldname": "add_ons_tab",
"fieldtype": "Tab Break",
"label": "Add-ons"
},
{
"fieldname": "template_add_ons",
"fieldtype": "Table",
"label": "Add-ons",
"options": "Event Template Add-on"
},
{
"fieldname": "custom_fields_tab",
"fieldtype": "Tab Break",
"label": "Custom Fields"
},
{
"fieldname": "template_custom_fields",
"fieldtype": "Table",
"label": "Custom Fields",
"options": "Event Template Custom Field"
}
],
"image_field": "banner_image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-12-31 12:00:00.000000",
"modified_by": "Administrator",
"module": "Events",
"name": "Event Template",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Event Manager",
"select": 1,
"share": 1,
"write": 1
}
],
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "template_name",
"track_changes": 1
}
@@ -0,0 +1,197 @@
# Copyright (c) 2025, Frappe Technologies and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class EventTemplate(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from event_manager.events.doctype.event_payment_gateway.event_payment_gateway import EventPaymentGateway
from event_manager.events.doctype.event_template_add_on.event_template_add_on import EventTemplateAddOn
from event_manager.events.doctype.event_template_custom_field.event_template_custom_field import (
EventTemplateCustomField,
)
from event_manager.events.doctype.event_template_ticket_type.event_template_ticket_type import (
EventTemplateTicketType,
)
from event_manager.proposals.doctype.sponsorship_deck_item.sponsorship_deck_item import SponsorshipDeckItem
about: DF.TextEditor | None
apply_tax: DF.Check
auto_send_pitch_deck: DF.Check
banner_image: DF.AttachImage | None
category: DF.Link | None
host: DF.Link | None
medium: DF.Literal["In Person", "Online"]
payment_gateways: DF.Table[EventPaymentGateway]
short_description: DF.SmallText | None
sponsor_deck_attachments: DF.Table[SponsorshipDeckItem]
sponsor_deck_cc: DF.SmallText | None
sponsor_deck_email_template: DF.Link | None
sponsor_deck_reply_to: DF.Data | None
tax_label: DF.Data | None
tax_percentage: DF.Percent
template_add_ons: DF.Table[EventTemplateAddOn]
template_custom_fields: DF.Table[EventTemplateCustomField]
template_name: DF.Data
template_ticket_types: DF.Table[EventTemplateTicketType]
ticket_email_template: DF.Link | None
ticket_print_format: DF.Link | None
time_zone: DF.Autocomplete | None
venue: DF.Link | None
# end: auto-generated types
pass
@frappe.whitelist()
def create_template_from_event(event_name: str, template_name: str, options: str) -> str:
"""
Create an Event Template from an existing Pohodex Event Manager Event.
Args:
event_name: Name of the source Pohodex Event Manager Event
template_name: Name for the new template
options: JSON string of what to include
Returns:
New Event Template document name
"""
if not frappe.has_permission("Event Template", "create"):
frappe.throw(_("You don't have permission to create templates"))
event = frappe.get_doc("Pohodex Event Manager Event", event_name)
options = frappe.parse_json(options)
template = frappe.new_doc("Event Template")
template.template_name = template_name
# Field mapping for direct copy
field_map = {
"category": "category",
"host": "host",
"banner_image": "banner_image",
"short_description": "short_description",
"about": "about",
"medium": "medium",
"venue": "venue",
"allow_guest_booking": "allow_guest_booking",
"guest_verification_method": "guest_verification_method",
"time_zone": "time_zone",
"send_ticket_email": "send_ticket_email",
"ticket_email_template": "ticket_email_template",
"ticket_print_format": "ticket_print_format",
"apply_tax": "apply_tax",
"tax_label": "tax_label",
"tax_percentage": "tax_percentage",
"auto_send_pitch_deck": "auto_send_pitch_deck",
"sponsor_deck_email_template": "sponsor_deck_email_template",
"sponsor_deck_reply_to": "sponsor_deck_reply_to",
"sponsor_deck_cc": "sponsor_deck_cc",
}
for option_key, field_name in field_map.items():
if options.get(option_key):
template.set(field_name, event.get(field_name))
# Copy child tables from event
if options.get("payment_gateways"):
for pg in event.payment_gateways:
template.append("payment_gateways", {"payment_gateway": pg.payment_gateway})
if options.get("sponsor_deck_attachments"):
for attachment in event.sponsor_deck_attachments:
template.append("sponsor_deck_attachments", {"file": attachment.file})
# Copy linked documents (Ticket Types, Add-ons, Custom Fields)
if options.get("ticket_types"):
ticket_types = frappe.get_all(
"Event Ticket Type",
filters={"event": event_name},
fields=[
"title",
"price",
"currency",
"is_published",
"max_tickets_available",
"auto_unpublish_after",
],
)
for tt in ticket_types:
template.append(
"template_ticket_types",
{
"title": tt.title,
"price": tt.price,
"currency": tt.currency,
"is_published": tt.is_published,
"max_tickets_available": tt.max_tickets_available,
"auto_unpublish_after": tt.auto_unpublish_after,
},
)
if options.get("add_ons"):
add_ons = frappe.get_all(
"Ticket Add-on",
filters={"event": event_name},
fields=["title", "price", "currency", "description", "user_selects_option", "options", "enabled"],
)
for addon in add_ons:
template.append(
"template_add_ons",
{
"title": addon.title,
"price": addon.price,
"currency": addon.currency,
"description": addon.description,
"user_selects_option": addon.user_selects_option,
"options": addon.options,
"enabled": addon.enabled,
},
)
if options.get("custom_fields"):
custom_fields = frappe.get_all(
"Pohodex Event Manager Custom Field",
filters={"event": event_name},
fields=[
"label",
"fieldname",
"fieldtype",
"options",
"applied_to",
"enabled",
"mandatory",
"placeholder",
"default_value",
"order",
],
)
for cf in custom_fields:
template.append(
"template_custom_fields",
{
"label": cf.label,
"fieldname": cf.fieldname,
"fieldtype": cf.fieldtype,
"options": cf.options,
"applied_to": cf.applied_to,
"enabled": cf.enabled,
"mandatory": cf.mandatory,
"placeholder": cf.placeholder,
"default_value": cf.default_value,
"order": cf.order,
},
)
template.insert()
return template.name
@@ -0,0 +1,505 @@
# Copyright (c) 2025, Frappe Technologies and contributors
# For license information, please see license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from event_manager.events.doctype.buzz_event.buzz_event import create_from_template
from event_manager.events.doctype.event_template.event_template import create_template_from_event
class TestEventTemplate(FrappeTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.create_test_fixtures()
@classmethod
def create_test_fixtures(cls):
"""Create required test data: Event Category, Host, etc."""
# Create Event Category if not exists
if not frappe.db.exists("Event Category", "Test Category"):
frappe.get_doc({"doctype": "Event Category", "category_name": "Test Category"}).insert(
ignore_permissions=True
)
# Create Event Host if not exists
if not frappe.db.exists("Event Host", "Test Host"):
frappe.get_doc({"doctype": "Event Host", "host_name": "Test Host"}).insert(
ignore_permissions=True
)
def tearDown(self):
"""Clean up test data after each test"""
frappe.db.rollback()
# ==================== Template Creation Tests ====================
def test_create_template_basic(self):
"""Test creating a basic Event Template"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Test Webinar Template",
"category": "Test Category",
"host": "Test Host",
"medium": "Online",
"about": "Test description",
}
)
template.insert()
self.assertEqual(template.template_name, "Test Webinar Template")
self.assertEqual(template.category, "Test Category")
self.assertEqual(template.medium, "Online")
def test_create_template_with_ticket_types(self):
"""Test creating a template with ticket types"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Template with Tickets",
"category": "Test Category",
"host": "Test Host",
"template_ticket_types": [
{
"title": "Early Bird",
"price": 100,
"currency": "INR",
"is_published": 1,
"max_tickets_available": 50,
},
{"title": "Regular", "price": 200, "currency": "INR", "is_published": 1},
],
}
)
template.insert()
self.assertEqual(len(template.template_ticket_types), 2)
self.assertEqual(template.template_ticket_types[0].title, "Early Bird")
self.assertEqual(template.template_ticket_types[0].price, 100)
def test_create_template_with_add_ons(self):
"""Test creating a template with add-ons"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Template with Add-ons",
"category": "Test Category",
"host": "Test Host",
"template_add_ons": [
{"title": "T-Shirt", "price": 500, "currency": "INR", "enabled": 1},
{
"title": "Lunch",
"price": 300,
"currency": "INR",
"user_selects_option": 1,
"options": "Veg\nNon-Veg",
"enabled": 1,
},
],
}
)
template.insert()
self.assertEqual(len(template.template_add_ons), 2)
self.assertEqual(template.template_add_ons[1].user_selects_option, 1)
def test_create_template_with_custom_fields(self):
"""Test creating a template with custom fields"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Template with Custom Fields",
"category": "Test Category",
"host": "Test Host",
"template_custom_fields": [
{
"label": "Company Name",
"fieldname": "company_name",
"fieldtype": "Data",
"applied_to": "Booking",
"mandatory": 1,
"enabled": 1,
},
{
"label": "Dietary Preference",
"fieldname": "dietary_preference",
"fieldtype": "Select",
"options": "Veg\nNon-Veg\nVegan",
"applied_to": "Ticket",
"enabled": 1,
},
],
}
)
template.insert()
self.assertEqual(len(template.template_custom_fields), 2)
self.assertEqual(template.template_custom_fields[0].mandatory, 1)
# ==================== Create Event from Template Tests ====================
def test_create_event_from_template_all_options(self):
"""Test creating an event from template with all options selected"""
# Create template
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Full Template",
"category": "Test Category",
"host": "Test Host",
"medium": "Online",
"about": "Template about text",
"apply_tax": 1,
"tax_label": "GST",
"tax_percentage": 18,
"template_ticket_types": [
{"title": "Standard", "price": 500, "currency": "INR", "is_published": 1}
],
"template_add_ons": [{"title": "Workshop", "price": 1000, "currency": "INR", "enabled": 1}],
"template_custom_fields": [
{
"label": "Phone",
"fieldname": "phone",
"fieldtype": "Phone",
"applied_to": "Booking",
"enabled": 1,
}
],
}
)
template.insert()
# Create event from template with all options
options = {
"category": 1,
"host": 1,
"medium": 1,
"about": 1,
"apply_tax": 1,
"tax_label": 1,
"tax_percentage": 1,
"ticket_types": 1,
"add_ons": 1,
"custom_fields": 1,
}
event_name = create_from_template(template.name, frappe.as_json(options))
event = frappe.get_doc("Pohodex Event Manager Event", event_name)
# Verify event fields
self.assertEqual(event.category, "Test Category")
self.assertEqual(event.host, "Test Host")
self.assertEqual(event.medium, "Online")
self.assertEqual(event.about, "Template about text")
self.assertEqual(event.apply_tax, 1)
self.assertEqual(event.tax_percentage, 18)
# Verify ticket types created (excluding default "Normal" ticket type)
ticket_types = frappe.get_all(
"Event Ticket Type", filters={"event": event_name, "title": "Standard"}, fields=["title", "price"]
)
self.assertEqual(len(ticket_types), 1)
self.assertEqual(ticket_types[0].title, "Standard")
# Verify add-ons created
add_ons = frappe.get_all("Ticket Add-on", filters={"event": event_name}, fields=["title", "price"])
self.assertEqual(len(add_ons), 1)
self.assertEqual(add_ons[0].title, "Workshop")
# Verify custom fields created
custom_fields = frappe.get_all(
"Pohodex Event Manager Custom Field", filters={"event": event_name}, fields=["label", "fieldtype"]
)
self.assertEqual(len(custom_fields), 1)
self.assertEqual(custom_fields[0].fieldtype, "Phone")
def test_create_event_from_template_partial_options(self):
"""Test creating an event with only some options selected"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Partial Template",
"category": "Test Category",
"host": "Test Host",
"medium": "In Person",
"about": "Should not be copied",
"template_ticket_types": [
{"title": "VIP", "price": 2000, "currency": "INR", "is_published": 1}
],
}
)
template.insert()
# Copy category, host (required) and ticket types, but not medium/about
options = {"category": 1, "host": 1, "medium": 0, "about": 0, "ticket_types": 1}
event_name = create_from_template(template.name, frappe.as_json(options))
event = frappe.get_doc("Pohodex Event Manager Event", event_name)
# Category should be copied
self.assertEqual(event.category, "Test Category")
# Host should be copied (it's mandatory)
self.assertEqual(event.host, "Test Host")
# About should NOT be copied
self.assertFalse(event.about)
# Ticket types should be copied
ticket_types = frappe.get_all("Event Ticket Type", filters={"event": event_name, "title": "VIP"})
self.assertEqual(len(ticket_types), 1)
def test_create_event_from_template_no_linked_docs(self):
"""Test creating an event without copying linked documents"""
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "No Linked Docs Template",
"category": "Test Category",
"host": "Test Host",
"template_ticket_types": [
{"title": "General", "price": 100, "currency": "INR", "is_published": 1}
],
}
)
template.insert()
# Copy fields but not linked docs
options = {"category": 1, "host": 1, "ticket_types": 0, "add_ons": 0, "custom_fields": 0}
event_name = create_from_template(template.name, frappe.as_json(options))
# Event fields should be copied
event = frappe.get_doc("Pohodex Event Manager Event", event_name)
self.assertEqual(event.category, "Test Category")
# No "General" ticket type should be created (only default "Normal")
ticket_types = frappe.get_all("Event Ticket Type", filters={"event": event_name, "title": "General"})
self.assertEqual(len(ticket_types), 0)
# ==================== Save as Template Tests ====================
def test_save_event_as_template(self):
"""Test saving an existing event as a template"""
# Create an event with ticket types and add-ons
event = frappe.get_doc(
{
"doctype": "Pohodex Event Manager Event",
"title": "Source Event",
"category": "Test Category",
"host": "Test Host",
"start_date": frappe.utils.today(),
"start_time": "09:00:00",
"end_time": "18:00:00",
"medium": "Online",
"about": "Event description",
}
)
event.insert()
# Create ticket type for the event
ticket_type = frappe.get_doc(
{
"doctype": "Event Ticket Type",
"event": event.name,
"title": "Premium",
"price": 1500,
"currency": "INR",
"is_published": 1,
}
)
ticket_type.insert()
# Create add-on for the event
add_on = frappe.get_doc(
{
"doctype": "Ticket Add-on",
"event": event.name,
"title": "Swag Kit",
"price": 500,
"currency": "INR",
"enabled": 1,
}
)
add_on.insert()
# Save as template (convert event.name to string as it's an int autoname)
options = {"category": 1, "host": 1, "medium": 1, "about": 1, "ticket_types": 1, "add_ons": 1}
template_name = create_template_from_event(
str(event.name), "My Event Template", frappe.as_json(options)
)
template = frappe.get_doc("Event Template", template_name)
# Verify template fields
self.assertEqual(template.template_name, "My Event Template")
self.assertEqual(template.category, "Test Category")
self.assertEqual(template.medium, "Online")
# Verify ticket types in template (excluding default "Normal")
premium_tickets = [t for t in template.template_ticket_types if t.title == "Premium"]
self.assertEqual(len(premium_tickets), 1)
self.assertEqual(premium_tickets[0].price, 1500)
# Verify add-ons in template
self.assertEqual(len(template.template_add_ons), 1)
self.assertEqual(template.template_add_ons[0].title, "Swag Kit")
def test_save_event_as_template_partial(self):
"""Test saving event as template with only some options"""
event = frappe.get_doc(
{
"doctype": "Pohodex Event Manager Event",
"title": "Partial Source Event",
"category": "Test Category",
"host": "Test Host",
"start_date": frappe.utils.today(),
"start_time": "09:00:00",
"end_time": "18:00:00",
"medium": "In Person",
"about": "Should be copied",
"apply_tax": 1,
"tax_percentage": 18,
}
)
event.insert()
# Only save category and about (convert event.name to string as it's an int autoname)
options = {"category": 1, "host": 0, "medium": 0, "about": 1, "apply_tax": 0}
template_name = create_template_from_event(
str(event.name), "Partial Template 2", frappe.as_json(options)
)
template = frappe.get_doc("Event Template", template_name)
self.assertEqual(template.category, "Test Category")
self.assertEqual(template.about, "Should be copied")
self.assertFalse(template.host)
self.assertFalse(template.apply_tax)
# ==================== Round Trip Tests ====================
def test_round_trip_event_to_template_to_event(self):
"""Test full round trip: Event -> Template -> New Event"""
# Step 1: Create original event
original_event = frappe.get_doc(
{
"doctype": "Pohodex Event Manager Event",
"title": "Original Conference",
"category": "Test Category",
"host": "Test Host",
"start_date": frappe.utils.today(),
"start_time": "09:00:00",
"end_time": "18:00:00",
"medium": "In Person",
"about": "Annual conference description",
"apply_tax": 1,
"tax_label": "GST",
"tax_percentage": 18,
}
)
original_event.insert()
# Add ticket types
for ticket_data in [
{"title": "Early Bird", "price": 1000},
{"title": "Regular", "price": 1500},
{"title": "VIP", "price": 3000},
]:
frappe.get_doc(
{
"doctype": "Event Ticket Type",
"event": original_event.name,
"title": ticket_data["title"],
"price": ticket_data["price"],
"currency": "INR",
"is_published": 1,
}
).insert()
# Step 2: Save as template (convert event.name to string as it's an int autoname)
template_options = {
"category": 1,
"host": 1,
"medium": 1,
"about": 1,
"apply_tax": 1,
"tax_label": 1,
"tax_percentage": 1,
"ticket_types": 1,
}
template_name = create_template_from_event(
str(original_event.name), "Conference Template", frappe.as_json(template_options)
)
# Step 3: Create new event from template
event_options = {
"category": 1,
"host": 1,
"medium": 1,
"about": 1,
"apply_tax": 1,
"tax_label": 1,
"tax_percentage": 1,
"ticket_types": 1,
}
new_event_name = create_from_template(template_name, frappe.as_json(event_options))
new_event = frappe.get_doc("Pohodex Event Manager Event", new_event_name)
# Verify new event matches original
self.assertEqual(new_event.category, original_event.category)
self.assertEqual(new_event.host, original_event.host)
self.assertEqual(new_event.medium, original_event.medium)
self.assertEqual(new_event.about, original_event.about)
self.assertEqual(new_event.tax_percentage, original_event.tax_percentage)
# Verify ticket types match (excluding default "Normal")
new_ticket_types = frappe.get_all(
"Event Ticket Type",
filters={"event": new_event_name, "title": ["in", ["Early Bird", "Regular", "VIP"]]},
fields=["title", "price"],
order_by="price",
)
self.assertEqual(len(new_ticket_types), 3)
self.assertEqual(new_ticket_types[0].title, "Early Bird")
self.assertEqual(new_ticket_types[0].price, 1000)
# ==================== Edge Case Tests ====================
def test_create_event_empty_template(self):
"""Test creating event from template with minimal data"""
# Template with required fields for Pohodex Event Manager Event (category and host are mandatory)
template = frappe.get_doc(
{
"doctype": "Event Template",
"template_name": "Empty Template",
"category": "Test Category",
"host": "Test Host",
}
)
template.insert()
options = {"category": 1, "host": 1}
event_name = create_from_template(template.name, frappe.as_json(options))
# Should create event without errors
self.assertTrue(frappe.db.exists("Pohodex Event Manager Event", event_name))
def test_template_name_required(self):
"""Test that template_name is required"""
template = frappe.get_doc({"doctype": "Event Template", "category": "Test Category"})
# Template uses autoname: field:template_name, so it raises ValidationError not MandatoryError
with self.assertRaises(frappe.exceptions.ValidationError):
template.insert()
def test_duplicate_template_name(self):
"""Test handling of duplicate template names"""
frappe.get_doc({"doctype": "Event Template", "template_name": "Duplicate Name"}).insert()
duplicate = frappe.get_doc({"doctype": "Event Template", "template_name": "Duplicate Name"})
with self.assertRaises(frappe.exceptions.DuplicateEntryError):
duplicate.insert()
@@ -0,0 +1,2 @@
# Copyright (c) 2025, Frappe Technologies and contributors
# For license information, please see license.txt
@@ -0,0 +1,84 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-12-31 12:00:00.000000",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"title",
"price",
"description",
"column_break_abcd",
"currency",
"user_selects_option",
"options"
],
"fields": [
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled?"
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"default": "0",
"fieldname": "price",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Price",
"non_negative": 1,
"options": "currency"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "column_break_abcd",
"fieldtype": "Column Break"
},
{
"default": "INR",
"fieldname": "currency",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Currency",
"options": "Currency"
},
{
"default": "0",
"fieldname": "user_selects_option",
"fieldtype": "Check",
"label": "User Selects Option?"
},
{
"fieldname": "options",
"fieldtype": "Small Text",
"label": "Options",
"mandatory_depends_on": "eval:doc.user_selects_option==true"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-12-31 12:00:00.000000",
"modified_by": "Administrator",
"module": "Events",
"name": "Event Template Add-on",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,29 @@
# Copyright (c) 2025, Frappe Technologies and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class EventTemplateAddon(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
currency: DF.Link | None
description: DF.SmallText | None
enabled: DF.Check
options: DF.SmallText | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
price: DF.Currency
title: DF.Data
user_selects_option: DF.Check
# end: auto-generated types
pass
@@ -0,0 +1,2 @@
# Copyright (c) 2025, Frappe Technologies and contributors
# For license information, please see license.txt
@@ -0,0 +1,103 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-12-31 12:00:00.000000",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"label",
"fieldname",
"mandatory",
"placeholder",
"default_value",
"column_break_efgh",
"applied_to",
"fieldtype",
"options",
"order"
],
"fields": [
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled?"
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"reqd": 1
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"label": "Name"
},
{
"default": "0",
"fieldname": "mandatory",
"fieldtype": "Check",
"label": "Mandatory?"
},
{
"fieldname": "placeholder",
"fieldtype": "Data",
"label": "Placeholder"
},
{
"fieldname": "default_value",
"fieldtype": "Data",
"label": "Default Value"
},
{
"fieldname": "column_break_efgh",
"fieldtype": "Column Break"
},
{
"default": "Booking",
"fieldname": "applied_to",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Applied To",
"options": "Booking\nTicket"
},
{
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "Data\nPhone\nEmail\nSelect\nDate\nNumber",
"reqd": 1
},
{
"fieldname": "options",
"fieldtype": "Small Text",
"label": "Options"
},
{
"default": "1",
"fieldname": "order",
"fieldtype": "Int",
"label": "Order",
"non_negative": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-12-31 12:00:00.000000",
"modified_by": "Administrator",
"module": "Events",
"name": "Event Template Custom Field",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,32 @@
# Copyright (c) 2025, Frappe Technologies and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class EventTemplateCustomField(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
applied_to: DF.Literal["Booking", "Ticket"]
default_value: DF.Data | None
enabled: DF.Check
fieldname: DF.Data | None
fieldtype: DF.Literal["Data", "Phone", "Email", "Select", "Date", "Number"]
label: DF.Data
mandatory: DF.Check
options: DF.SmallText | None
order: DF.Int
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
placeholder: DF.Data | None
# end: auto-generated types
pass
@@ -0,0 +1,2 @@
# Copyright (c) 2025, Frappe Technologies and contributors
# For license information, please see license.txt
@@ -0,0 +1,79 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-12-31 12:00:00.000000",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"price",
"currency",
"column_break_xyzq",
"is_published",
"max_tickets_available",
"auto_unpublish_after"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"default": "0",
"fieldname": "price",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Price",
"non_negative": 1,
"options": "currency"
},
{
"default": "INR",
"fieldname": "currency",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Currency",
"options": "Currency",
"reqd": 1
},
{
"fieldname": "column_break_xyzq",
"fieldtype": "Column Break"
},
{
"default": "1",
"fieldname": "is_published",
"fieldtype": "Check",
"label": "Is Published?"
},
{
"fieldname": "max_tickets_available",
"fieldtype": "Int",
"label": "Max Tickets Available",
"non_negative": 1
},
{
"depends_on": "eval:doc.is_published",
"fieldname": "auto_unpublish_after",
"fieldtype": "Date",
"label": "Auto Unpublish After"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-12-31 12:00:00.000000",
"modified_by": "Administrator",
"module": "Events",
"name": "Event Template Ticket Type",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

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