From 9d741caffd6b77724b06a7bedef9dd2b06b3f2e9 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Mon, 9 Sep 2024 10:50:53 +0800 Subject: [PATCH 01/15] Increate test coverage --- spp_grm/README.rst | 0 spp_grm/__init__.py | 1 + spp_grm/__manifest__.py | 35 +++ spp_grm/data/grm_data.xml | 88 ++++++ spp_grm/models/__init__.py | 6 + spp_grm/models/grm_ticket.py | 148 +++++++++ spp_grm/models/grm_ticket_category.py | 21 ++ spp_grm/models/grm_ticket_channel.py | 19 ++ spp_grm/models/grm_ticket_stage.py | 34 +++ spp_grm/models/grm_ticket_tag.py | 15 + spp_grm/models/res_partner.py | 36 +++ spp_grm/pyproject.toml | 3 + spp_grm/security/grm_security.xml | 22 ++ spp_grm/security/ir.model.access.csv | 18 ++ spp_grm/static/description/icon.png | Bin 0 -> 12567 bytes spp_grm/tests/__init__.py | 3 + spp_grm/tests/grm_ticket.py | 43 +++ spp_grm/tests/grm_ticket_stage.py | 42 +++ spp_grm/tests/res_partner.py | 34 +++ spp_grm/views/grm_ticket_category_views.xml | 76 +++++ spp_grm/views/grm_ticket_channel_views.xml | 76 +++++ spp_grm/views/grm_ticket_menu.xml | 19 ++ spp_grm/views/grm_ticket_stage_views.xml | 88 ++++++ spp_grm/views/grm_ticket_tag_views.xml | 68 +++++ spp_grm/views/grm_ticket_views.xml | 315 ++++++++++++++++++++ spp_grm/views/res_config_settings_views.xml | 63 ++++ spp_grm/views/res_partner_views.xml | 68 +++++ 27 files changed, 1341 insertions(+) create mode 100644 spp_grm/README.rst create mode 100644 spp_grm/__init__.py create mode 100644 spp_grm/__manifest__.py create mode 100644 spp_grm/data/grm_data.xml create mode 100644 spp_grm/models/__init__.py create mode 100644 spp_grm/models/grm_ticket.py create mode 100644 spp_grm/models/grm_ticket_category.py create mode 100644 spp_grm/models/grm_ticket_channel.py create mode 100644 spp_grm/models/grm_ticket_stage.py create mode 100644 spp_grm/models/grm_ticket_tag.py create mode 100644 spp_grm/models/res_partner.py create mode 100644 spp_grm/pyproject.toml create mode 100644 spp_grm/security/grm_security.xml create mode 100644 spp_grm/security/ir.model.access.csv create mode 100644 spp_grm/static/description/icon.png create mode 100644 spp_grm/tests/__init__.py create mode 100644 spp_grm/tests/grm_ticket.py create mode 100644 spp_grm/tests/grm_ticket_stage.py create mode 100644 spp_grm/tests/res_partner.py create mode 100644 spp_grm/views/grm_ticket_category_views.xml create mode 100644 spp_grm/views/grm_ticket_channel_views.xml create mode 100644 spp_grm/views/grm_ticket_menu.xml create mode 100644 spp_grm/views/grm_ticket_stage_views.xml create mode 100644 spp_grm/views/grm_ticket_tag_views.xml create mode 100644 spp_grm/views/grm_ticket_views.xml create mode 100644 spp_grm/views/res_config_settings_views.xml create mode 100644 spp_grm/views/res_partner_views.xml diff --git a/spp_grm/README.rst b/spp_grm/README.rst new file mode 100644 index 000000000..e69de29bb diff --git a/spp_grm/__init__.py b/spp_grm/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/spp_grm/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/spp_grm/__manifest__.py b/spp_grm/__manifest__.py new file mode 100644 index 000000000..1f2a88917 --- /dev/null +++ b/spp_grm/__manifest__.py @@ -0,0 +1,35 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "OpenSPP - Grievance Redress Mechanism", + "summary": """ + Grievance redress mechanism module for OpenSPP""", + "version": "17.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/openspp-modules", + "license": "LGPL-3", + "development_status": "Beta", + "category": "OpenSPP", + "external_dependencies": {"python": []}, + "maintainers": ["jeremi", "gonzalesedwin1123"], + "depends": ["base", "mail", "g2p_registry_base", "g2p_registry_individual", "g2p_registry_group"], + "data": [ + "data/grm_data.xml", + "security/grm_security.xml", + "security/ir.model.access.csv", + "views/res_partner_views.xml", + "views/grm_ticket_menu.xml", + "views/grm_ticket_stage_views.xml", + "views/grm_ticket_category_views.xml", + "views/grm_ticket_channel_views.xml", + "views/grm_ticket_tag_views.xml", + "views/grm_ticket_views.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": True, + "installable": True, + "auto_install": False, +} diff --git a/spp_grm/data/grm_data.xml b/spp_grm/data/grm_data.xml new file mode 100644 index 000000000..9e32032c0 --- /dev/null +++ b/spp_grm/data/grm_data.xml @@ -0,0 +1,88 @@ + + + + + Grievance Redress Mechanism + Grievance Redress Mechanism (GRM) for OpenSPP. + 9 + + + + + + GRM Ticket Sequence + spp.grm.ticket.sequence + 6 + no_gap + + + %(range_year)s- + + + + + + 1 + New + True + False + + + + 2 + In Progress + False + False + + + + 3 + Awaiting + False + False + + + + 4 + Done + False + True + True + + + + + 5 + Cancelled + False + True + True + + + + + 6 + Rejected + False + True + True + + + + + + Web + + + Other + + + + Ticket Created + spp.grm.ticket + + + Ticket created + + + diff --git a/spp_grm/models/__init__.py b/spp_grm/models/__init__.py new file mode 100644 index 000000000..a44ccc2c0 --- /dev/null +++ b/spp_grm/models/__init__.py @@ -0,0 +1,6 @@ +from . import grm_ticket +from . import grm_ticket_stage +from . import grm_ticket_tag +from . import grm_ticket_channel +from . import grm_ticket_category +from . import res_partner diff --git a/spp_grm/models/grm_ticket.py b/spp_grm/models/grm_ticket.py new file mode 100644 index 000000000..9b44bc414 --- /dev/null +++ b/spp_grm/models/grm_ticket.py @@ -0,0 +1,148 @@ +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class SPPGRMTicket(models.Model): + _name = "spp.grm.ticket" + _description = "Grievance Redress Mechanism Ticket" + _rec_name = "number" + _rec_names_search = ["number", "name"] + _order = "priority desc, sequence, number desc, id desc" + _mail_post_access = "read" + _inherit = ["mail.thread.cc", "mail.activity.mixin"] + + def _default_stage_id(self): + stages = self.env["spp.grm.ticket.stage"].search([]) + if stages: + return stages[0].id + return None + + number = fields.Char(string="Ticket number", default="/", readonly=True) + name = fields.Char(string="Title", required=True) + description = fields.Html(required=True, sanitize_style=True) + user_id = fields.Many2one( + comodel_name="res.users", + string="Assigned user", + tracking=True, + index=True, + compute="_compute_user_id", + store=True, + readonly=False, + ) + stage_id = fields.Many2one( + comodel_name="spp.grm.ticket.stage", + string="Stage", + default=_default_stage_id, + store=True, + readonly=False, + ondelete="restrict", + tracking=True, + group_expand="_read_group_stage_ids", + copy=False, + index=True, + ) + partner_id = fields.Many2one( + comodel_name="res.partner", string="Registrant", required=True, domain="[('is_registrant', '=', True)]" + ) + partner_email = fields.Char(string="Email", related="partner_id.email", store=True) + last_stage_update = fields.Datetime(default=fields.Datetime.now) + assigned_date = fields.Datetime() + closed_date = fields.Datetime() + closed = fields.Boolean(related="stage_id.closed") + unattended = fields.Boolean(related="stage_id.unattended", store=True) + tag_ids = fields.Many2many(comodel_name="spp.grm.ticket.tag", string="Tags") + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + required=True, + default=lambda self: self.env.company, + ) + channel_id = fields.Many2one(comodel_name="spp.grm.ticket.channel", string="Channel") + category_id = fields.Many2one( + comodel_name="spp.grm.ticket.category", + string="Category", + ) + priority = fields.Selection( + selection=[ + ("0", "Low"), + ("1", "Medium"), + ("2", "High"), + ("3", "Very High"), + ], + default="1", + ) + attachment_ids = fields.One2many( + comodel_name="ir.attachment", + inverse_name="res_id", + domain=[("res_model", "=", "spp.grm.ticket")], + string="Media Attachments", + ) + color = fields.Integer(string="Color Index") + kanban_state = fields.Selection( + selection=[ + ("normal", "Default"), + ("done", "Ready for next stage"), + ("blocked", "Blocked"), + ], + ) + sequence = fields.Integer( + index=True, + default=10, + help="Gives the sequence order when displaying a list of tickets.", + ) + active = fields.Boolean(default=True) + + def name_get(self): + res = [] + for rec in self: + res.append((rec.id, rec.number + " - " + rec.name)) + return res + + def _creation_subtype(self): + return self.env.ref("spp_grm.grm_tck_created") + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get("number", "/") == "/": + vals["number"] = self._prepare_ticket_number() + _logger.debug(f"Creating ticket {vals['number']}") + if vals.get("user_id") and not vals.get("assigned_date"): + vals["assigned_date"] = fields.Datetime.now() + return super().create(vals_list) + + def copy(self, default=None): + self.ensure_one() + if default is None: + default = {} + if "number" not in default: + default["number"] = self._prepare_ticket_number() + res = super().copy(default) + return res + + def write(self, vals): + for _ticket in self: + now = fields.Datetime.now() + if vals.get("stage_id"): + stage = self.env["spp.grm.ticket.stage"].browse([vals["stage_id"]]) + vals["last_stage_update"] = now + if stage.closed: + vals["closed_date"] = now + if vals.get("user_id"): + vals["assigned_date"] = now + return super().write(vals) + + @api.model + def _read_group_stage_ids(self, stages, domain, order): + """Read group method for stage_id field.""" + return stages.search(domain, order=order) + + def assign_to_me(self): + self.write({"user_id": self.env.user.id}) + + def _prepare_ticket_number(self): + # Generate ticket number + return self.env["ir.sequence"].next_by_code("spp.grm.ticket.sequence") diff --git a/spp_grm/models/grm_ticket_category.py b/spp_grm/models/grm_ticket_category.py new file mode 100644 index 000000000..164858ce3 --- /dev/null +++ b/spp_grm/models/grm_ticket_category.py @@ -0,0 +1,21 @@ +from odoo import fields, models + + +class SPPGRMCategory(models.Model): + _name = "spp.grm.ticket.category" + _description = "Grievance Redress Mechanism Ticket Category" + _order = "sequence, id" + + sequence = fields.Integer(default=10) + active = fields.Boolean( + default=True, + ) + name = fields.Char( + required=True, + translate=True, + ) + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + default=lambda self: self.env.company, + ) diff --git a/spp_grm/models/grm_ticket_channel.py b/spp_grm/models/grm_ticket_channel.py new file mode 100644 index 000000000..caf88e24b --- /dev/null +++ b/spp_grm/models/grm_ticket_channel.py @@ -0,0 +1,19 @@ +from odoo import fields, models + + +class SPPGRMTicketChannel(models.Model): + _name = "spp.grm.ticket.channel" + _description = "Grievance Redress Mechanism Ticket Channel" + _order = "sequence, id" + + sequence = fields.Integer(default=10) + name = fields.Char( + required=True, + translate=True, + ) + active = fields.Boolean(default=True) + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + default=lambda self: self.env.company, + ) diff --git a/spp_grm/models/grm_ticket_stage.py b/spp_grm/models/grm_ticket_stage.py new file mode 100644 index 000000000..fcaaa585f --- /dev/null +++ b/spp_grm/models/grm_ticket_stage.py @@ -0,0 +1,34 @@ +from odoo import api, fields, models + + +class SPPGRMTicketStage(models.Model): + _name = "spp.grm.ticket.stage" + _description = "Grievance Redress Mechanism Ticket Stage" + _order = "sequence, id" + + name = fields.Char(string="Stage Name", required=True, translate=True) + description = fields.Html(translate=True, sanitize_style=True) + sequence = fields.Integer(default=1) + active = fields.Boolean(default=True) + unattended = fields.Boolean() + closed = fields.Boolean() + mail_template_id = fields.Many2one( + comodel_name="mail.template", + string="Email Template", + domain=[("model", "=", "helpdesk.ticket")], + help="If set an email will be sent to the " "customer when the ticket" "reaches this step.", + ) + fold = fields.Boolean( + string="Folded in Kanban", + help="This stage is folded in the kanban view " "when there are no records in that stage " "to display.", + ) + company_id = fields.Many2one( + comodel_name="res.company", + 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/spp_grm/models/grm_ticket_tag.py b/spp_grm/models/grm_ticket_tag.py new file mode 100644 index 000000000..73cbddd84 --- /dev/null +++ b/spp_grm/models/grm_ticket_tag.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class SPPGRMTicketTag(models.Model): + _name = "spp.grm.ticket.tag" + _description = "Grievance Redress Mechanism Ticket Tag" + + name = fields.Char() + color = fields.Integer(string="Color Index") + active = fields.Boolean(default=True) + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + default=lambda self: self.env.company, + ) diff --git a/spp_grm/models/res_partner.py b/spp_grm/models/res_partner.py new file mode 100644 index 000000000..fde8aa004 --- /dev/null +++ b/spp_grm/models/res_partner.py @@ -0,0 +1,36 @@ +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + grm_ticket_ids = fields.One2many( + comodel_name="spp.grm.ticket", + inverse_name="partner_id", + string="Related Tickets", + ) + + grm_ticket_count = fields.Integer(compute="_compute_grm_ticket_count", string="Ticket Count") + + grm_ticket_active_count = fields.Integer(compute="_compute_grm_ticket_count", string="Active Ticket Count") + + grm_ticket_count_string = fields.Char(compute="_compute_grm_ticket_count", string="Tickets") + + def _compute_grm_ticket_count(self): + for record in self: + ticket_ids = self.env["spp.grm.ticket"].search([("partner_id", "child_of", record.id)]) + record.grm_ticket_count = len(ticket_ids) + record.grm_ticket_active_count = len(ticket_ids.filtered(lambda ticket: not ticket.stage_id.closed)) + count_active = record.grm_ticket_active_count + count = record.grm_ticket_count + record.grm_ticket_count_string = f"{count_active} / {count}" + + def action_view_grm_tickets(self): + return { + "name": "Tickets", # self.name, + "view_mode": "tree,form", + "res_model": "spp.grm.ticket", + "type": "ir.actions.act_window", + "domain": [("partner_id", "child_of", self.id)], + "context": self.env.context, + } diff --git a/spp_grm/pyproject.toml b/spp_grm/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_grm/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_grm/security/grm_security.xml b/spp_grm/security/grm_security.xml new file mode 100644 index 000000000..c7f134c86 --- /dev/null +++ b/spp_grm/security/grm_security.xml @@ -0,0 +1,22 @@ + + + + + User + + + + Manager + + + + + + + User Own Tickets + + [('user_id', '=', user.id)] + + + + diff --git a/spp_grm/security/ir.model.access.csv b/spp_grm/security/ir.model.access.csv new file mode 100644 index 000000000..ff3054862 --- /dev/null +++ b/spp_grm/security/ir.model.access.csv @@ -0,0 +1,18 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_spp_grm_ticket_manager,GRM Ticket Manager Access,model_spp_grm_ticket,group_grm_manager,1,1,1,1 +access_spp_grm_ticket_stage_manager,GRM Ticket Stage Manager Access,model_spp_grm_ticket_stage,group_grm_manager,1,1,1,1 +access_spp_grm_ticket_tag_manager,GRM Ticket Tags Manager Access,model_spp_grm_ticket_tag,group_grm_manager,1,1,1,1 +access_spp_grm_ticket_channel_manager,GRM Ticket Channels Manager Access,model_spp_grm_ticket_channel,group_grm_manager,1,1,1,1 +access_spp_grm_ticket_category_manager,GRM Ticket Category Manager Access,model_spp_grm_ticket_category,group_grm_manager,1,1,1,1 + +access_spp_grm_ticket_user,GRM Ticket User Access,model_spp_grm_ticket,group_grm_user,1,1,1,0 +access_spp_grm_ticket_stage_user,GRM Ticket Stage User Access,model_spp_grm_ticket_stage,group_grm_manager,1,0,0,0 +access_spp_grm_ticket_tag_user,GRM Ticket Tags User Access,model_spp_grm_ticket_tag,group_grm_manager,1,1,1,1 +access_spp_grm_ticket_channel_user,GRM Ticket Channels User Access,model_spp_grm_ticket_channel,group_grm_manager,1,0,0,0 +access_spp_grm_ticket_category_user,GRM Ticket Category User Access,model_spp_grm_ticket_category,group_grm_manager,1,0,0,0 + +access_spp_grm_ticket_base_user,GRM Ticket Base User Access,model_spp_grm_ticket,base.group_user,1,1,1,0 +access_spp_grm_ticket_stage_base_user,GRM Ticket Stage Base User Access,model_spp_grm_ticket_stage,base.group_user,1,0,0,0 +access_spp_grm_ticket_tag_base_user,GRM Ticket Tags Base User Access,model_spp_grm_ticket_tag,base.group_user,1,1,1,0 +access_spp_grm_ticket_channel_base_user,GRM Ticket Channels Base User Access,model_spp_grm_ticket_channel,base.group_user,1,0,0,0 +access_spp_grm_ticket_category_base_user,GRM Ticket Category Base User Access,model_spp_grm_ticket_category,base.group_user,1,0,0,0 diff --git a/spp_grm/static/description/icon.png b/spp_grm/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..35f8fec263588314689c920efc4b6728b407daaa GIT binary patch literal 12567 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®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 + + + view.spp_grm_category.form + spp.grm.ticket.category + +
+
+
+ + + +
+
+ + + + +
+
+
+
+ + + view.spp_grm_category.tree + spp.grm.ticket.category + + + + + + + + + + + spp.grm.ticket.category.search + spp.grm.ticket.category + + + + + + + + + + + + + + + Categories + ir.actions.act_window + spp.grm.ticket.category + tree + + + + +
diff --git a/spp_grm/views/grm_ticket_channel_views.xml b/spp_grm/views/grm_ticket_channel_views.xml new file mode 100644 index 000000000..ed484bde1 --- /dev/null +++ b/spp_grm/views/grm_ticket_channel_views.xml @@ -0,0 +1,76 @@ + + + + view.spp_grm_channel.form + spp.grm.ticket.channel + +
+
+
+ + +
+
+ + + + +
+
+
+
+ + + view.spp_grm_channel.tree + spp.grm.ticket.channel + + + + + + + + + + + spp.grm.ticket.channel.search + spp.grm.ticket.channel + + + + + + + + + + + + + + + Channels + ir.actions.act_window + spp.grm.ticket.channel + tree + + + + + +
diff --git a/spp_grm/views/grm_ticket_menu.xml b/spp_grm/views/grm_ticket_menu.xml new file mode 100644 index 000000000..289d53b12 --- /dev/null +++ b/spp_grm/views/grm_ticket_menu.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/spp_grm/views/grm_ticket_stage_views.xml b/spp_grm/views/grm_ticket_stage_views.xml new file mode 100644 index 000000000..b3bb57017 --- /dev/null +++ b/spp_grm/views/grm_ticket_stage_views.xml @@ -0,0 +1,88 @@ + + + + spp.grm.ticket.stage.form + spp.grm.ticket.stage + +
+
+
+ + +
+
+
+
+ + + + + + + + + + + + + + +
+
+
+
+ + + spp.grm.ticket.stage.tree + spp.grm.ticket.stage + + + + + + + + + + + + spp.grm.ticket.stage.search + spp.grm.ticket.stage + + + + + + + + + + + + + + + Stages + ir.actions.act_window + spp.grm.ticket.stage + tree,form + + + + +
diff --git a/spp_grm/views/grm_ticket_tag_views.xml b/spp_grm/views/grm_ticket_tag_views.xml new file mode 100644 index 000000000..f94619810 --- /dev/null +++ b/spp_grm/views/grm_ticket_tag_views.xml @@ -0,0 +1,68 @@ + + + + spp.grm.ticket.tag.form + spp.grm.ticket.tag + +
+
+
+ + +
+
+ +
+
+ + + +
+
+
+
+ + + spp.grm.ticket.tag.tree + spp.grm.ticket.tag + + + + + + + + + + + spp.grm.ticket.tag.search + spp.grm.ticket.tag + + + + + + + + + + + Ticket Tags + ir.actions.act_window + spp.grm.ticket.tag + tree + + + + +
diff --git a/spp_grm/views/grm_ticket_views.xml b/spp_grm/views/grm_ticket_views.xml new file mode 100644 index 000000000..51d292b13 --- /dev/null +++ b/spp_grm/views/grm_ticket_views.xml @@ -0,0 +1,315 @@ + + + + spp.grm.ticket.view.form + spp.grm.ticket + +
+
+
+ + +
+
+
+

+ +

+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + spp.grm.ticket.view.tree + spp.grm.ticket + + + + + + + + + + + + + + + + + + + + + spp.grm.ticket.view.search + spp.grm.ticket + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spp.grm.ticket.kanban + spp.grm.ticket + + + + + + + + + + + + + + + +
+
+
+ + + + + + +
+ +
+
+
+ + +
+
+ + + +
+
+
+ + + + + + + + Tickets + ir.actions.act_window + spp.grm.ticket + tree,kanban,form + + + + + diff --git a/spp_grm/views/res_config_settings_views.xml b/spp_grm/views/res_config_settings_views.xml new file mode 100644 index 000000000..4aa60387d --- /dev/null +++ b/spp_grm/views/res_config_settings_views.xml @@ -0,0 +1,63 @@ + + + + res.config.settings + + + +
+

New ticket form (portal)

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ + Settings + ir.actions.act_window + res.config.settings + form + inline + {'module' : 'helpdesk_mgmt', 'bin_size': False} + +
diff --git a/spp_grm/views/res_partner_views.xml b/spp_grm/views/res_partner_views.xml new file mode 100644 index 000000000..a577761b0 --- /dev/null +++ b/spp_grm/views/res_partner_views.xml @@ -0,0 +1,68 @@ + + + + + view_individuals_form_g2p_inherit + res.partner + + +
+ +
+
+
+ + + + view_groups_form_g2p_inherit + res.partner + + +
+ +
+
+
+ + + + view_partner_form + res.partner + + +
+ +
+
+
+
From 41addcb35d8a8f9b2207869b016b54805ea00b97 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Mon, 9 Sep 2024 15:59:44 +0800 Subject: [PATCH 02/15] Add more tests in grm_tickets --- spp_grm/tests/grm_ticket.py | 72 ++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/spp_grm/tests/grm_ticket.py b/spp_grm/tests/grm_ticket.py index 5a6f3643a..73a1c2df8 100644 --- a/spp_grm/tests/grm_ticket.py +++ b/spp_grm/tests/grm_ticket.py @@ -1,43 +1,83 @@ +from odoo import fields from odoo.tests.common import TransactionCase class SPPGRMTicketTests(TransactionCase): def setUp(self): super().setUp() + self.ticket_stage_open = self.env["spp.grm.ticket.stage"].create({"name": "Open", "closed": False}) + self.ticket_stage_closed = self.env["spp.grm.ticket.stage"].create({"name": "Closed", "closed": True}) self.ticket = self.env["spp.grm.ticket"].create( { "name": "Test Ticket", "description": "Test Description", "partner_id": self.env.ref("base.res_partner_1").id, + "stage_id": self.ticket_stage_open.id, } ) - def ticket_creation(self): + def test_ticket_creation_number_generation(self): + """Test creation with default '/' number triggers number generation.""" new_ticket = self.env["spp.grm.ticket"].create( { "name": "New Ticket", "description": "New Description", "partner_id": self.env.ref("base.res_partner_2").id, + "number": "/", # Explicitly use "/" to trigger number generation } ) - self.assertEqual(new_ticket.name, "New Ticket") + self.assertNotEqual(new_ticket.number, "/") # Ensure number is generated + self.assertTrue(new_ticket.number.startswith("GRM")) # Example pattern, adjust as needed - def ticket_number_generation(self): - self.assertNotEqual(self.ticket.number, "/") + def test_ticket_creation_user_assignment(self): + """Test creation where user_id is set but assigned_date is not.""" + new_ticket = self.env["spp.grm.ticket"].create( + { + "name": "Assigned Ticket", + "description": "Assigned Description", + "partner_id": self.env.ref("base.res_partner_2").id, + "user_id": self.env.ref("base.user_admin").id, # Assign user + # Do not provide assigned_date + } + ) + self.assertEqual(new_ticket.user_id, self.env.ref("base.user_admin")) + self.assertIsNotNone(new_ticket.assigned_date) # Ensure assigned_date is set + + def test_ticket_copy_number_generation(self): + """Test copying a ticket generates a new number.""" + copied_ticket = self.ticket.copy() + self.assertNotEqual(copied_ticket.number, self.ticket.number) # Ensure different number - def ticket_assignment(self): + def test_ticket_copy_custom_number(self): + """Test copying a ticket with a provided number in default.""" + copied_ticket = self.ticket.copy(default={"number": "CUSTOM123"}) + self.assertEqual(copied_ticket.number, "CUSTOM123") # Ensure custom number is used + + def test_ticket_write_stage_update(self): + """Test writing stage_id updates last_stage_update and closed_date (if closed).""" + now = fields.Datetime.now() + + # Transition to a closed stage + self.ticket.write({"stage_id": self.ticket_stage_closed.id}) + self.assertEqual(self.ticket.stage_id, self.ticket_stage_closed) + self.assertEqual(self.ticket.closed_date.date(), now.date()) # Check closed_date is set + self.assertEqual(self.ticket.last_stage_update.date(), now.date()) # Check last_stage_update is set + + def test_ticket_write_user_assignment(self): + """Test writing user_id updates assigned_date.""" + now = fields.Datetime.now() + + # Assign user self.ticket.write({"user_id": self.env.ref("base.user_admin").id}) self.assertEqual(self.ticket.user_id, self.env.ref("base.user_admin")) + self.assertEqual(self.ticket.assigned_date.date(), now.date()) # Check assigned_date is set - def ticket_stage_transition(self): - stage_closed = self.env["spp.grm.ticket.stage"].search([("closed", "=", True)], limit=1) - self.ticket.write({"stage_id": stage_closed.id}) - self.assertEqual(self.ticket.stage_id, stage_closed) - - def ticket_copy(self): - copied_ticket = self.ticket.copy() - self.assertNotEqual(copied_ticket.number, self.ticket.number) + def test_ticket_write_no_stage_or_user_change(self): + """Test that writing without stage_id or user_id does not alter dates.""" + original_last_stage_update = self.ticket.last_stage_update + original_assigned_date = self.ticket.assigned_date - def ticket_assign_to_me(self): - self.ticket.assign_to_me() - self.assertEqual(self.ticket.user_id, self.env.user) + # Write without changing stage_id or user_id + self.ticket.write({"name": "Updated Ticket Name"}) + self.assertEqual(self.ticket.last_stage_update, original_last_stage_update) # No change + self.assertEqual(self.ticket.assigned_date, original_assigned_date) # No change From b61e83384d588736030edcca2fb2ba0f8241532b Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Tue, 10 Sep 2024 14:43:20 +0800 Subject: [PATCH 03/15] Fix issue with tests --- spp_grm/tests/grm_ticket.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spp_grm/tests/grm_ticket.py b/spp_grm/tests/grm_ticket.py index 73a1c2df8..559989881 100644 --- a/spp_grm/tests/grm_ticket.py +++ b/spp_grm/tests/grm_ticket.py @@ -3,8 +3,9 @@ class SPPGRMTicketTests(TransactionCase): - def setUp(self): - super().setUp() + @classmethod + def setUpClass(self): + super().setUpClass() self.ticket_stage_open = self.env["spp.grm.ticket.stage"].create({"name": "Open", "closed": False}) self.ticket_stage_closed = self.env["spp.grm.ticket.stage"].create({"name": "Closed", "closed": True}) self.ticket = self.env["spp.grm.ticket"].create( From 462a0d63817fd58f63711446b6a2f4c56e78ac7c Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Tue, 10 Sep 2024 15:26:36 +0800 Subject: [PATCH 04/15] Fix issue with running test --- spp_grm/tests/grm_ticket.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spp_grm/tests/grm_ticket.py b/spp_grm/tests/grm_ticket.py index 559989881..3572bc55c 100644 --- a/spp_grm/tests/grm_ticket.py +++ b/spp_grm/tests/grm_ticket.py @@ -4,16 +4,16 @@ class SPPGRMTicketTests(TransactionCase): @classmethod - def setUpClass(self): + def setUpClass(cls): super().setUpClass() - self.ticket_stage_open = self.env["spp.grm.ticket.stage"].create({"name": "Open", "closed": False}) - self.ticket_stage_closed = self.env["spp.grm.ticket.stage"].create({"name": "Closed", "closed": True}) - self.ticket = self.env["spp.grm.ticket"].create( + cls.ticket_stage_open = cls.env["spp.grm.ticket.stage"].create({"name": "Open", "closed": False}) + cls.ticket_stage_closed = cls.env["spp.grm.ticket.stage"].create({"name": "Closed", "closed": True}) + cls.ticket = cls.env["spp.grm.ticket"].create( { "name": "Test Ticket", "description": "Test Description", - "partner_id": self.env.ref("base.res_partner_1").id, - "stage_id": self.ticket_stage_open.id, + "partner_id": cls.env.ref("base.res_partner_1").id, + "stage_id": cls.ticket_stage_open.id, } ) From 7a7a6a6bc0f8856481992d2460731a25137527b2 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Tue, 10 Sep 2024 16:43:27 +0800 Subject: [PATCH 05/15] Renamed the test files --- spp_grm/tests/__init__.py | 6 +++--- spp_grm/tests/{grm_ticket.py => test_grm_ticket.py} | 0 .../tests/{grm_ticket_stage.py => test_grm_ticket_stage.py} | 0 spp_grm/tests/{res_partner.py => test_res_partner.py} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename spp_grm/tests/{grm_ticket.py => test_grm_ticket.py} (100%) rename spp_grm/tests/{grm_ticket_stage.py => test_grm_ticket_stage.py} (100%) rename spp_grm/tests/{res_partner.py => test_res_partner.py} (100%) diff --git a/spp_grm/tests/__init__.py b/spp_grm/tests/__init__.py index 08898aa51..133c82356 100644 --- a/spp_grm/tests/__init__.py +++ b/spp_grm/tests/__init__.py @@ -1,3 +1,3 @@ -from . import grm_ticket -from . import grm_ticket_stage -from . import res_partner +from . import test_grm_ticket +from . import test_grm_ticket_stage +from . import test_res_partner diff --git a/spp_grm/tests/grm_ticket.py b/spp_grm/tests/test_grm_ticket.py similarity index 100% rename from spp_grm/tests/grm_ticket.py rename to spp_grm/tests/test_grm_ticket.py diff --git a/spp_grm/tests/grm_ticket_stage.py b/spp_grm/tests/test_grm_ticket_stage.py similarity index 100% rename from spp_grm/tests/grm_ticket_stage.py rename to spp_grm/tests/test_grm_ticket_stage.py diff --git a/spp_grm/tests/res_partner.py b/spp_grm/tests/test_res_partner.py similarity index 100% rename from spp_grm/tests/res_partner.py rename to spp_grm/tests/test_res_partner.py From 3ba9f76d03df085e50dc233f4a371c1da9c9e121 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Tue, 10 Sep 2024 17:37:41 +0800 Subject: [PATCH 06/15] Renamed the test files --- spp_grm/tests/test_grm_ticket.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/spp_grm/tests/test_grm_ticket.py b/spp_grm/tests/test_grm_ticket.py index 3572bc55c..617910bb4 100644 --- a/spp_grm/tests/test_grm_ticket.py +++ b/spp_grm/tests/test_grm_ticket.py @@ -8,11 +8,17 @@ def setUpClass(cls): super().setUpClass() cls.ticket_stage_open = cls.env["spp.grm.ticket.stage"].create({"name": "Open", "closed": False}) cls.ticket_stage_closed = cls.env["spp.grm.ticket.stage"].create({"name": "Closed", "closed": True}) + cls.partner_1 = cls.env['res.partner'].create({ + 'name': 'Test Partner 1', + }) + cls.partner_2 = cls.env['res.partner'].create({ + 'name': 'Test Partner 2', + }) cls.ticket = cls.env["spp.grm.ticket"].create( { "name": "Test Ticket", "description": "Test Description", - "partner_id": cls.env.ref("base.res_partner_1").id, + "partner_id": cls.partner_1.id, "stage_id": cls.ticket_stage_open.id, } ) @@ -23,7 +29,7 @@ def test_ticket_creation_number_generation(self): { "name": "New Ticket", "description": "New Description", - "partner_id": self.env.ref("base.res_partner_2").id, + "partner_id": self.partner_2.id, "number": "/", # Explicitly use "/" to trigger number generation } ) @@ -36,7 +42,7 @@ def test_ticket_creation_user_assignment(self): { "name": "Assigned Ticket", "description": "Assigned Description", - "partner_id": self.env.ref("base.res_partner_2").id, + "partner_id": self.partner_2.id, "user_id": self.env.ref("base.user_admin").id, # Assign user # Do not provide assigned_date } From 4061ff1e724ac393c8b750fd2fe22aeb694f3532 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Tue, 10 Sep 2024 17:40:31 +0800 Subject: [PATCH 07/15] Renamed the test files --- spp_grm/tests/test_grm_ticket.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/spp_grm/tests/test_grm_ticket.py b/spp_grm/tests/test_grm_ticket.py index 617910bb4..89d696767 100644 --- a/spp_grm/tests/test_grm_ticket.py +++ b/spp_grm/tests/test_grm_ticket.py @@ -8,12 +8,16 @@ def setUpClass(cls): super().setUpClass() cls.ticket_stage_open = cls.env["spp.grm.ticket.stage"].create({"name": "Open", "closed": False}) cls.ticket_stage_closed = cls.env["spp.grm.ticket.stage"].create({"name": "Closed", "closed": True}) - cls.partner_1 = cls.env['res.partner'].create({ - 'name': 'Test Partner 1', - }) - cls.partner_2 = cls.env['res.partner'].create({ - 'name': 'Test Partner 2', - }) + cls.partner_1 = cls.env["res.partner"].create( + { + "name": "Test Partner 1", + } + ) + cls.partner_2 = cls.env["res.partner"].create( + { + "name": "Test Partner 2", + } + ) cls.ticket = cls.env["spp.grm.ticket"].create( { "name": "Test Ticket", From 04c96ae0fd68e3ea56259383be6c1ee053186399 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Tue, 10 Sep 2024 19:57:52 +0800 Subject: [PATCH 08/15] Fix ticket stage and res_partner tests --- spp_grm/tests/test_grm_ticket.py | 27 +++++++++++--------------- spp_grm/tests/test_grm_ticket_stage.py | 7 ++++--- spp_grm/tests/test_res_partner.py | 11 ++++++----- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/spp_grm/tests/test_grm_ticket.py b/spp_grm/tests/test_grm_ticket.py index 89d696767..1000dbc69 100644 --- a/spp_grm/tests/test_grm_ticket.py +++ b/spp_grm/tests/test_grm_ticket.py @@ -34,11 +34,10 @@ def test_ticket_creation_number_generation(self): "name": "New Ticket", "description": "New Description", "partner_id": self.partner_2.id, - "number": "/", # Explicitly use "/" to trigger number generation + "number": "/", } ) - self.assertNotEqual(new_ticket.number, "/") # Ensure number is generated - self.assertTrue(new_ticket.number.startswith("GRM")) # Example pattern, adjust as needed + self.assertNotEqual(new_ticket.number, "/") def test_ticket_creation_user_assignment(self): """Test creation where user_id is set but assigned_date is not.""" @@ -47,48 +46,44 @@ def test_ticket_creation_user_assignment(self): "name": "Assigned Ticket", "description": "Assigned Description", "partner_id": self.partner_2.id, - "user_id": self.env.ref("base.user_admin").id, # Assign user - # Do not provide assigned_date + "user_id": self.env.ref("base.user_admin").id, } ) self.assertEqual(new_ticket.user_id, self.env.ref("base.user_admin")) - self.assertIsNotNone(new_ticket.assigned_date) # Ensure assigned_date is set + self.assertIsNotNone(new_ticket.assigned_date) def test_ticket_copy_number_generation(self): """Test copying a ticket generates a new number.""" copied_ticket = self.ticket.copy() - self.assertNotEqual(copied_ticket.number, self.ticket.number) # Ensure different number + self.assertNotEqual(copied_ticket.number, self.ticket.number) def test_ticket_copy_custom_number(self): """Test copying a ticket with a provided number in default.""" copied_ticket = self.ticket.copy(default={"number": "CUSTOM123"}) - self.assertEqual(copied_ticket.number, "CUSTOM123") # Ensure custom number is used + self.assertEqual(copied_ticket.number, "CUSTOM123") def test_ticket_write_stage_update(self): """Test writing stage_id updates last_stage_update and closed_date (if closed).""" now = fields.Datetime.now() - # Transition to a closed stage self.ticket.write({"stage_id": self.ticket_stage_closed.id}) self.assertEqual(self.ticket.stage_id, self.ticket_stage_closed) - self.assertEqual(self.ticket.closed_date.date(), now.date()) # Check closed_date is set - self.assertEqual(self.ticket.last_stage_update.date(), now.date()) # Check last_stage_update is set + self.assertEqual(self.ticket.closed_date.date(), now.date()) + self.assertEqual(self.ticket.last_stage_update.date(), now.date()) def test_ticket_write_user_assignment(self): """Test writing user_id updates assigned_date.""" now = fields.Datetime.now() - # Assign user self.ticket.write({"user_id": self.env.ref("base.user_admin").id}) self.assertEqual(self.ticket.user_id, self.env.ref("base.user_admin")) - self.assertEqual(self.ticket.assigned_date.date(), now.date()) # Check assigned_date is set + self.assertEqual(self.ticket.assigned_date.date(), now.date()) def test_ticket_write_no_stage_or_user_change(self): """Test that writing without stage_id or user_id does not alter dates.""" original_last_stage_update = self.ticket.last_stage_update original_assigned_date = self.ticket.assigned_date - # Write without changing stage_id or user_id self.ticket.write({"name": "Updated Ticket Name"}) - self.assertEqual(self.ticket.last_stage_update, original_last_stage_update) # No change - self.assertEqual(self.ticket.assigned_date, original_assigned_date) # No change + self.assertEqual(self.ticket.last_stage_update, original_last_stage_update) + self.assertEqual(self.ticket.assigned_date, original_assigned_date) diff --git a/spp_grm/tests/test_grm_ticket_stage.py b/spp_grm/tests/test_grm_ticket_stage.py index 6ed8c3f07..71b64c4f7 100644 --- a/spp_grm/tests/test_grm_ticket_stage.py +++ b/spp_grm/tests/test_grm_ticket_stage.py @@ -2,9 +2,10 @@ class SPPGRMTicketStageTests(TransactionCase): - def setUp(self): - super().setUp() - self.stage = self.env["spp.grm.ticket.stage"].create( + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.stage = cls.env["spp.grm.ticket.stage"].create( { "name": "Test Stage", "sequence": 1, diff --git a/spp_grm/tests/test_res_partner.py b/spp_grm/tests/test_res_partner.py index c526c1c45..df5218176 100644 --- a/spp_grm/tests/test_res_partner.py +++ b/spp_grm/tests/test_res_partner.py @@ -2,17 +2,18 @@ class ResPartnerTicketTests(TransactionCase): - def setUp(self): - super().setUp() - self.partner = self.env["res.partner"].create( + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create( { "name": "Test Partner", } ) - self.ticket = self.env["spp.grm.ticket"].create( + cls.ticket = cls.env["spp.grm.ticket"].create( { "name": "Test Ticket", - "partner_id": self.partner.id, + "partner_id": cls.partner.id, } ) From aa15f0e46495410353cd30ab3f60ac61f7cf9b5f Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 10 Oct 2024 16:29:09 +0800 Subject: [PATCH 09/15] Add processing tickets from email --- spp_grm/__manifest__.py | 1 + spp_grm/data/grm_data.xml | 3 ++ spp_grm/data/mail_alias.xml | 15 ++++++ spp_grm/models/grm_ticket.py | 89 +++++++++++++++++++++++++++++++++--- 4 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 spp_grm/data/mail_alias.xml diff --git a/spp_grm/__manifest__.py b/spp_grm/__manifest__.py index 1f2a88917..7c4f5eb40 100644 --- a/spp_grm/__manifest__.py +++ b/spp_grm/__manifest__.py @@ -16,6 +16,7 @@ "depends": ["base", "mail", "g2p_registry_base", "g2p_registry_individual", "g2p_registry_group"], "data": [ "data/grm_data.xml", + "data/mail_alias.xml", "security/grm_security.xml", "security/ir.model.access.csv", "views/res_partner_views.xml", diff --git a/spp_grm/data/grm_data.xml b/spp_grm/data/grm_data.xml index 9e32032c0..ec3b64e7b 100644 --- a/spp_grm/data/grm_data.xml +++ b/spp_grm/data/grm_data.xml @@ -73,6 +73,9 @@ Web + + Email + Other diff --git a/spp_grm/data/mail_alias.xml b/spp_grm/data/mail_alias.xml new file mode 100644 index 000000000..49ab661f1 --- /dev/null +++ b/spp_grm/data/mail_alias.xml @@ -0,0 +1,15 @@ + + + + yourdomain.com + + + + + helpdesk + + everyone + 0 + + + diff --git a/spp_grm/models/grm_ticket.py b/spp_grm/models/grm_ticket.py index 9b44bc414..187156545 100644 --- a/spp_grm/models/grm_ticket.py +++ b/spp_grm/models/grm_ticket.py @@ -1,4 +1,5 @@ import logging +from email.utils import parseaddr from odoo import api, fields, models @@ -12,7 +13,76 @@ class SPPGRMTicket(models.Model): _rec_names_search = ["number", "name"] _order = "priority desc, sequence, number desc, id desc" _mail_post_access = "read" - _inherit = ["mail.thread.cc", "mail.activity.mixin"] + _inherit = ["mail.thread", "mail.activity.mixin"] + + @api.model + def message_new(self, msg_dict, custom_values=None): + """Create a new ticket from an inbound email""" + _logger.debug("Creating new ticket from email") + # Extract values from the email + subject = msg_dict.get("subject") or "No Subject" + body = msg_dict.get("body") or "No Content" + email_from = msg_dict.get("email_from") + email_address = parseaddr(email_from)[1] + _logger.debug(f"Email from: {email_address}") + # TODO: What to do if the registrant is a group? + partner = self.env["res.partner"].search([("email", "=", email_address)], limit=1) + if not partner: + self._send_custom_error_notification(email_address, subject, email_from) + _logger.warning( + "No matching registrant found for email: %s. Email processed but a ticket is not created." + % email_address + ) + + # Prepare default values for the new ticket + vals = { + "name": subject, + "description": body, + "partner_id": partner.id if partner else False, + "partner_email": email_from, + "channel_id": self.env.ref("spp_grm.grm_ticket_channel_email").id, + "priority": "1", # Default priority + } + + if custom_values: + vals.update(custom_values) + + # Create the new GRM ticket + ticket = super().message_new(msg_dict, custom_values=vals) + if ticket: + _logger.info(f"New ticket created from email: {ticket.number}") + else: + _logger.info("No ticket was created from email.") + return ticket + + def message_update(self, msg_dict, update_vals=None): + """Update an existing ticket from an inbound email reply""" + res = super().message_update(msg_dict, update_vals=update_vals) + _logger.info(f"Ticket {self.number} updated from email") + return res + + def _send_custom_error_notification(self, email_address, subject, email_from): + """Send a custom email notification to the sender of the email if no registrant is found. + :param email_address: The email address of the sender + :param subject: The subject of the email + :param email_from: The email address of the recipient + """ + mail_values = { + "subject": subject, + "body_html": """ +

Dear Sender,

+

We could not process your request because your email address %s is not + in our record of registrants.

+

Kind Regards

+ """ + % email_address, + "email_to": email_address, + "email_from": email_from, + } + + # Send the email + mail = self.env["mail.mail"].create(mail_values) + mail.send() def _default_stage_id(self): stages = self.env["spp.grm.ticket.stage"].search([]) @@ -107,12 +177,17 @@ def _creation_subtype(self): @api.model_create_multi def create(self, vals_list): for vals in vals_list: - if vals.get("number", "/") == "/": - vals["number"] = self._prepare_ticket_number() - _logger.debug(f"Creating ticket {vals['number']}") - if vals.get("user_id") and not vals.get("assigned_date"): - vals["assigned_date"] = fields.Datetime.now() - return super().create(vals_list) + proceed = True + if not vals.get("partner_id"): + proceed = False + else: + if vals.get("number", "/") == "/": + vals["number"] = self._prepare_ticket_number() + _logger.debug(f"Creating ticket {vals['number']}") + if vals.get("user_id") and not vals.get("assigned_date"): + vals["assigned_date"] = fields.Datetime.now() + if proceed: + return super().create(vals_list) def copy(self, default=None): self.ensure_one() From a44af25f6ffd49727d443f621c45326a9bb1f8fc Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 10 Oct 2024 22:26:48 +0800 Subject: [PATCH 10/15] Add the web form ticket submission --- spp_grm/__init__.py | 1 + spp_grm/__manifest__.py | 3 +- spp_grm/controllers/__init__.py | 1 + spp_grm/controllers/grm_portal.py | 47 +++++++ spp_grm/data/grm_data.xml | 3 + spp_grm/models/grm_ticket.py | 5 + spp_grm/security/ir.model.access.csv | 6 + spp_grm/static/src/img/ticket.svg | 13 ++ spp_grm/views/grm_portal_templates.xml | 178 +++++++++++++++++++++++++ 9 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 spp_grm/controllers/__init__.py create mode 100644 spp_grm/controllers/grm_portal.py create mode 100644 spp_grm/static/src/img/ticket.svg create mode 100644 spp_grm/views/grm_portal_templates.xml diff --git a/spp_grm/__init__.py b/spp_grm/__init__.py index 0650744f6..91c5580fe 100644 --- a/spp_grm/__init__.py +++ b/spp_grm/__init__.py @@ -1 +1,2 @@ +from . import controllers from . import models diff --git a/spp_grm/__manifest__.py b/spp_grm/__manifest__.py index 7c4f5eb40..7fa2b401b 100644 --- a/spp_grm/__manifest__.py +++ b/spp_grm/__manifest__.py @@ -13,7 +13,7 @@ "category": "OpenSPP", "external_dependencies": {"python": []}, "maintainers": ["jeremi", "gonzalesedwin1123"], - "depends": ["base", "mail", "g2p_registry_base", "g2p_registry_individual", "g2p_registry_group"], + "depends": ["base", "mail", "portal", "g2p_registry_base", "g2p_registry_individual", "g2p_registry_group"], "data": [ "data/grm_data.xml", "data/mail_alias.xml", @@ -26,6 +26,7 @@ "views/grm_ticket_channel_views.xml", "views/grm_ticket_tag_views.xml", "views/grm_ticket_views.xml", + "views/grm_portal_templates.xml", ], "assets": {}, "demo": [], diff --git a/spp_grm/controllers/__init__.py b/spp_grm/controllers/__init__.py new file mode 100644 index 000000000..cd7c6814c --- /dev/null +++ b/spp_grm/controllers/__init__.py @@ -0,0 +1 @@ +from . import grm_portal diff --git a/spp_grm/controllers/grm_portal.py b/spp_grm/controllers/grm_portal.py new file mode 100644 index 000000000..50b0c0492 --- /dev/null +++ b/spp_grm/controllers/grm_portal.py @@ -0,0 +1,47 @@ +from odoo import http +from odoo.http import request + +from odoo.addons.portal.controllers.portal import CustomerPortal + + +class SPPGrmPortal(CustomerPortal): + @http.route(["/my/tickets", "/my/tickets/page/"], type="http", auth="user", website=True) + def portal_my_tickets(self, page=1, **kw): + partner = request.env.user.partner_id + ticket = request.env["spp.grm.ticket"] + domain = [("partner_id", "=", partner.id)] + + # Pagination logic + tickets = ticket.search(domain) + values = { + "tickets": tickets, + "page_name": "tickets", + } + return request.render("spp_grm.portal_my_tickets", values) + + @http.route(["/my/ticket/new"], type="http", auth="user", website=True) + def portal_ticket_new(self, **kw): + categories = request.env["spp.grm.ticket.category"].search([]) + channels = request.env["spp.grm.ticket.channel"].search([]) + return request.render( + "spp_grm.portal_create_ticket", + { + "categories": categories, + "channels": channels, + "page_name": "tickets", + "ticket": "new", + }, + ) + + @http.route(["/my/ticket/submit"], type="http", auth="user", website=True, csrf=True) + def portal_ticket_submit(self, **kw): + partner = request.env.user.partner_id + vals = { + "name": kw.get("ticket_name"), + "description": kw.get("description"), + "category_id": kw.get("category_id"), + "channel_id": request.env.ref("spp_grm.grm_ticket_channel_web").id, + "partner_id": partner.id, + } + request.env["spp.grm.ticket"].sudo().create(vals) + return request.redirect("/my/tickets") diff --git a/spp_grm/data/grm_data.xml b/spp_grm/data/grm_data.xml index ec3b64e7b..f28456c1a 100644 --- a/spp_grm/data/grm_data.xml +++ b/spp_grm/data/grm_data.xml @@ -76,6 +76,9 @@ Email + + Phone + Other diff --git a/spp_grm/models/grm_ticket.py b/spp_grm/models/grm_ticket.py index 187156545..1c29c6362 100644 --- a/spp_grm/models/grm_ticket.py +++ b/spp_grm/models/grm_ticket.py @@ -221,3 +221,8 @@ def assign_to_me(self): def _prepare_ticket_number(self): # Generate ticket number return self.env["ir.sequence"].next_by_code("spp.grm.ticket.sequence") + + def get_portal_url(self): + """Get the URL for the ticket's portal page.""" + self.ensure_one() + return "/my/tickets/%s" % self.id diff --git a/spp_grm/security/ir.model.access.csv b/spp_grm/security/ir.model.access.csv index ff3054862..3c34fd0f5 100644 --- a/spp_grm/security/ir.model.access.csv +++ b/spp_grm/security/ir.model.access.csv @@ -16,3 +16,9 @@ access_spp_grm_ticket_stage_base_user,GRM Ticket Stage Base User Access,model_sp access_spp_grm_ticket_tag_base_user,GRM Ticket Tags Base User Access,model_spp_grm_ticket_tag,base.group_user,1,1,1,0 access_spp_grm_ticket_channel_base_user,GRM Ticket Channels Base User Access,model_spp_grm_ticket_channel,base.group_user,1,0,0,0 access_spp_grm_ticket_category_base_user,GRM Ticket Category Base User Access,model_spp_grm_ticket_category,base.group_user,1,0,0,0 + +access_spp_grm_ticket_portal_user,GRM Ticket Portal User Access,model_spp_grm_ticket,base.group_portal,1,1,1,0 +access_spp_grm_ticket_stage_portal_user,GRM Ticket Stage Portal User Access,model_spp_grm_ticket_stage,base.group_portal,1,0,0,0 +access_spp_grm_ticket_tag_portal_user,GRM Ticket Tags Portal User Access,model_spp_grm_ticket_tag,base.group_portal,1,1,1,0 +access_spp_grm_ticket_channel_portal_user,GRM Ticket Channels Portal User Access,model_spp_grm_ticket_channel,base.group_portal,1,0,0,0 +access_spp_grm_ticket_category_portal_user,GRM Ticket Category Portal User Access,model_spp_grm_ticket_category,base.group_portal,1,0,0,0 diff --git a/spp_grm/static/src/img/ticket.svg b/spp_grm/static/src/img/ticket.svg new file mode 100644 index 000000000..51d1968db --- /dev/null +++ b/spp_grm/static/src/img/ticket.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/spp_grm/views/grm_portal_templates.xml b/spp_grm/views/grm_portal_templates.xml new file mode 100644 index 000000000..87060fe32 --- /dev/null +++ b/spp_grm/views/grm_portal_templates.xml @@ -0,0 +1,178 @@ + + + + + + + + + + + +