Support form #115

Merged
rixx merged 4 commits from 53-support-form into main 2025-06-23 09:34:42 +00:00
16 changed files with 219 additions and 45 deletions

View file

@ -66,3 +66,5 @@ SERVALA_ODOO_DB=''
SERVALA_ODOO_URL='' SERVALA_ODOO_URL=''
SERVALA_ODOO_USERNAME='' SERVALA_ODOO_USERNAME=''
SERVALA_ODOO_PASSWORD='' SERVALA_ODOO_PASSWORD=''
# Helpdesk team ID for support tickets in Odoo. Defaults to 5.
SERVALA_ODOO_HELPDESK_TEAM_ID='5'

View file

@ -58,6 +58,7 @@ class Organization(ServalaModelMixin, models.Model):
details = "{base}details/" details = "{base}details/"
services = "{base}services/" services = "{base}services/"
instances = "{base}instances/" instances = "{base}instances/"
support = "{base}support/"
@cached_property @cached_property
def slug(self): def slug(self):

View file

@ -93,3 +93,24 @@ class User(ServalaModelMixin, PermissionsMixin, AbstractBaseUser):
) )
if result: if result:
return result[0] 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

View file

@ -1,4 +1,5 @@
from .organization import OrganizationForm from .organization import OrganizationForm
from .profile import UserProfileForm from .profile import UserProfileForm
from .support import SupportForm
__all__ = ["OrganizationForm", "UserProfileForm"] __all__ = ["OrganizationForm", "UserProfileForm", "SupportForm"]

View file

@ -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."),
)

View file

@ -1,9 +1,13 @@
{% extends "error_base.html" %} {% extends "error_base.html" %}
{% block title %}
{% block title %}403 - Access Forbidden{% endblock title %} 403 - Access Forbidden
{% endblock title %}
{% block error_alt %}Access Forbidden{% endblock error_alt %} {% block error_alt %}
Access Forbidden
{% block error_title %}Access Forbidden{% endblock error_title %} {% endblock error_alt %}
{% block error_title %}
{% block error_message %}You are not authorized to access this page.{% endblock error_message %} Access Forbidden
{% endblock error_title %}
{% block error_message %}
You are not authorized to access this page.
{% endblock error_message %}

View file

@ -1,9 +1,13 @@
{% extends "error_base.html" %} {% extends "error_base.html" %}
{% block title %}
{% block title %}404 - Page Not Found{% endblock title %} 404 - Page Not Found
{% endblock title %}
{% block error_alt %}Not Found{% endblock error_alt %} {% block error_alt %}
Not Found
{% block error_title %}Page Not Found{% endblock error_title %} {% endblock error_alt %}
{% block error_title %}
{% block error_message %}The page you are looking for could not be found.{% endblock error_message %} Page Not Found
{% endblock error_title %}
{% block error_message %}
The page you are looking for could not be found.
{% endblock error_message %}

View file

@ -1,9 +1,13 @@
{% extends "error_base.html" %} {% extends "error_base.html" %}
{% block title %}
{% block title %}500 - Server Error{% endblock title %} 500 - Server Error
{% endblock title %}
{% block error_alt %}Server Error{% endblock error_alt %} {% block error_alt %}
Server Error
{% block error_title %}Server Error{% endblock error_title %} {% endblock error_alt %}
{% block error_title %}
{% block error_message %}The website is currently unavailable. Please try again later or contact support.{% endblock error_message %} Server Error
{% endblock error_title %}
{% block error_message %}
The website is currently unavailable. Please try again later or contact support.
{% endblock error_message %}

View file

@ -4,7 +4,11 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Error{% endblock title %} - Servala</title> <title>
{% block title %}
Error
{% endblock title %}
- Servala</title>
<link rel="shortcut icon" <link rel="shortcut icon"
href="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2033%2034'%20fill-rule='evenodd'%20stroke-linejoin='round'%20stroke-miterlimit='2'%20xmlns:v='https://vecta.io/nano'%3e%3cpath%20d='M3%2027.472c0%204.409%206.18%205.552%2013.5%205.552%207.281%200%2013.5-1.103%2013.5-5.513s-6.179-5.552-13.5-5.552c-7.281%200-13.5%201.103-13.5%205.513z'%20fill='%23435ebe'%20fill-rule='nonzero'/%3e%3ccircle%20cx='16.5'%20cy='8.8'%20r='8.8'%20fill='%2341bbdd'/%3e%3c/svg%3e" href="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2033%2034'%20fill-rule='evenodd'%20stroke-linejoin='round'%20stroke-miterlimit='2'%20xmlns:v='https://vecta.io/nano'%3e%3cpath%20d='M3%2027.472c0%204.409%206.18%205.552%2013.5%205.552%207.281%200%2013.5-1.103%2013.5-5.513s-6.179-5.552-13.5-5.552c-7.281%200-13.5%201.103-13.5%205.513z'%20fill='%23435ebe'%20fill-rule='nonzero'/%3e%3ccircle%20cx='16.5'%20cy='8.8'%20r='8.8'%20fill='%2341bbdd'/%3e%3c/svg%3e"
type="image/x-icon"> type="image/x-icon">
@ -22,12 +26,20 @@
alt="Sir Vala - {% block error_alt %}Error{% endblock error_alt %}" alt="Sir Vala - {% block error_alt %}Error{% endblock error_alt %}"
style="max-width: 300px; style="max-width: 300px;
margin-bottom: 2rem"> margin-bottom: 2rem">
<h1 class="error-title">{% block error_title %}Error{% endblock error_title %}</h1> <h1 class="error-title">
<p class="fs-5 text-gray-600">{% block error_message %}An error occurred.{% endblock error_message %}</p> {% block error_title %}
Error
{% endblock error_title %}
</h1>
<p class="fs-5 text-gray-600">
{% block error_message %}
An error occurred.
{% endblock error_message %}
</p>
<a href="/" class="btn btn-lg btn-outline-primary mt-3">Go Home</a> <a href="/" class="btn btn-lg btn-outline-primary mt-3">Go Home</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View file

@ -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 %}
<section class="section">
<div class="row">
<div class="col-md-9">
<div class="card">
<div class="card-content">
<div class="card-body">
<h5>{% translate "Get Support" %}</h5>
<p class="text-muted">
{% translate "Need help? Submit your question or issue below and our support team will get back to you." %}
</p>
<form method="post">
{% csrf_token %}
{% translate "Submit Support Request" as form_submit_label %}
{% include "includes/form.html" with form=form %}
</form>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h4>{% translate "Support Information" %}</h4>
</div>
<div class="card-body">
<p class="small text-muted">
{% translate "When you submit a support request, it will be sent to our support team who will respond via email." %}
</p>
<p class="small text-muted">{% translate "For urgent issues, please contact us directly." %}</p>
</div>
</div>
</div>
</div>
</section>
{% endblock content %}

View file

@ -32,6 +32,13 @@
</span> </span>
</a> </a>
</li> </li>
<li class="menu-item">
<a href="{{ request.organization.urls.support }}" class='menu-link'>
<span>
<i class="bi bi-question-circle"></i>
{% translate "Support" %}</span>
</a>
</li>
</ul> </ul>
</div> </div>
</nav> </nav>

View file

@ -1,4 +1,3 @@
from django.conf import settings
from django.urls import include, path from django.urls import include, path
from django.views.generic import RedirectView from django.views.generic import RedirectView
@ -61,22 +60,13 @@ urlpatterns = [
views.ServiceInstanceDeleteView.as_view(), views.ServiceInstanceDeleteView.as_view(),
name="organization.instance.delete", name="organization.instance.delete",
), ),
path(
"support/",
views.SupportView.as_view(),
name="organization.support",
),
] ]
), ),
), ),
path("", RedirectView.as_view(pattern_name="frontend:profile"), name="index"), 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"),
] ]

View file

@ -14,6 +14,7 @@ from .service import (
ServiceListView, ServiceListView,
ServiceOfferingDetailView, ServiceOfferingDetailView,
) )
from .support import SupportView
__all__ = [ __all__ = [
"IndexView", "IndexView",
@ -29,6 +30,7 @@ __all__ = [
"ServiceListView", "ServiceListView",
"ServiceOfferingDetailView", "ServiceOfferingDetailView",
"ProfileView", "ProfileView",
"SupportView",
"custom_404", "custom_404",
"custom_403", "custom_403",
"custom_500", "custom_500",

View file

@ -0,0 +1,57 @@
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,
"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,
_(
"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)

View file

@ -131,6 +131,7 @@ ODOO = {
"DB": os.environ.get("SERVALA_ODOO_DB"), "DB": os.environ.get("SERVALA_ODOO_DB"),
"USERNAME": os.environ.get("SERVALA_ODOO_USERNAME"), "USERNAME": os.environ.get("SERVALA_ODOO_USERNAME"),
"PASSWORD": os.environ.get("SERVALA_ODOO_PASSWORD"), "PASSWORD": os.environ.get("SERVALA_ODOO_PASSWORD"),
"HELPDESK_TEAM_ID": int(os.environ.get("SERVALA_ODOO_HELPDESK_TEAM_ID", "5")),
} }
####################################### #######################################

View file

@ -6,7 +6,7 @@ from django.urls import path
from django.urls.conf import include from django.urls.conf import include
from django.utils.translation import gettext_lazy as _ 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_title = _("Servala Admin")
admin.site.site_header = _("Servala Management") admin.site.site_header = _("Servala Management")
@ -26,6 +26,21 @@ urlpatterns = [
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_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 # Custom error handlers
handler404 = "servala.frontend.views.custom_404" handler404 = "servala.frontend.views.custom_404"