From f7232d08e806f70d6ad70479fbc9b651eaee3cb7 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 12 Nov 2025 11:52:54 +0100 Subject: [PATCH] Extract odoo helpdesk ticket creation --- src/servala/api/views.py | 18 +++----- src/servala/core/odoo.py | 16 +++++++ src/servala/frontend/views/support.py | 20 +++------ src/servala/settings_test.py | 1 + src/tests/test_api_exoscale.py | 61 +++++++++++++++++---------- 5 files changed, 70 insertions(+), 46 deletions(-) diff --git a/src/servala/api/views.py b/src/servala/api/views.py index c4857e5..7d8ebfa 100644 --- a/src/servala/api/views.py +++ b/src/servala/api/views.py @@ -21,7 +21,7 @@ from servala.core.models import ( User, ) from servala.core.models.service import Service, ServiceInstance, ServiceOffering -from servala.core.odoo import CLIENT +from servala.core.odoo import create_helpdesk_ticket logger = logging.getLogger(__name__) @@ -269,9 +269,7 @@ The Servala Team""" organization = None try: # Look for instances with this name in the service offering's context - # TODO: we do not currently match instance IDs from exoscale yet, this - # will likely not work at all yet - instances = ( + service_instance = ( ServiceInstance.objects.filter( name=instance_id, context__service_offering=service_offering, @@ -280,7 +278,7 @@ The Servala Team""" .first() ) - if instances: + if service_instance: organization = service_instance.organization except Exception: pass @@ -327,13 +325,11 @@ The Servala Team""" description_parts.append(f" - {full_name} ({user_link}) - {role}") description = "\n".join(description_parts) - ticket_data = { - "name": f"Exoscale OSB {action} - {service.name} - {instance_id}", - "team_id": settings.ODOO["HELPDESK_TEAM_ID"], - "description": description, - } - CLIENT.execute("helpdesk.ticket", "create", [ticket_data]) + create_helpdesk_ticket( + title=f"Exoscale OSB {action} - {service.name} - {instance_id}", + description=description, + ) logger.info( f"Created {action} helpdesk ticket for instance {instance_id}, service {service.name}" ) diff --git a/src/servala/core/odoo.py b/src/servala/core/odoo.py index ba91dc7..517829a 100644 --- a/src/servala/core/odoo.py +++ b/src/servala/core/odoo.py @@ -207,3 +207,19 @@ def get_invoice_addresses(user): return invoice_addresses or [] except Exception: return [] + + +def create_helpdesk_ticket(title, description, partner_id=None, sale_order_id=None): + ticket_data = { + "name": title, + "team_id": settings.ODOO["HELPDESK_TEAM_ID"], + "description": description, + } + + if partner_id: + ticket_data["partner_id"] = partner_id + + if sale_order_id: + ticket_data["sale_order_id"] = sale_order_id + + return CLIENT.execute("helpdesk.ticket", "create", [ticket_data]) diff --git a/src/servala/frontend/views/support.py b/src/servala/frontend/views/support.py index 6f4c4aa..2cd4cf3 100644 --- a/src/servala/frontend/views/support.py +++ b/src/servala/frontend/views/support.py @@ -1,11 +1,10 @@ -from django.conf import settings from django.contrib import messages from django.shortcuts import redirect from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView -from servala.core.odoo import CLIENT +from servala.core.odoo import create_helpdesk_ticket from servala.frontend.forms.support import SupportForm from servala.frontend.views.mixins import OrganizationViewMixin @@ -24,21 +23,16 @@ class SupportView(OrganizationViewMixin, FormView): 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]) + create_helpdesk_ticket( + title=f"Servala Support - Organization {organization.name}", + description=message, + partner_id=partner_id, + sale_order_id=organization.odoo_sale_order_id or None, + ) messages.success( self.request, _( diff --git a/src/servala/settings_test.py b/src/servala/settings_test.py index 477ecb2..fec1bfb 100644 --- a/src/servala/settings_test.py +++ b/src/servala/settings_test.py @@ -8,6 +8,7 @@ overrides/adds settings specific to testing. from servala.settings import * # noqa: F403, F401 SECRET_KEY = "test-secret-key-for-testing-only-do-not-use-in-production" +SALT_KEY = SECRET_KEY PASSWORD_HASHERS = [ "django.contrib.auth.hashers.MD5PasswordHasher", ] diff --git a/src/tests/test_api_exoscale.py b/src/tests/test_api_exoscale.py index 283e604..19f8b93 100644 --- a/src/tests/test_api_exoscale.py +++ b/src/tests/test_api_exoscale.py @@ -614,7 +614,8 @@ def test_delete_creates_ticket_with_admin_links( test_service_offering, instance_id, ): - mock_api_client = mocker.patch("servala.api.views.CLIENT") + # Mock the create_helpdesk_ticket function + mock_create_ticket = mocker.patch("servala.api.views.create_helpdesk_ticket") response = osb_client.delete( f"/api/osb/v2/service_instances/{instance_id}" @@ -623,13 +624,15 @@ def test_delete_creates_ticket_with_admin_links( assert response.status_code == 200 - ticket_call = mock_api_client.execute.call_args_list[-1] - ticket_data = ticket_call[0][2][0] + # Verify the ticket was created with admin URL + mock_create_ticket.assert_called_once() + call_kwargs = mock_create_ticket.call_args[1] - assert "admin/core/serviceoffering" in ticket_data["description"] - assert f"/{test_service_offering.pk}/" in ticket_data["description"] + # Check that the description contains an admin URL + assert "admin/core/serviceoffering" in call_kwargs["description"] + assert f"/{test_service_offering.pk}/" in call_kwargs["description"] assert ( - ticket_data["name"] + call_kwargs["title"] == f"Exoscale OSB Offboard - {test_service.name} - {instance_id}" ) @@ -644,7 +647,9 @@ def test_patch_creates_ticket_with_user_admin_links( instance_id, org_owner, ): - mock_api_client = mocker.patch("servala.api.views.CLIENT") + # Mock the create_helpdesk_ticket function + mock_create_ticket = mocker.patch("servala.api.views.create_helpdesk_ticket") + payload = { "service_id": test_service.osb_service_id, "plan_id": test_service_offering.osb_plan_id, @@ -666,13 +671,17 @@ def test_patch_creates_ticket_with_user_admin_links( ) assert response.status_code == 200 - ticket_call = mock_api_client.execute.call_args_list[-1] - ticket_data = ticket_call[0][2][0] - assert "admin/core/serviceoffering" in ticket_data["description"] - assert "admin/core/user" in ticket_data["description"] - assert f"/{org_owner.pk}/" in ticket_data["description"] + + # Verify the ticket was created with admin URLs + mock_create_ticket.assert_called_once() + call_kwargs = mock_create_ticket.call_args[1] + + # Check that the description contains admin URLs + assert "admin/core/serviceoffering" in call_kwargs["description"] + assert "admin/core/user" in call_kwargs["description"] + assert f"/{org_owner.pk}/" in call_kwargs["description"] assert ( - ticket_data["name"] + call_kwargs["title"] == f"Exoscale OSB Suspend - {test_service.name} - {instance_id}" ) @@ -686,7 +695,9 @@ def test_ticket_includes_organization_and_instance_when_found( test_service_offering, organization, ): - mock_api_client = mocker.patch("servala.api.views.CLIENT") + # Mock the create_helpdesk_ticket function + mock_create_ticket = mocker.patch("servala.api.views.create_helpdesk_ticket") + service_definition = ServiceDefinition.objects.create( name="Test Definition", service=test_service, @@ -719,11 +730,17 @@ def test_ticket_includes_organization_and_instance_when_found( ) assert response.status_code == 200 - ticket_call = mock_api_client.execute.call_args_list[-1] - ticket_data = ticket_call[0][2][0] - assert f"Organization: {organization.name}" in ticket_data["description"] - assert "admin/core/organization" in ticket_data["description"] - assert f"/{organization.pk}/" in ticket_data["description"] - assert f"Instance: {service_instance.name}" in ticket_data["description"] - assert "admin/core/serviceinstance" in ticket_data["description"] - assert f"/{service_instance.pk}/" in ticket_data["description"] + + # Verify the ticket was created with all admin URLs + mock_create_ticket.assert_called_once() + call_kwargs = mock_create_ticket.call_args[1] + + # Check organization is included + assert f"Organization: {organization.name}" in call_kwargs["description"] + assert "admin/core/organization" in call_kwargs["description"] + assert f"/{organization.pk}/" in call_kwargs["description"] + + # Check instance is included + assert f"Instance: {service_instance.name}" in call_kwargs["description"] + assert "admin/core/serviceinstance" in call_kwargs["description"] + assert f"/{service_instance.pk}/" in call_kwargs["description"]