diff --git a/helpdesk_mgmt/controllers/main.py b/helpdesk_mgmt/controllers/main.py
index f043bf930d..924d03e3f0 100644
--- a/helpdesk_mgmt/controllers/main.py
+++ b/helpdesk_mgmt/controllers/main.py
@@ -25,7 +25,9 @@ def support_ticket_close(self, **kw):
.sudo()
.search([("id", "=", values["ticket_id"])])
)
- ticket.stage_id = values.get("stage_id")
+ stage = http.request.env["helpdesk.ticket.stage"].browse(values.get("stage_id"))
+ if stage.close_from_portal: # protect against invalid target stage request
+ ticket.stage_id = values.get("stage_id")
return werkzeug.utils.redirect("/my/ticket/" + str(ticket.id))
diff --git a/helpdesk_mgmt/controllers/myaccount.py b/helpdesk_mgmt/controllers/myaccount.py
index c51cc67120..74e0e674c0 100644
--- a/helpdesk_mgmt/controllers/myaccount.py
+++ b/helpdesk_mgmt/controllers/myaccount.py
@@ -182,7 +182,7 @@ def portal_my_ticket(self, ticket_id, access_token=None, **kw):
def _ticket_get_page_view_values(self, ticket, access_token, **kwargs):
closed_stages = request.env["helpdesk.ticket.stage"].search(
- [("closed", "=", True)]
+ [("close_from_portal", "=", True)]
)
files = (
request.env["ir.attachment"]
diff --git a/helpdesk_mgmt/data/helpdesk_data.xml b/helpdesk_mgmt/data/helpdesk_data.xml
index 5fc5aad88d..760aa670f2 100644
--- a/helpdesk_mgmt/data/helpdesk_data.xml
+++ b/helpdesk_mgmt/data/helpdesk_data.xml
@@ -118,6 +118,7 @@
Done
False
True
+ True
True
@@ -127,6 +128,17 @@
Cancelled
False
True
+ True
+ True
+
+
+
+
+ 6
+ Rejected
+ False
+ True
+ False
True
diff --git a/helpdesk_mgmt/models/helpdesk_ticket_stage.py b/helpdesk_mgmt/models/helpdesk_ticket_stage.py
index 96255c72f2..2a31fabae5 100644
--- a/helpdesk_mgmt/models/helpdesk_ticket_stage.py
+++ b/helpdesk_mgmt/models/helpdesk_ticket_stage.py
@@ -1,4 +1,4 @@
-from odoo import fields, models
+from odoo import api, fields, models
class HelpdeskTicketStage(models.Model):
@@ -12,6 +12,10 @@ class HelpdeskTicketStage(models.Model):
active = fields.Boolean(default=True)
unattended = fields.Boolean(string="Unattended")
closed = fields.Boolean(string="Closed")
+ close_from_portal = fields.Boolean(
+ help="Display button in portal ticket form to allow closing ticket "
+ "with this stage as target."
+ )
mail_template_id = fields.Many2one(
comodel_name="mail.template",
string="Email Template",
@@ -31,3 +35,8 @@ class HelpdeskTicketStage(models.Model):
string="Company",
default=lambda self: self.env.company,
)
+
+ @api.onchange("closed")
+ def _onchange_closed(self):
+ if not self.closed:
+ self.close_from_portal = False
diff --git a/helpdesk_mgmt/tests/test_helpdesk_portal.py b/helpdesk_mgmt/tests/test_helpdesk_portal.py
index 4509ec8483..ef7692e478 100644
--- a/helpdesk_mgmt/tests/test_helpdesk_portal.py
+++ b/helpdesk_mgmt/tests/test_helpdesk_portal.py
@@ -69,6 +69,26 @@ def _submit_ticket(self, files=None, **values):
resp = self.url_open("/submitted/ticket", data=data, files=files)
self.assertEqual(resp.status_code, 200)
+ def _count_close_buttons(self, resp) -> int:
+ """Count close buttons in a form by counting forms with that target."""
+ return resp.text.count('action="/ticket/close"')
+
+ def _call_close_ticket(self, ticket, stage):
+ """Call /ticket/close with the specified target stage, check redirect."""
+ resp = self.url_open(
+ "/ticket/close",
+ data={
+ "csrf_token": http.WebRequest.csrf_token(self),
+ "stage_id": stage.id,
+ "ticket_id": ticket.id,
+ },
+ allow_redirects=False,
+ )
+ self.assertEqual(resp.status_code, 302)
+ self.assertTrue(resp.is_redirect) # http://127.0.0.1:8069/my/ticket/
+ self.assertTrue(resp.headers["Location"].endswith(f"/my/ticket/{ticket.id}"))
+ return resp
+
@odoo.tests.tagged("post_install", "-at_install")
class TestHelpdeskPortal(TestHelpdeskPortalBase):
@@ -124,3 +144,24 @@ def test_submit_ticket_with_attachments(self):
# check that both files are public (access_token is set)
self.assertTrue(attachment_ids[0].access_token)
self.assertTrue(attachment_ids[1].access_token)
+
+ def test_close_ticket(self):
+ """Close a ticket from the portal."""
+ self.assertFalse(self.portal_ticket.closed)
+ self.authenticate("test-portal", "test-portal")
+ resp = self.url_open(f"/my/ticket/{self.portal_ticket.id}")
+ self.assertEqual(self._count_close_buttons(resp), 2) # 2 close stages in data/
+ stage = self.env.ref("helpdesk_mgmt.helpdesk_ticket_stage_done")
+ self._call_close_ticket(self.portal_ticket, stage)
+ self.assertTrue(self.portal_ticket.closed)
+ self.assertEqual(self.portal_ticket.stage_id, stage)
+ resp = self.url_open(f"/my/ticket/{self.portal_ticket.id}")
+ self.assertEqual(self._count_close_buttons(resp), 0) # no close buttons now
+
+ def test_close_ticket_invalid_stage(self):
+ """Attempt to close a ticket from the portal with an invalid target stage."""
+ self.authenticate("test-portal", "test-portal")
+ stage = self.env.ref("helpdesk_mgmt.helpdesk_ticket_stage_awaiting")
+ self._call_close_ticket(self.portal_ticket, stage)
+ self.assertFalse(self.portal_ticket.closed)
+ self.assertNotEqual(self.portal_ticket.stage_id, stage)
diff --git a/helpdesk_mgmt/views/helpdesk_ticket_stage_views.xml b/helpdesk_mgmt/views/helpdesk_ticket_stage_views.xml
index aee2ae90ac..b71e9fe981 100644
--- a/helpdesk_mgmt/views/helpdesk_ticket_stage_views.xml
+++ b/helpdesk_mgmt/views/helpdesk_ticket_stage_views.xml
@@ -49,6 +49,10 @@
+