diff --git a/.gitignore b/.gitignore index 0090721f..a6d0ee76 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ coverage.xml # Visual Studio cache/options directory .vs/ .vscode +**/odools.toml # OSX Files .DS_Store diff --git a/README.md b/README.md index 3c34ba5b..525bff5c 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ addon | version | maintainers | summary [g2p_proxy_means_test](g2p_proxy_means_test/) | 17.0.1.3.0 | | G2P: Proxy Means Test [g2p_reimbursement_portal](g2p_reimbursement_portal/) | 17.0.1.3.0 | | G2P Reimbursement Portal [g2p_social_registry_importer](g2p_social_registry_importer/) | 17.0.1.3.0 | | Import records from Social Registry +[g2p_support_desk](g2p_support_desk/) | 17.0.1.3.0 | | OpenG2P Support Desk Management System [g2p_theme](g2p_theme/) | 17.0.1.3.0 | | OpenG2P Theme diff --git a/g2p_bridge_configuration/models/payment_manager.py b/g2p_bridge_configuration/models/payment_manager.py index 84f69d29..a1bd6cf3 100644 --- a/g2p_bridge_configuration/models/payment_manager.py +++ b/g2p_bridge_configuration/models/payment_manager.py @@ -46,7 +46,9 @@ def publish_bridge_benefit_program(self): }, } - token = self.create_jwt_token(json.dumps(data, separators=(",", ":"))) + token = self.create_jwt_token( + json.dumps(data, indent=None, separators=(",", ":"), sort_keys=True) + ) headers = { "Accept": "application/json", "Content-Type": "application/json", diff --git a/g2p_payment_g2p_connect/models/cycle.py b/g2p_payment_g2p_connect/models/cycle.py index 1009c3d9..8a59d2b4 100644 --- a/g2p_payment_g2p_connect/models/cycle.py +++ b/g2p_payment_g2p_connect/models/cycle.py @@ -37,7 +37,9 @@ def generate_summary(self): "message": self.disbursement_envelope_id, } - token = payment_manager.create_jwt_token(json.dumps(data, separators=(",", ":"))) + token = payment_manager.create_jwt_token( + json.dumps(data, indent=None, separators=(",", ":"), sort_keys=True) + ) headers = { "Accept": "application/json", "Content-Type": "application/json", diff --git a/g2p_payment_g2p_connect/models/payment_manager.py b/g2p_payment_g2p_connect/models/payment_manager.py index 4b0e54a4..88a9daad 100644 --- a/g2p_payment_g2p_connect/models/payment_manager.py +++ b/g2p_payment_g2p_connect/models/payment_manager.py @@ -179,7 +179,9 @@ def _send_payments(self, batches): ) try: _logger.info("G2P Bridge Disbursement Batch Data: %s", batch_data) - token = self.create_jwt_token(json.dumps(batch_data, separators=(",", ":"))) + token = self.create_jwt_token( + json.dumps(batch_data, indent=None, separators=(",", ":"), sort_keys=True) + ) headers = { "Accept": "application/json", "Content-Type": "application/json", @@ -268,7 +270,9 @@ def payments_status_check(self, id_): } try: _logger.info("G2P Connect Disbursement Status Data: %s", status_data) - token = payment_manager.create_jwt_token(json.dumps(status_data, separators=(",", ":"))) + token = payment_manager.create_jwt_token( + json.dumps(status_data, indent=None, separators=(",", ":"), sort_keys=True) + ) headers = { "Accept": "application/json", "Content-Type": "application/json", @@ -388,7 +392,9 @@ def _create_envelope_g2p_bridge(self, cycle): }, } try: - token = self.create_jwt_token(json.dumps(envelope_request_data, separators=(",", ":"))) + token = self.create_jwt_token( + json.dumps(envelope_request_data, indent=None, separators=(",", ":"), sort_keys=True) + ) headers = { "Accept": "application/json", "Content-Type": "application/json", diff --git a/g2p_program_registrant_info/wizard/g2p_program_registrant_info_wizard.py b/g2p_program_registrant_info/wizard/g2p_program_registrant_info_wizard.py index 0e57b9b1..074253a9 100644 --- a/g2p_program_registrant_info/wizard/g2p_program_registrant_info_wizard.py +++ b/g2p_program_registrant_info/wizard/g2p_program_registrant_info_wizard.py @@ -60,8 +60,8 @@ def jsonize_form_data(self, data, program, membership=None): @api.model def add_files_to_store(self, files, store, program_membership=None, tags=None): file_details = [] - DOC_TAGS = self.env["g2p.document.tag"] try: + DOC_TAGS = self.env["g2p.document.tag"] for file in files: if file and store: document_file = self.env["storage.file"].create( diff --git a/g2p_programs/models/managers/payment_manager.py b/g2p_programs/models/managers/payment_manager.py index fc17aa6f..b175c4ba 100644 --- a/g2p_programs/models/managers/payment_manager.py +++ b/g2p_programs/models/managers/payment_manager.py @@ -37,7 +37,7 @@ class BasePaymentManager(models.AbstractModel): name = fields.Char("Manager Name", required=True) program_id = fields.Many2one("g2p.program", string="Program", required=True) - def prepare_payments(self, entitlements): + def prepare_payments(self, cycle, entitlements): """ This method is used to prepare the payment list of the entitlements. :param entitlements: The entitlements. @@ -142,7 +142,7 @@ def constrains_batch_tag_ids(self): if rec.batch_tag_ids.sorted("order")[-1].domain != "[]": raise ValidationError(_("Last tag in the Batch Tags list must contain empty domain.")) - def prepare_payments(self, cycle, entitlements=None): + def prepare_payments(self, cycle, entitlements): if not entitlements: entitlements = cycle.entitlement_ids.filtered(lambda a: a.state == "approved") else: diff --git a/g2p_social_registry_importer/models/fetch_social_registry_beneficiary.py b/g2p_social_registry_importer/models/fetch_social_registry_beneficiary.py index 0296c4d4..4ef14e0e 100644 --- a/g2p_social_registry_importer/models/fetch_social_registry_beneficiary.py +++ b/g2p_social_registry_importer/models/fetch_social_registry_beneficiary.py @@ -141,7 +141,7 @@ def test_connection(self): }, } - @api.onchange("registry") + @api.onchange("target_registry") def onchange_target_registry(self): for rec in self: rec.target_program = None diff --git a/g2p_support_desk/README.md b/g2p_support_desk/README.md new file mode 100644 index 00000000..116cdee3 --- /dev/null +++ b/g2p_support_desk/README.md @@ -0,0 +1,11 @@ +# OpenG2P Support Desk Management System + +This module provides a comprehensive support desk management system with: + +- Ticket Management +- Team Management +- SLA Tracking +- Knowledge Base +- Beneficiary Portal Access + +Refer to https://docs.openg2p.org. diff --git a/g2p_support_desk/__init__.py b/g2p_support_desk/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/g2p_support_desk/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/g2p_support_desk/__manifest__.py b/g2p_support_desk/__manifest__.py new file mode 100644 index 00000000..0a99a41a --- /dev/null +++ b/g2p_support_desk/__manifest__.py @@ -0,0 +1,35 @@ +{ + "name": "G2P Support Desk", + "category": "G2P", + "version": "17.0.1.3.0", + "sequence": 1, + "author": "OpenG2P", + "website": "https://openg2p.org", + "license": "LGPL-3", + "summary": "OpenG2P Support Desk Management System", + "depends": [ + "base", + "mail", + "portal", + "web", + "g2p_programs", + "g2p_registry_base", + ], + "data": [ + "security/support_desk_security.xml", + "security/ir.model.access.csv", + "views/support_ticket_views.xml", + "views/support_team_views.xml", + "views/support_category_views.xml", + "views/support_tag_views.xml", + "views/support_stage_views.xml", + "views/menu_views.xml", + "data/support_desk_data.xml", + ], + "demo": [ + "demo/helpdesk_demo.xml", + ], + "installable": True, + "application": True, + "auto_install": False, +} diff --git a/g2p_support_desk/data/support_desk_data.xml b/g2p_support_desk/data/support_desk_data.xml new file mode 100644 index 00000000..bf56602e --- /dev/null +++ b/g2p_support_desk/data/support_desk_data.xml @@ -0,0 +1,76 @@ + + + + + Support Ticket + support.ticket + TICK + 5 + + + + + + New + 10 + True + + + + In Progress + 20 + + + + Waiting + 30 + True + + + + Done + 40 + True + True + + + + Cancelled + 50 + True + True + + + + + Technical + 10 + + + + Functional + 20 + + + + + Bug + 1 + + + + Enhancement + 2 + + + + Urgent + 3 + + + + + Support Team + + + diff --git a/g2p_support_desk/demo/helpdesk_demo.xml b/g2p_support_desk/demo/helpdesk_demo.xml new file mode 100644 index 00000000..d94f4801 --- /dev/null +++ b/g2p_support_desk/demo/helpdesk_demo.xml @@ -0,0 +1,58 @@ + + + + + Technical Support + Handle technical issues and bug reports + + + + Beneficiary Service + Handle general inquiries and beneficiary support + + + + + Bug Report + Software bugs and technical issues + 1 + + + + Feature Request + New feature suggestions and improvements + 2 + + + + + Critical + 1 + + + + Improvement + 4 + + + + + Login Issue + Unable to login to the system + + + + 2 + + + + + New Dashboard Feature + Request for customizable dashboard widgets + + + + 1 + + + diff --git a/g2p_support_desk/models/__init__.py b/g2p_support_desk/models/__init__.py new file mode 100644 index 00000000..106d8b20 --- /dev/null +++ b/g2p_support_desk/models/__init__.py @@ -0,0 +1,5 @@ +from . import support_ticket +from . import support_team +from . import support_category +from . import support_tag +from . import support_stage diff --git a/g2p_support_desk/models/support_category.py b/g2p_support_desk/models/support_category.py new file mode 100644 index 00000000..c691ffd9 --- /dev/null +++ b/g2p_support_desk/models/support_category.py @@ -0,0 +1,21 @@ +from odoo import fields, models + + +class SupportCategory(models.Model): + _name = "support.category" + _description = "Support Ticket Category" + _order = "sequence, name" + + name = fields.Char("Category Name", required=True) + sequence = fields.Integer(default=10) + description = fields.Text() + parent_id = fields.Many2one("support.category", string="Parent Category") + child_ids = fields.One2many("support.category", "parent_id", string="Child Categories") + active = fields.Boolean(default=True) + + ticket_ids = fields.One2many("support.ticket", "category_id", string="Tickets") + ticket_count = fields.Integer(compute="_compute_ticket_count", string="Tickets Count") + + def _compute_ticket_count(self): + for category in self: + category.ticket_count = len(category.ticket_ids) diff --git a/g2p_support_desk/models/support_stage.py b/g2p_support_desk/models/support_stage.py new file mode 100644 index 00000000..5ddee0e8 --- /dev/null +++ b/g2p_support_desk/models/support_stage.py @@ -0,0 +1,23 @@ +from odoo import fields, models + + +class SupportStage(models.Model): + _name = "support.stage" + _description = "Support Ticket Stage" + _order = "sequence, id" + + name = fields.Char(string="Stage Name", required=True, translate=True) + sequence = fields.Integer(default=10) + is_default = fields.Boolean(string="Default Stage") + fold = fields.Boolean(string="Folded in Kanban") + done = fields.Boolean(string="Request Done") + + description = fields.Text(translate=True) + team_ids = fields.Many2many("support.team", string="Teams") + + template_id = fields.Many2one( + "mail.template", + string="Email Template", + domain=[("model", "=", "support.ticket")], + help="Automatically send an email when the ticket reaches this stage.", + ) diff --git a/g2p_support_desk/models/support_tag.py b/g2p_support_desk/models/support_tag.py new file mode 100644 index 00000000..b7f85274 --- /dev/null +++ b/g2p_support_desk/models/support_tag.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class SupportTag(models.Model): + _name = "support.tag" + _description = "Support Ticket Tag" + + name = fields.Char(string="Tag Name", required=True) + color = fields.Integer(string="Color Index") + active = fields.Boolean(default=True) diff --git a/g2p_support_desk/models/support_team.py b/g2p_support_desk/models/support_team.py new file mode 100644 index 00000000..d993b6c6 --- /dev/null +++ b/g2p_support_desk/models/support_team.py @@ -0,0 +1,22 @@ +from odoo import fields, models + + +class SupportTeam(models.Model): + _name = "support.team" + _description = "Support Team" + _inherit = ["mail.thread"] + + name = fields.Char(string="Team Name", required=True) + leader_id = fields.Many2one("res.users", string="Team Leader", tracking=True) + member_ids = fields.Many2many("res.users", string="Team Members") + description = fields.Text() + + ticket_ids = fields.One2many("support.ticket", "team_id", string="Tickets") + ticket_count = fields.Integer(compute="_compute_ticket_count", string="Tickets Count") + + active = fields.Boolean(default=True) + color = fields.Integer(string="Color Index") + + def _compute_ticket_count(self): + for team in self: + team.ticket_count = len(team.ticket_ids) diff --git a/g2p_support_desk/models/support_ticket.py b/g2p_support_desk/models/support_ticket.py new file mode 100644 index 00000000..a0d391f4 --- /dev/null +++ b/g2p_support_desk/models/support_ticket.py @@ -0,0 +1,110 @@ +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class SupportTicket(models.Model): + _name = "support.ticket" + _description = "Support Ticket" + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "priority desc, id desc" + + name = fields.Char(string="Subject", required=True, tracking=True) + number = fields.Char(string="Ticket Number") + description = fields.Html() + team_id = fields.Many2one( + "support.team", + string="Team", + tracking=True, + ) + program_id = fields.Many2one( + "g2p.program", + string="Program", + tracking=True, + ) + user_id = fields.Many2one( + "res.users", + string="Assigned To", + default=lambda self: self.env.user, + tracking=True, + domain=[("share", "=", False)], + ) + + beneficiary_id = fields.Many2one("g2p.program_membership", string="Beneficiary", tracking=True) + # partner_email = fields.Char(string='Beneficiary Email') + # partner_phone = fields.Char(string='Beneficiary Phone') + + category_id = fields.Many2one("support.category", string="Category") + tag_ids = fields.Many2many("support.tag", string="Tags") + stage_id = fields.Many2one( + "support.stage", + string="Stage", + tracking=True, + # default=lambda self: self._get_default_stage(), + copy=False, + required=True, + ) + priority = fields.Selection( + [("0", "Low"), ("1", "Medium"), ("2", "High"), ("3", "Urgent")], + default="1", + tracking=True, + ) + color = fields.Integer(string="Color Index") + active = fields.Boolean(default=True) + + closed_date = fields.Datetime() + + # Statistics + # response_time = fields.Float( + # string="Response Time (Hours)", readonly=True, compute="_compute_response_time", store=True + # ) + resolution_time = fields.Float(string="Resolution Time (Hours)") + + def action_assign_to_me(self): + self.ensure_one() + self.user_id = self.env.user.id + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + if "number" in fields_list and "number" not in res: + res["number"] = self.env["ir.sequence"].next_by_code("support.ticket") or "New" + return res + + # @api.onchange('partner_id') + # def _onchange_partner_id(self): + # if self.partner_id: + # self.partner_email = self.partner_id.email or False + # self.partner_phone = self.partner_id.phone or False + # else: + # self.partner_email = False + # self.partner_phone = False + + # @api.onchange('stage_id') + # def _onchange_stage_id(self): + # if not self.stage_id: + # return + # if self.stage_id.done: + # self.closed_date = fields.Datetime.now() + # else: + # self.closed_date = False + + # @api.depends("create_date", "write_date") + # def _compute_response_time(self): + # for ticket in self: + # if ticket.create_date and ticket.write_date: + # delta = ticket.write_date - ticket.create_date + # # Ensure we have a positive time difference + # if delta.total_seconds() > 0: + # ticket.response_time = delta.total_seconds() / 3600.0 # Convert to hours + # else: + # # If write_date is not after create_date, use a small positive value + # ticket.response_time = 0.1 + # else: + # ticket.response_time = 0.0 + + @api.onchange("program_id") + def _onchange_program_id(self): + self.beneficiary_id = False diff --git a/g2p_support_desk/pyproject.toml b/g2p_support_desk/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/g2p_support_desk/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/g2p_support_desk/security/ir.model.access.csv b/g2p_support_desk/security/ir.model.access.csv new file mode 100644 index 00000000..37718923 --- /dev/null +++ b/g2p_support_desk/security/ir.model.access.csv @@ -0,0 +1,13 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_support_ticket_user,support.ticket.user,model_support_ticket,group_support_desk_user,1,1,1,0 +access_support_ticket_manager,support.ticket.manager,model_support_ticket,group_support_desk_manager,1,1,1,1 +access_support_team_user,support.team.user,model_support_team,group_support_desk_user,1,0,0,0 +access_support_team_manager,support.team.manager,model_support_team,group_support_desk_manager,1,1,1,1 +access_support_category_user,support.category.user,model_support_category,group_support_desk_user,1,0,0,0 +access_support_category_manager,support.category.manager,model_support_category,group_support_desk_manager,1,1,1,1 +access_support_tag_user,support.tag.user,model_support_tag,group_support_desk_user,1,0,0,0 +access_support_tag_manager,support.tag.manager,model_support_tag,group_support_desk_manager,1,1,1,1 +access_support_stage_user,support.stage.user,model_support_stage,group_support_desk_user,1,0,0,0 +access_support_stage_manager,support.stage.manager,model_support_stage,group_support_desk_manager,1,1,1,1 +access_g2p_program_user,g2p.program.user,g2p_programs.model_g2p_program,group_support_desk_user,1,0,0,0 +access_g2p_program_manager,g2p.program.manager,g2p_programs.model_g2p_program,group_support_desk_manager,1,1,1,1 diff --git a/g2p_support_desk/security/support_desk_security.xml b/g2p_support_desk/security/support_desk_security.xml new file mode 100644 index 00000000..84aa509f --- /dev/null +++ b/g2p_support_desk/security/support_desk_security.xml @@ -0,0 +1,57 @@ + + + + + User + + + + + + Manager + + + + + + + + Personal Tickets + + [('user_id','=',user.id)] + + + + + + + + + All Tickets + + [(1,'=',1)] + + + + + + Program User Rule + + [(1,'=',1)] + + + + + + + + + Program Manager Rule + + [(1,'=',1)] + + + diff --git a/g2p_support_desk/static/description/icon.png b/g2p_support_desk/static/description/icon.png new file mode 100644 index 00000000..5ecb429e Binary files /dev/null and b/g2p_support_desk/static/description/icon.png differ diff --git a/g2p_support_desk/tests/__init__.py b/g2p_support_desk/tests/__init__.py new file mode 100644 index 00000000..219312da --- /dev/null +++ b/g2p_support_desk/tests/__init__.py @@ -0,0 +1 @@ +# from . import test_support_desk diff --git a/g2p_support_desk/tests/test_support_desk.py b/g2p_support_desk/tests/test_support_desk.py new file mode 100644 index 00000000..2a127976 --- /dev/null +++ b/g2p_support_desk/tests/test_support_desk.py @@ -0,0 +1,398 @@ +import logging +import time + +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase +from odoo.tools import html2plaintext + +_logger = logging.getLogger(__name__) + + +class SupportDeskTest(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Create test users + cls.user_1 = cls.env["res.users"].create( + { + "name": "Test User 1", + "login": "test_user_1", + "email": "test_user_1@example.com", + "groups_id": [(4, cls.env.ref("g2p_support_desk.group_support_desk_user").id)], + } + ) + cls.user_2 = cls.env["res.users"].create( + { + "name": "Test User 2", + "login": "test_user_2", + "email": "test_user_2@example.com", + "groups_id": [(4, cls.env.ref("g2p_support_desk.group_support_desk_manager").id)], + } + ) + + # Create test program with minimal required fields + cls.program = cls.env["g2p.program"].create( + { + "name": "Test Program", + "target_type": "individual", + } + ) + + # Create test team + cls.team = cls.env["support.team"].create( + { + "name": "Test Team", + "leader_id": cls.user_2.id, + "member_ids": [(4, cls.user_1.id)], + } + ) + + # Create test category + cls.category = cls.env["support.category"].create( + { + "name": "Test Category", + "sequence": 1, + } + ) + + # Create test tag + cls.tag = cls.env["support.tag"].create( + { + "name": "Test Tag", + "color": 1, + } + ) + + # Create test stages + cls.stage_new = cls.env["support.stage"].create( + { + "name": "New", + "sequence": 1, + "is_default": True, + } + ) + cls.stage_in_progress = cls.env["support.stage"].create( + { + "name": "In Progress", + "sequence": 2, + } + ) + cls.stage_done = cls.env["support.stage"].create( + { + "name": "Done", + "sequence": 3, + } + ) + + def test_01_ticket_creation(self): + """Test ticket creation and basic fields""" + # First, ensure we're using the correct stage + default_stage = self.env["support.stage"].search([("is_default", "=", True)], limit=1) + if not default_stage: + default_stage = self.env["support.stage"].search([], limit=1) + self.assertTrue(default_stage, "No default stage found") + + ticket = self.env["support.ticket"].create( + { + "name": "Test Ticket", + "description": "Test Description", + "team_id": self.team.id, + "category_id": self.category.id, + "tag_ids": [(4, self.tag.id)], + "priority": "1", # Medium priority + "program_id": self.program.id, + } + ) + + self.assertEqual(ticket.name, "Test Ticket") + self.assertEqual(html2plaintext(ticket.description), "Test Description") + self.assertEqual(ticket.team_id, self.team) + self.assertEqual(ticket.category_id, self.category) + self.assertEqual(ticket.tag_ids, self.tag) + self.assertEqual(ticket.priority, "1") + self.assertEqual(ticket.program_id, self.program) + self.assertEqual(ticket.stage_id, default_stage) + self.assertTrue(ticket.active) + + def test_02_ticket_workflow(self): + """Test ticket workflow and stage transitions""" + ticket = self.env["support.ticket"].create( + { + "name": "Workflow Test Ticket", + "description": "Test Description", + "team_id": self.team.id, + } + ) + + # Test stage transitions + ticket.write({"stage_id": self.stage_in_progress.id}) + self.assertEqual(ticket.stage_id, self.stage_in_progress) + + ticket.write({"stage_id": self.stage_done.id}) + self.assertEqual(ticket.stage_id, self.stage_done) + self.assertIsNotNone(ticket.closed_date) + + def test_03_ticket_assignments(self): + """Test ticket assignments and reassignments""" + ticket = self.env["support.ticket"].create( + { + "name": "Assignment Test Ticket", + "description": "Test Description", + "team_id": self.team.id, + } + ) + + # Test user assignment + ticket.write({"user_id": self.user_1.id}) + self.assertEqual(ticket.user_id, self.user_1) + + # Test team reassignment + new_team = self.env["support.team"].create( + { + "name": "New Test Team", + "leader_id": self.user_2.id, + } + ) + ticket.write({"team_id": new_team.id}) + self.assertEqual(ticket.team_id, new_team) + + def test_04_ticket_priority(self): + """Test ticket priority changes""" + ticket = self.env["support.ticket"].create( + { + "name": "Priority Test Ticket", + "description": "Test Description", + "team_id": self.team.id, + } + ) + + # Test priority changes + priorities = ["0", "1", "2", "3"] + for priority in priorities: + ticket.write({"priority": priority}) + self.assertEqual(ticket.priority, priority) + + def test_05_ticket_tags(self): + """Test ticket tag management""" + ticket = self.env["support.ticket"].create( + { + "name": "Tag Test Ticket", + "description": "Test Description", + "team_id": self.team.id, + } + ) + + # Create additional tags + tag2 = self.env["support.tag"].create( + { + "name": "Test Tag 2", + "color": 2, + } + ) + tag3 = self.env["support.tag"].create( + { + "name": "Test Tag 3", + "color": 3, + } + ) + + # Test adding multiple tags + ticket.write({"tag_ids": [(4, self.tag.id), (4, tag2.id), (4, tag3.id)]}) + self.assertEqual(len(ticket.tag_ids), 3) + + # Test removing tags + ticket.write({"tag_ids": [(3, self.tag.id)]}) + self.assertEqual(len(ticket.tag_ids), 2) + + def test_06_ticket_search(self): + """Test ticket search functionality""" + # Create test tickets + ticket1 = self.env["support.ticket"].create( + { + "name": "Search Test Ticket 1", + "description": "Test Description 1", + "team_id": self.team.id, + "priority": "2", + } + ) + self.env["support.ticket"].create( + { + "name": "Search Test Ticket 2", + "description": "Test Description 2", + "team_id": self.team.id, + "priority": "0", + } + ) + + # Test search by name + tickets = self.env["support.ticket"].search([("name", "ilike", "Search Test")]) + self.assertEqual(len(tickets), 2) + + # Test search by priority + tickets = self.env["support.ticket"].search([("priority", "=", "2")]) + self.assertEqual(len(tickets), 1) + self.assertEqual(tickets[0], ticket1) + + def test_07_ticket_access_rights(self): + """Test ticket access rights""" + # Create ticket as user 1 + ticket = ( + self.env["support.ticket"] + .with_user(self.user_1) + .create( + { + "name": "Access Test Ticket", + "description": "Test Description", + "team_id": self.team.id, + } + ) + ) + + # Test user 1 access (should have access since they created it) + user1_ticket = self.env["support.ticket"].with_user(self.user_1).search([("id", "=", ticket.id)]) + self.assertEqual(len(user1_ticket), 1) + + # Test user 2 access (should have access as manager) + user2_ticket = self.env["support.ticket"].with_user(self.user_2).search([("id", "=", ticket.id)]) + self.assertEqual(len(user2_ticket), 1) + + # Create another ticket as user 2 + ticket2 = ( + self.env["support.ticket"] + .with_user(self.user_2) + .create( + { + "name": "Access Test Ticket 2", + "description": "Test Description", + "team_id": self.team.id, + } + ) + ) + + # Test user 1 access to ticket2 (should not have access) + user1_ticket2 = self.env["support.ticket"].with_user(self.user_1).search([("id", "=", ticket2.id)]) + self.assertEqual(len(user1_ticket2), 0) + + # Test user 2 access to ticket2 (should have access) + user2_ticket2 = self.env["support.ticket"].with_user(self.user_2).search([("id", "=", ticket2.id)]) + self.assertEqual(len(user2_ticket2), 1) + + def test_09_ticket_response_time(self): + """Test ticket response time tracking""" + ticket = self.env["support.ticket"].create( + { + "name": "Response Time Test Ticket", + "description": "Test Description", + "team_id": self.team.id, + } + ) + + # Add a small delay to ensure time difference + time.sleep(1) + + # Simulate first response by updating the ticket + ticket.write({"description": "Updated description with response", "user_id": self.user_1.id}) + + # Response time should be calculated based on create_date and write_date + self.assertIsNotNone(ticket.response_time) + self.assertGreater(ticket.response_time, 0) + + def test_10_ticket_resolution_time(self): + """Test ticket resolution time tracking""" + ticket = self.env["support.ticket"].create( + { + "name": "Resolution Time Test Ticket", + "description": "Test Description", + "team_id": self.team.id, + } + ) + + # Simulate resolution + ticket.write({"stage_id": self.stage_done.id, "closed_date": self.env.cr.now()}) + self.assertIsNotNone(ticket.closed_date) + self.assertIsNotNone(ticket.resolution_time) + + def test_11_ticket_beneficiary_filter(self): + """Test ticket beneficiary filter""" + # Create a ticket with a program + ticket = self.env["support.ticket"].create( + { + "name": "Beneficiary Test Ticket", + "description": "Test Description", + "team_id": self.team.id, + "program_id": self.program.id, + } + ) + + # Test that no beneficiary is selected initially + self.assertFalse(ticket.beneficiary_id) + + # Add a beneficiary to the program + partner = self.env["res.partner"].create( + { + "name": "Test Beneficiary", + "is_registrant": True, # Required for program membership + } + ) + membership = self.env["g2p.program_membership"].create( + { + "program_id": self.program.id, + "partner_id": partner.id, + } + ) + + # Test that beneficiary can be selected after adding to program + ticket.write({"beneficiary_id": membership.id}) + self.assertEqual(ticket.beneficiary_id, membership) + + # Remove the beneficiary from the program + membership.unlink() + + # Test that beneficiary is cleared when removed from program + # We need to trigger the constraint check by writing to the program_id + ticket.write({"program_id": self.program.id}) + self.assertFalse(ticket.beneficiary_id) + + # Test that beneficiary cannot be selected when not in program + with self.assertRaises(ValidationError): + ticket.write({"beneficiary_id": membership.id}) + + def test_13_ticket_onchange_program_id(self): + """Test ticket onchange program id""" + # Create a ticket with a program + ticket = self.env["support.ticket"].create( + { + "name": "Onchange Program Test Ticket", + "description": "Test Description", + "team_id": self.team.id, + "program_id": self.program.id, + } + ) + + # Test that no beneficiary is selected initially + self.assertFalse(ticket.beneficiary_id) + + # Add a beneficiary to the program + partner = self.env["res.partner"].create( + { + "name": "Test Beneficiary", + "is_registrant": True, # Required for program membership + } + ) + membership = self.env["g2p.program_membership"].create( + { + "program_id": self.program.id, + "partner_id": partner.id, + } + ) + + # Test that beneficiary can be selected after adding to program + ticket.write({"beneficiary_id": membership.id}) + self.assertEqual(ticket.beneficiary_id, membership) + + # Remove the program + ticket.write({"program_id": False}) + + # Test that beneficiary is cleared when program is removed + self.assertFalse(ticket.beneficiary_id) diff --git a/g2p_support_desk/views/menu_views.xml b/g2p_support_desk/views/menu_views.xml new file mode 100644 index 00000000..13284a08 --- /dev/null +++ b/g2p_support_desk/views/menu_views.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/g2p_support_desk/views/support_category_views.xml b/g2p_support_desk/views/support_category_views.xml new file mode 100644 index 00000000..21b4078b --- /dev/null +++ b/g2p_support_desk/views/support_category_views.xml @@ -0,0 +1,63 @@ + + + + + support.category.form + support.category + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + support.category.tree + support.category + + + + + + + + + + + + + Support Categories + support.category + tree,form + + + Create your first support category! + + + + diff --git a/g2p_support_desk/views/support_stage_views.xml b/g2p_support_desk/views/support_stage_views.xml new file mode 100644 index 00000000..7527a37f --- /dev/null +++ b/g2p_support_desk/views/support_stage_views.xml @@ -0,0 +1,56 @@ + + + + + support.stage.form + support.stage + + + + + + + + + + + + + + + + + + + + + + + + + + + support.stage.tree + support.stage + + + + + + + + + + + + + Support Stages + support.stage + tree,form + + + Create your first support stage! + + + + diff --git a/g2p_support_desk/views/support_tag_views.xml b/g2p_support_desk/views/support_tag_views.xml new file mode 100644 index 00000000..fbb1ca63 --- /dev/null +++ b/g2p_support_desk/views/support_tag_views.xml @@ -0,0 +1,46 @@ + + + + + support.tag.form + support.tag + + + + + + + + + + + + + + + + + + + + support.tag.tree + support.tag + + + + + + + + + + Support Tags + support.tag + tree,form + + + Create your first support tag! + + + + diff --git a/g2p_support_desk/views/support_team_views.xml b/g2p_support_desk/views/support_team_views.xml new file mode 100644 index 00000000..daaa22a3 --- /dev/null +++ b/g2p_support_desk/views/support_team_views.xml @@ -0,0 +1,82 @@ + + + + + support.team.form + support.team + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + support.team.tree + support.team + + + + + + + + + + + + support.team.search + support.team + + + + + + + + + + + + + Support Teams + support.team + tree,form + + + Create your first support team! + + + + diff --git a/g2p_support_desk/views/support_ticket_views.xml b/g2p_support_desk/views/support_ticket_views.xml new file mode 100644 index 00000000..cee65f90 --- /dev/null +++ b/g2p_support_desk/views/support_ticket_views.xml @@ -0,0 +1,213 @@ + + + + + support.ticket.form + support.ticket + + + + + + + + + + Archive + Unarchive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + support.ticket.tree + support.ticket + + + + + + + + + + + + + + + + + + support.ticket.kanban + support.ticket + + + + + + + + + + + + + + Edit + Delete + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + support.ticket.search + support.ticket + + + + + + + + + + + + + + + + + + + + + + + + + Support Tickets + support.ticket + kanban,tree,form + {'search_default_my_tickets': 1} + + + Create your first support ticket! + + + + + + + Create Ticket + support.ticket + form + current + {'form_view_initial_mode': 'edit'} + +
+ Create your first support category! +
+ Create your first support stage! +
+ Create your first support tag! +
+ Create your first support team! +
+ Create your first support ticket! +