From 631f72669114041906ceb733f2d1da70df27521c Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Sun, 22 Jun 2025 19:04:45 +0200 Subject: [PATCH 1/4] Implement support requests via Odoo --- .env.example | 2 + src/servala/core/models/organization.py | 1 + src/servala/core/models/user.py | 21 ++++++++ src/servala/frontend/forms/__init__.py | 3 +- src/servala/frontend/forms/support.py | 10 ++++ .../frontend/organizations/support.html | 43 ++++++++++++++++ .../frontend/templates/includes/menu.html | 7 +++ src/servala/frontend/urls.py | 5 ++ src/servala/frontend/views/__init__.py | 2 + src/servala/frontend/views/support.py | 50 +++++++++++++++++++ src/servala/settings.py | 1 + 11 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 src/servala/frontend/forms/support.py create mode 100644 src/servala/frontend/templates/frontend/organizations/support.html create mode 100644 src/servala/frontend/views/support.py diff --git a/.env.example b/.env.example index aa287ce..ab361d7 100644 --- a/.env.example +++ b/.env.example @@ -66,3 +66,5 @@ SERVALA_ODOO_DB='' SERVALA_ODOO_URL='' SERVALA_ODOO_USERNAME='' SERVALA_ODOO_PASSWORD='' +# Helpdesk team ID for support tickets in Odoo. Defaults to 5. +SERVALA_ODOO_HELPDESK_TEAM_ID='5' diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 1662dfa..a9dcd4e 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -58,6 +58,7 @@ class Organization(ServalaModelMixin, models.Model): details = "{base}details/" services = "{base}services/" instances = "{base}instances/" + support = "{base}support/" @cached_property def slug(self): diff --git a/src/servala/core/models/user.py b/src/servala/core/models/user.py index 5d513f4..38cf80c 100644 --- a/src/servala/core/models/user.py +++ b/src/servala/core/models/user.py @@ -93,3 +93,24 @@ class User(ServalaModelMixin, PermissionsMixin, AbstractBaseUser): ) if result: return result[0] + + def get_or_create_odoo_contact(self, organization): + if ( + not organization.billing_entity + or not organization.billing_entity.odoo_company_id + ): + return None + + if existing_contact := self.get_odoo_contact(organization): + return existing_contact["id"] + + partner_data = { + "name": f"{self.first_name} {self.last_name}".strip() or self.email, + "email": self.email, + "company_type": "person", + "type": "contact", + "parent_id": organization.billing_entity.odoo_company_id, + } + + contact_id = odoo.CLIENT.execute("res.partner", "create", [partner_data]) + return contact_id diff --git a/src/servala/frontend/forms/__init__.py b/src/servala/frontend/forms/__init__.py index 01447a1..0bf5d64 100644 --- a/src/servala/frontend/forms/__init__.py +++ b/src/servala/frontend/forms/__init__.py @@ -1,4 +1,5 @@ from .organization import OrganizationForm from .profile import UserProfileForm +from .support import SupportForm -__all__ = ["OrganizationForm", "UserProfileForm"] +__all__ = ["OrganizationForm", "UserProfileForm", "SupportForm"] diff --git a/src/servala/frontend/forms/support.py b/src/servala/frontend/forms/support.py new file mode 100644 index 0000000..dd62c95 --- /dev/null +++ b/src/servala/frontend/forms/support.py @@ -0,0 +1,10 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + + +class SupportForm(forms.Form): + message = forms.CharField( + label=_("Message"), + widget=forms.Textarea(attrs={"rows": 8}), + help_text=_("Please describe your issue or question in detail."), + ) diff --git a/src/servala/frontend/templates/frontend/organizations/support.html b/src/servala/frontend/templates/frontend/organizations/support.html new file mode 100644 index 0000000..75618a6 --- /dev/null +++ b/src/servala/frontend/templates/frontend/organizations/support.html @@ -0,0 +1,43 @@ +{% extends "frontend/base.html" %} +{% load i18n %} +{% block html_title %} + {% block page_title %} + {% translate "Support" %} + {% endblock page_title %} +{% endblock html_title %} +{% block content %} +
+
+
+
+
+
+
{% translate "Get Support" %}
+

+ {% translate "Need help? Submit your question or issue below and our support team will get back to you." %} +

+
+ {% csrf_token %} + {% translate "Submit Support Request" as form_submit_label %} + {% include "includes/form.html" with form=form %} +
+
+
+
+
+
+
+
+

{% translate "Support Information" %}

+
+
+

+ {% translate "When you submit a support request, it will be sent to our support team who will respond via email." %} +

+

{% translate "For urgent issues, please contact us directly." %}

+
+
+
+
+
+{% endblock content %} diff --git a/src/servala/frontend/templates/includes/menu.html b/src/servala/frontend/templates/includes/menu.html index f965fa0..c84214c 100644 --- a/src/servala/frontend/templates/includes/menu.html +++ b/src/servala/frontend/templates/includes/menu.html @@ -32,6 +32,13 @@ + diff --git a/src/servala/frontend/urls.py b/src/servala/frontend/urls.py index 490c9ce..2c801a4 100644 --- a/src/servala/frontend/urls.py +++ b/src/servala/frontend/urls.py @@ -61,6 +61,11 @@ urlpatterns = [ views.ServiceInstanceDeleteView.as_view(), name="organization.instance.delete", ), + path( + "support/", + views.SupportView.as_view(), + name="organization.support", + ), ] ), ), diff --git a/src/servala/frontend/views/__init__.py b/src/servala/frontend/views/__init__.py index 98b23a3..5ae01ec 100644 --- a/src/servala/frontend/views/__init__.py +++ b/src/servala/frontend/views/__init__.py @@ -14,6 +14,7 @@ from .service import ( ServiceListView, ServiceOfferingDetailView, ) +from .support import SupportView __all__ = [ "IndexView", @@ -29,6 +30,7 @@ __all__ = [ "ServiceListView", "ServiceOfferingDetailView", "ProfileView", + "SupportView", "custom_404", "custom_403", "custom_500", diff --git a/src/servala/frontend/views/support.py b/src/servala/frontend/views/support.py new file mode 100644 index 0000000..e8e29ce --- /dev/null +++ b/src/servala/frontend/views/support.py @@ -0,0 +1,50 @@ +from django.conf import settings +from django.contrib import messages +from django.shortcuts import redirect +from django.utils.translation import gettext_lazy as _ +from django.views.generic import FormView + +from servala.core.odoo import CLIENT +from servala.frontend.forms.support import SupportForm +from servala.frontend.views.mixins import OrganizationViewMixin + + +class SupportView(OrganizationViewMixin, FormView): + form_class = SupportForm + template_name = "frontend/organizations/support.html" + + def form_valid(self, form): + message = form.cleaned_data["message"] + organization = self.organization + user = self.request.user + + try: + partner_id = user.get_or_create_odoo_contact(organization) + if not partner_id: + raise Exception("Could not get or create Odoo contact for user") + + ticket_data = { + "name": f"Servala Support - Organization {organization.name}", + "team_id": settings.ODOO["HELPDESK_TEAM_ID"], + "partner_id": partner_id, + "sale_order_id": organization.odoo_sale_order_id, + "description": message, + } + CLIENT.execute("helpdesk.ticket", "create", [ticket_data]) + messages.success( + self.request, + _( + "Your support request has been submitted successfully. We will contact you shortly." + ), + ) + + except Exception as e: + messages.error( + self.request, + _( + "There was an error submitting your support request. Please try again or contact us directly." + ), + ) + print(f"Error creating helpdesk ticket: {e}") + + return redirect(self.request.path) diff --git a/src/servala/settings.py b/src/servala/settings.py index 8322e4a..ed28324 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -131,6 +131,7 @@ ODOO = { "DB": os.environ.get("SERVALA_ODOO_DB"), "USERNAME": os.environ.get("SERVALA_ODOO_USERNAME"), "PASSWORD": os.environ.get("SERVALA_ODOO_PASSWORD"), + "HELPDESK_TEAM_ID": int(os.environ.get("SERVALA_ODOO_HELPDESK_TEAM_ID", "5")), } ####################################### From 4ccefd1c7f4a2f931c3cfa5124fe3d7866a16d52 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Sun, 22 Jun 2025 19:53:55 +0200 Subject: [PATCH 2/4] Fall back in case of missing sale order ID --- src/servala/frontend/views/support.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/servala/frontend/views/support.py b/src/servala/frontend/views/support.py index e8e29ce..e7ed763 100644 --- a/src/servala/frontend/views/support.py +++ b/src/servala/frontend/views/support.py @@ -27,9 +27,16 @@ class SupportView(OrganizationViewMixin, FormView): "name": f"Servala Support - Organization {organization.name}", "team_id": settings.ODOO["HELPDESK_TEAM_ID"], "partner_id": partner_id, - "sale_order_id": organization.odoo_sale_order_id, "description": message, } + + # All orgs should have a sale order ID, but legacy ones might not have it. + # Also, we want to be very sure that support requests work, especially for + # organizations where something in the creation process may have gone wrong, + # so if the ID does not exist, we omit it entirely. + if organization.odoo_sale_order_id: + ticket_data["sale_order_id"] = organization.odoo_sale_order_id + CLIENT.execute("helpdesk.ticket", "create", [ticket_data]) messages.success( self.request, From 357fea64d1ddd0a749af8f33b21d8b60abe1e8a0 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 23 Jun 2025 11:33:15 +0200 Subject: [PATCH 3/4] Code style --- src/servala/frontend/templates/403.html | 20 +++++++++++-------- src/servala/frontend/templates/404.html | 20 +++++++++++-------- src/servala/frontend/templates/500.html | 20 +++++++++++-------- .../frontend/templates/error_base.html | 20 +++++++++++++++---- 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/servala/frontend/templates/403.html b/src/servala/frontend/templates/403.html index 74200cf..0bc271b 100644 --- a/src/servala/frontend/templates/403.html +++ b/src/servala/frontend/templates/403.html @@ -1,9 +1,13 @@ {% extends "error_base.html" %} - -{% block title %}403 - Access Forbidden{% endblock title %} - -{% block error_alt %}Access Forbidden{% endblock error_alt %} - -{% block error_title %}Access Forbidden{% endblock error_title %} - -{% block error_message %}You are not authorized to access this page.{% endblock error_message %} \ No newline at end of file +{% block title %} + 403 - Access Forbidden +{% endblock title %} +{% block error_alt %} + Access Forbidden +{% endblock error_alt %} +{% block error_title %} + Access Forbidden +{% endblock error_title %} +{% block error_message %} + You are not authorized to access this page. +{% endblock error_message %} diff --git a/src/servala/frontend/templates/404.html b/src/servala/frontend/templates/404.html index 6de2a23..4c8f896 100644 --- a/src/servala/frontend/templates/404.html +++ b/src/servala/frontend/templates/404.html @@ -1,9 +1,13 @@ {% extends "error_base.html" %} - -{% block title %}404 - Page Not Found{% endblock title %} - -{% block error_alt %}Not Found{% endblock error_alt %} - -{% block error_title %}Page Not Found{% endblock error_title %} - -{% block error_message %}The page you are looking for could not be found.{% endblock error_message %} \ No newline at end of file +{% block title %} + 404 - Page Not Found +{% endblock title %} +{% block error_alt %} + Not Found +{% endblock error_alt %} +{% block error_title %} + Page Not Found +{% endblock error_title %} +{% block error_message %} + The page you are looking for could not be found. +{% endblock error_message %} diff --git a/src/servala/frontend/templates/500.html b/src/servala/frontend/templates/500.html index 7e42311..2e102a1 100644 --- a/src/servala/frontend/templates/500.html +++ b/src/servala/frontend/templates/500.html @@ -1,9 +1,13 @@ {% extends "error_base.html" %} - -{% block title %}500 - Server Error{% endblock title %} - -{% block error_alt %}Server Error{% endblock error_alt %} - -{% block error_title %}Server Error{% endblock error_title %} - -{% block error_message %}The website is currently unavailable. Please try again later or contact support.{% endblock error_message %} \ No newline at end of file +{% block title %} + 500 - Server Error +{% endblock title %} +{% block error_alt %} + Server Error +{% endblock error_alt %} +{% block error_title %} + Server Error +{% endblock error_title %} +{% block error_message %} + The website is currently unavailable. Please try again later or contact support. +{% endblock error_message %} diff --git a/src/servala/frontend/templates/error_base.html b/src/servala/frontend/templates/error_base.html index 1f05b3e..0097bee 100644 --- a/src/servala/frontend/templates/error_base.html +++ b/src/servala/frontend/templates/error_base.html @@ -4,7 +4,11 @@ - {% block title %}Error{% endblock title %} - Servala + + {% block title %} + Error + {% endblock title %} + - Servala @@ -22,12 +26,20 @@ alt="Sir Vala - {% block error_alt %}Error{% endblock error_alt %}" style="max-width: 300px; margin-bottom: 2rem"> -

{% block error_title %}Error{% endblock error_title %}

-

{% block error_message %}An error occurred.{% endblock error_message %}

+

+ {% block error_title %} + Error + {% endblock error_title %} +

+

+ {% block error_message %} + An error occurred. + {% endblock error_message %} +

Go Home - \ No newline at end of file + From 55c64d74ff4eb2b49bbb515da4c5cf0b60d0614b Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 23 Jun 2025 11:33:21 +0200 Subject: [PATCH 4/4] Make error views only available in dev mode --- src/servala/frontend/urls.py | 15 --------------- src/servala/urls.py | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/servala/frontend/urls.py b/src/servala/frontend/urls.py index 2c801a4..52a048e 100644 --- a/src/servala/frontend/urls.py +++ b/src/servala/frontend/urls.py @@ -1,4 +1,3 @@ -from django.conf import settings from django.urls import include, path from django.views.generic import RedirectView @@ -70,18 +69,4 @@ urlpatterns = [ ), ), path("", RedirectView.as_view(pattern_name="frontend:profile"), name="index"), - # Error page URLs available in all environments - path( - "error404/", - views.custom_404, - {"exception": Exception("Test 404")}, - name="error_404", - ), - path( - "error403/", - views.custom_403, - {"exception": Exception("Test 403")}, - name="error_403", - ), - path("error500/", views.custom_500, name="error_500"), ] diff --git a/src/servala/urls.py b/src/servala/urls.py index ef23afb..43526c8 100644 --- a/src/servala/urls.py +++ b/src/servala/urls.py @@ -6,7 +6,7 @@ from django.urls import path from django.urls.conf import include from django.utils.translation import gettext_lazy as _ -from servala.frontend import urls +from servala.frontend import urls, views admin.site.site_title = _("Servala Admin") admin.site.site_header = _("Servala Management") @@ -26,6 +26,21 @@ urlpatterns = [ if settings.DEBUG: urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += [ + path( + "error404/", + views.custom_404, + {"exception": Exception("Test 404")}, + name="error_404", + ), + path( + "error403/", + views.custom_403, + {"exception": Exception("Test 403")}, + name="error_403", + ), + path("error500/", views.custom_500, name="error_500"), + ] # Custom error handlers handler404 = "servala.frontend.views.custom_404"