4 Commits

Author SHA1 Message Date
Stéphan Sainléger
923a4f16c4 [IMP] helpdesk_user_default_ticket_team: document behaviour and convert README to markdown
- Add docstrings to ``_compute_user_id``, ``create`` and
  ``_define_user_id`` and clarify the inline comments describing the
  team-leader fallback rules.
- Convert ``README.rst`` to ``README.md``, documenting usage, the
  graceful handling of partners without a linked user, and how to run
  the test suite.
2026-06-25 15:39:54 +02:00
Stéphan Sainléger
b58fb77f3a [ADD] helpdesk_user_default_ticket_team: add test coverage
Add a ``TransactionCase`` test suite covering the module behaviour:

- ``_define_user_id`` decision rules (no team, no current user, user not
  in team, user in team, team without leader).
- ``create`` auto-assignment (with/without partner, with/without default
  team, explicit team not overridden, partner without linked user, batch
  ``vals_list`` creation).
- ``_compute_user_id`` recomputation on team change.
- Full portal creation flow (team + project + user) and presence of the
  ``default_helpdesk_ticket_team_id`` field on ``res.users``.
2026-06-25 15:39:30 +02:00
Stéphan Sainléger
6b8906325e [FIX] helpdesk_user_default_ticket_team: handle partner without linked user
``create`` accessed ``partner.user_ids[0]`` which raised ``IndexError``
when the ticket's partner had no linked user (e.g. a plain contact).

Use ``partner.user_ids[:1]`` so a partner without a user yields an empty
recordset, which the following ``if not user`` guard handles gracefully,
leaving the ticket's team untouched.
2026-06-25 15:39:23 +02:00
Stéphan Sainléger
1bfe51109e [REF] helpdesk_user_default_ticket_team: extract user assignment into `_define_user_id`
Extract the team-leader assignment logic into a dedicated
``_define_user_id`` helper, shared between ``_compute_user_id`` and
``create``:

- ``_compute_user_id`` now delegates to ``_define_user_id`` instead of
  inlining the membership/leader checks.
- ``create`` also assigns ``user_id`` from the default team via the same
  helper, so a portal-created ticket gets the team leader assigned.

This centralises the decision (keep current user / fall back to team
leader) in a single, reusable place.
2026-06-25 15:39:12 +02:00
5 changed files with 555 additions and 80 deletions

View File

@@ -0,0 +1,92 @@
# helpdesk_user_default_ticket_team
Automate ticket team attribution when a ticket is created by a portal user.
This module extends the `helpdesk_mgmt` module by allowing to configure a
default helpdesk team per user. It provides:
- A new `default_helpdesk_ticket_team_id` field on the user form, located in
the Preferences tab.
- Automatic team assignment when a ticket is created:
- If the ticket has no team assigned and has a linked partner.
- The module looks up the user associated with the partner. A partner with
no linked user (e.g. a plain contact) is handled gracefully and left
untouched.
- If the user has a default helpdesk team configured, it is automatically
assigned to the ticket.
- If the assigned team has a default project, the ticket is also linked to
that project.
- Automatic user assignment when a team is selected:
- When a team is set or changed on a ticket, if the current assigned user
is not a member of that team, the team leader (`team_id.user_id`) is
automatically assigned instead.
- If the team has no leader configured, the assigned user is left unchanged
(to avoid creating unassigned tickets).
This is particularly useful for multi-company or multi-team environments where
portal users should always have their tickets routed to a specific team.
# Installation
Use Odoo normal module installation procedure to install
`helpdesk_user_default_ticket_team`.
This module depends on:
- `helpdesk_mgmt`: provides the base helpdesk ticket functionality.
- `helpdesk_mgmt_project`: provides the link between tickets and projects.
# Usage
1. Open a user form (Settings > Users & Companies > Users).
2. In the **Preferences** tab, set the **Default Helpdesk Team** field.
3. When that user (through its linked partner) creates a ticket without an
explicit team, the ticket is automatically routed to the configured team,
its default project, and the team leader.
# Testing
Automated tests live in `tests/test_helpdesk_ticket.py` and cover:
- The `_define_user_id` decision logic (no team, no current user, user not in
team, user in team, team without leader).
- The `create` auto-assignment (with/without partner, with/without default
team, explicit team not overridden, partner without linked user, batch
creation).
- The `_compute_user_id` recomputation when the team changes.
- The full portal creation flow (team + project + user) and the presence of
the `default_helpdesk_ticket_team_id` field on `res.users`.
Run them with:
```
odoo-bin -d <db> --test-enable --stop-after-init \
-i helpdesk_user_default_ticket_team
```
# Known issues / Roadmap
None yet.
# Bug Tracker
Bugs are tracked on [our issues website](https://git.elabore.coop/Elabore/helpdesk-tools/issues).
In case of trouble, please check there if your issue has already been
reported. If you spotted it first, help us smashing it by providing a
detailed and welcomed feedback.
# Credits
## Contributors
- Stéphan Sainléger - [Email](mailto:stephan.sainleger@elabore.coop)
## Funders
The development of this module has been financially supported by:
- Elabore (https://elabore.coop)
## Maintainer
This module is maintained by Elabore.

View File

@@ -1,75 +0,0 @@
=================================
helpdesk_user_default_ticket_team
=================================
Automate ticket team attribution when ticket created by portal user.
This module extends the ``helpdesk_mgmt`` module by allowing to configure a
default helpdesk team per user. It provides:
* A new ``default_helpdesk_ticket_team_id`` field on the user form, located in
the Preferences tab.
* Automatic team assignment when a ticket is created:
* If the ticket has no team assigned and has a linked partner.
* The module looks up the portal user associated with the partner.
* If the user has a default helpdesk team configured, it is automatically
assigned to the ticket.
* If the assigned team has a default project, the ticket is also linked to
that project.
* Automatic user assignment when a team is selected:
* When a team is set or changed on a ticket, if the current assigned user
is not a member of that team, the team leader (``team_id.user_id``) is
automatically assigned instead.
* If the team has no leader configured, the assigned user is left unchanged
(to avoid creating unassigned tickets).
This is particularly useful for multi-company or multi-team environments where
portal users should always have their tickets routed to a specific team.
Installation
============
Use Odoo normal module installation procedure to install
``helpdesk_user_default_ticket_team``.
This module depends on:
* ``helpdesk_mgmt``: provides the base helpdesk ticket functionality.
* ``helpdesk_mgmt_project``: provides the link between tickets and projects.
Known issues / Roadmap
======================
None yet.
Bug Tracker
===========
Bugs are tracked on `our issues website <https://git.elabore.coop/Elabore/helpdesk-tools/issues>`_. In case of
trouble, please check there if your issue has already been
reported. If you spotted it first, help us smashing it by providing a
detailed and welcomed feedback.
Credits
=======
Contributors
------------
* Stéphan Sainléger - `Email <mailto:stephan.sainleger@elabore.coop>`_
Funders
-------
The development of this module has been financially supported by:
* Elabore (https://elabore.coop)
Maintainer
----------
This module is maintained by Elabore.

View File

@@ -6,21 +6,41 @@ class HelpdeskTicket(models.Model):
@api.depends("team_id") @api.depends("team_id")
def _compute_user_id(self): def _compute_user_id(self):
"""Recompute the assigned user whenever the team changes.
Delegates to :meth:`_define_user_id` so that, when a team is set or
changed, the assigned user is realigned with that team (replaced by
the team leader if the current user does not belong to the team).
"""
for ticket in self: for ticket in self:
if ticket.team_id: ticket.user_id = self._define_user_id(
if ticket.user_id not in ticket.team_id.user_ids: ticket.team_id,
if ticket.team_id.user_id: ticket.user_id,
ticket.user_id = ticket.team_id.user_id )
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):
"""Auto-assign team, project and user for portal-created tickets.
When a ticket is created without an explicit team but with a partner,
the partner's linked user is looked up and, if that user has a default
helpdesk team configured, the ticket is routed accordingly:
* ``team_id`` is set to the user's default helpdesk team.
* ``project_id`` is set to the team's default project, if any.
* ``user_id`` is set to the team leader (via :meth:`_define_user_id`).
Tickets that already have a team, that have no partner, or whose
partner has no linked user / no default team are left untouched.
"""
for vals in vals_list: for vals in vals_list:
if not vals.get("team_id") and vals.get("partner_id"): if not vals.get("team_id") and vals.get("partner_id"):
# Find the user who creates the ticket # Find the user who creates the ticket
partner = self.env["res.partner"].browse(vals.get("partner_id")) partner = self.env["res.partner"].browse(vals.get("partner_id"))
if not partner: if not partner:
continue continue
user = self.env["res.users"].browse(partner.user_ids[0].id) # A partner may have no linked user (e.g. a plain contact)
user = partner.user_ids[:1]
if not user: if not user:
continue continue
@@ -35,4 +55,42 @@ class HelpdeskTicket(models.Model):
if team.default_project_id: if team.default_project_id:
vals["project_id"] = team.default_project_id.id vals["project_id"] = team.default_project_id.id
# Set the user_id to which the ticket is assigned
user_id = self._define_user_id(team, None)
if user_id:
vals["user_id"] = user_id.id
return super().create(vals_list) return super().create(vals_list)
def _define_user_id(self, team_id=None, ticket_user_id=None):
"""Determine which user should be assigned to a ticket for a team.
Decides whether the current ticket user must be kept or replaced by
the helpdesk team leader (``team_id.user_id``), according to the
following rules:
* No team -> keep ``ticket_user_id`` unchanged.
* No current user -> use the team leader.
* User not in team -> replace with the team leader.
* User is a team member-> keep ``ticket_user_id`` unchanged.
:param team_id: the ``helpdesk.ticket.team`` record (or falsy).
:param ticket_user_id: the currently assigned ``res.users`` (or falsy).
:return: the ``res.users`` record that should be assigned (possibly an
empty recordset when the team has no leader and the current user
is not a member of the team).
"""
if not team_id:
# If no team, return the current ticket_user_id
return ticket_user_id
if not ticket_user_id:
# If no current user, fall back to the team leader (may be empty).
return team_id.user_id
if ticket_user_id not in team_id.user_ids:
# If the current user is not a team member, replace it with the
# team leader (may be empty).
return team_id.user_id
# Otherwise the current user is a valid team member: keep it unchanged.
return ticket_user_id

View File

@@ -0,0 +1 @@
from . import test_helpdesk_ticket

View File

@@ -0,0 +1,399 @@
# Copyright 2025 Stéphan Sainléger (Elabore)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests.common import TransactionCase
class TestHelpdeskUserDefaultTicketTeam(TransactionCase):
"""Tests for helpdesk_user_default_ticket_team module."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
# -----------------------------------------------------------------
# Create users
# -----------------------------------------------------------------
cls.team_leader = cls.env["res.users"].create(
{
"name": "Team Leader",
"login": "team_leader",
"email": "leader@test.com",
}
)
cls.team_member = cls.env["res.users"].create(
{
"name": "Team Member",
"login": "team_member",
"email": "member@test.com",
}
)
cls.portal_user = cls.env["res.users"].create(
{
"name": "Portal User",
"login": "portal_user",
"email": "portal@test.com",
}
)
cls.user_no_team = cls.env["res.users"].create(
{
"name": "No Team User",
"login": "no_team_user",
"email": "noteam@test.com",
}
)
# -----------------------------------------------------------------
# Create a project (needed by helpdesk_mgmt_project)
# -----------------------------------------------------------------
cls.project = cls.env["project.project"].create(
{"name": "Test Project"}
)
# -----------------------------------------------------------------
# Create helpdesk teams
# -----------------------------------------------------------------
cls.team_with_leader = cls.env["helpdesk.ticket.team"].create(
{
"name": "Team With Leader",
"user_id": cls.team_leader.id,
"user_ids": [
(6, 0, [cls.team_leader.id, cls.team_member.id])
],
}
)
cls.team_no_leader = cls.env["helpdesk.ticket.team"].create(
{
"name": "Team No Leader",
"user_ids": [(6, 0, [cls.team_member.id])],
}
)
cls.team_with_project = cls.env["helpdesk.ticket.team"].create(
{
"name": "Team With Project",
"user_id": cls.team_leader.id,
"user_ids": [(6, 0, [cls.team_leader.id])],
"default_project_id": cls.project.id,
}
)
cls.empty_team = cls.env["helpdesk.ticket.team"].create(
{"name": "Empty Team"}
)
# -----------------------------------------------------------------
# Assign default teams to users
# -----------------------------------------------------------------
cls.portal_user.write(
{"default_helpdesk_ticket_team_id": cls.team_with_leader.id}
)
# -----------------------------------------------------------------
# Create a partner linked to the portal user
# -----------------------------------------------------------------
cls.portal_partner = cls.env["res.partner"].create(
{
"name": "Portal Partner",
"email": "portal@test.com",
}
)
cls.portal_user.write({"partner_id": cls.portal_partner.id})
# Partner for user without default team
cls.no_team_partner = cls.env["res.partner"].create(
{
"name": "No Team Partner",
"email": "noteam@test.com",
}
)
cls.user_no_team.write({"partner_id": cls.no_team_partner.id})
# Partner without any linked user
cls.orphan_partner = cls.env["res.partner"].create(
{"name": "Orphan Partner", "email": "orphan@test.com"}
)
# Convenience models
cls.HelpdeskTicket = cls.env["helpdesk.ticket"]
cls.HelpdeskTeam = cls.env["helpdesk.ticket.team"]
# -----------------------------------------------------------------
# _define_user_id tests
# -----------------------------------------------------------------
def test_define_user_id_no_team(self):
"""Without a team, _define_user_id returns the current user_id unchanged."""
ticket = self.HelpdeskTicket.create(
{"name": "No team ticket", "description": "test"}
)
result = ticket._define_user_id(team_id=None, ticket_user_id=self.team_member)
self.assertEqual(result, self.team_member)
def test_define_user_id_no_ticket_user(self):
"""With a team but no current user, returns the team leader."""
ticket = self.HelpdeskTicket.create(
{"name": "Test ticket", "description": "test"}
)
result = ticket._define_user_id(
team_id=self.team_with_leader, ticket_user_id=None
)
self.assertEqual(result, self.team_leader)
def test_define_user_id_user_not_in_team(self):
"""When the current user is not in the team, returns the team leader."""
ticket = self.HelpdeskTicket.create(
{"name": "Test ticket", "description": "test"}
)
# team_with_leader has team_leader and team_member; user_no_team is not in it
result = ticket._define_user_id(
team_id=self.team_with_leader, ticket_user_id=self.user_no_team
)
self.assertEqual(result, self.team_leader)
def test_define_user_id_user_in_team(self):
"""When the current user is a team member, they are kept unchanged."""
ticket = self.HelpdeskTicket.create(
{"name": "Test ticket", "description": "test"}
)
result = ticket._define_user_id(
team_id=self.team_with_leader, ticket_user_id=self.team_member
)
self.assertEqual(result, self.team_member)
def test_define_user_id_team_no_leader(self):
"""When the team has no leader, the current user_id is returned unchanged."""
ticket = self.HelpdeskTicket.create(
{"name": "Test ticket", "description": "test"}
)
# team_no_leader has no user_id (team leader)
result = ticket._define_user_id(
team_id=self.team_no_leader, ticket_user_id=self.team_member
)
self.assertEqual(result, self.team_member)
# -----------------------------------------------------------------
# create() auto-assignment tests
# -----------------------------------------------------------------
def test_create_with_partner_default_team(self):
"""Creating a ticket with a partner whose user has a default team
automatically sets the team_id."""
ticket = self.HelpdeskTicket.create(
{
"name": "Auto team ticket",
"description": "test",
"partner_id": self.portal_partner.id,
}
)
self.assertEqual(
ticket.team_id,
self.team_with_leader,
"Team should be auto-assigned from the portal user's default team",
)
def test_create_with_partner_default_team_and_project(self):
"""When the default team has a default_project_id, the ticket gets it."""
# Give portal_user a default team that has a project
self.portal_user.write(
{"default_helpdesk_ticket_team_id": self.team_with_project.id}
)
ticket = self.HelpdeskTicket.create(
{
"name": "Auto team+project ticket",
"description": "test",
"partner_id": self.portal_partner.id,
}
)
self.assertEqual(ticket.team_id, self.team_with_project)
self.assertEqual(
ticket.project_id,
self.project,
"Project should be auto-assigned from the team's default project",
)
# Reset portal_user default team for other tests
self.portal_user.write(
{"default_helpdesk_ticket_team_id": self.team_with_leader.id}
)
def test_create_with_partner_no_default_team(self):
"""Creating a ticket with a partner whose user has no default team
does not set a team."""
ticket = self.HelpdeskTicket.create(
{
"name": "No default team ticket",
"description": "test",
"partner_id": self.no_team_partner.id,
}
)
self.assertFalse(
ticket.team_id,
"Team should not be set if the user has no default team",
)
def test_create_with_explicit_team(self):
"""When a team_id is explicitly provided, it is not overridden."""
ticket = self.HelpdeskTicket.create(
{
"name": "Explicit team ticket",
"description": "test",
"partner_id": self.portal_partner.id,
"team_id": self.team_no_leader.id,
}
)
self.assertEqual(
ticket.team_id,
self.team_no_leader,
"Explicitly provided team should not be overridden",
)
def test_create_without_partner(self):
"""Creating a ticket without a partner does not trigger auto-assignment."""
ticket = self.HelpdeskTicket.create(
{"name": "No partner ticket", "description": "test"}
)
self.assertFalse(
ticket.team_id,
"Team should not be auto-assigned when there is no partner",
)
def test_create_partner_without_user(self):
"""Creating a ticket with a partner that has no linked user
does not crash and does not set a team."""
ticket = self.HelpdeskTicket.create(
{
"name": "Orphan partner ticket",
"description": "test",
"partner_id": self.orphan_partner.id,
}
)
self.assertFalse(
ticket.team_id,
"Team should not be set when partner has no linked user",
)
def test_create_multi_vals_list(self):
"""create() with a vals_list processes each ticket independently."""
tickets = self.HelpdeskTicket.create(
[
{
"name": "Batch ticket 1",
"description": "test",
"partner_id": self.portal_partner.id,
},
{
"name": "Batch ticket 2",
"description": "test",
"partner_id": self.no_team_partner.id,
},
{
"name": "Batch ticket 3",
"description": "test",
},
]
)
self.assertEqual(len(tickets), 3)
# First ticket: portal user with default team
self.assertEqual(
tickets[0].team_id,
self.team_with_leader,
"First batch ticket should get the default team",
)
# Second ticket: user without default team
self.assertFalse(
tickets[1].team_id,
"Second batch ticket should have no team",
)
# Third ticket: no partner at all
self.assertFalse(
tickets[2].team_id,
"Third batch ticket should have no team",
)
# -----------------------------------------------------------------
# _compute_user_id tests (depends on team_id)
# -----------------------------------------------------------------
def test_compute_user_id_team_change_replaces(self):
"""When team_id changes to a team where the current user is not a member,
user_id is replaced by the new team's leader."""
ticket = self.HelpdeskTicket.create(
{
"name": "Team change ticket",
"description": "test",
"user_id": self.user_no_team.id,
}
)
# user_no_team is not in team_with_leader members
ticket.team_id = self.team_with_leader
ticket._compute_user_id()
self.assertEqual(
ticket.user_id,
self.team_leader,
"User should be replaced by the new team's leader",
)
def test_compute_user_id_team_change_kept(self):
"""When team_id changes to a team where the current user is a member,
user_id is kept unchanged."""
ticket = self.HelpdeskTicket.create(
{
"name": "Team kept ticket",
"description": "test",
"user_id": self.team_member.id,
}
)
# team_member is in team_with_leader members
ticket.team_id = self.team_with_leader
ticket._compute_user_id()
self.assertEqual(
ticket.user_id,
self.team_member,
"User should stay unchanged when they belong to the new team",
)
# -----------------------------------------------------------------
# Integration test
# -----------------------------------------------------------------
def test_full_creation_flow(self):
"""Full flow: portal user creates a ticket → default team + project + user
are all set correctly."""
self.portal_user.write(
{"default_helpdesk_ticket_team_id": self.team_with_project.id}
)
ticket = self.HelpdeskTicket.create(
{
"name": "Full flow ticket",
"description": "test",
"partner_id": self.portal_partner.id,
}
)
# Team auto-assigned from portal user's default
self.assertEqual(ticket.team_id, self.team_with_project)
# Project auto-assigned from team's default project
self.assertEqual(ticket.project_id, self.project)
# User auto-assigned to team leader (since no user was provided)
self.assertEqual(ticket.user_id, self.team_leader)
# Reset
self.portal_user.write(
{"default_helpdesk_ticket_team_id": self.team_with_leader.id}
)
# -----------------------------------------------------------------
# res.users field test
# -----------------------------------------------------------------
def test_user_default_team_field(self):
"""The default_helpdesk_ticket_team_id field exists on res.users."""
self.assertIn(
"default_helpdesk_ticket_team_id",
self.env["res.users"]._fields,
"Field default_helpdesk_ticket_team_id must exist on res.users",
)
self.assertEqual(
self.portal_user.default_helpdesk_ticket_team_id,
self.team_with_leader,
)
self.assertFalse(
self.user_no_team.default_helpdesk_ticket_team_id,
)