helpdesk_ticket.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from odoo import _, api, fields, models
  4. from odoo.exceptions import ValidationError
  5. class HelpdeskTicket(models.Model):
  6. _inherit = 'helpdesk.ticket'
  7. request_type_id = fields.Many2one(
  8. 'helpdesk.request.type',
  9. string=_('Request Type'),
  10. required=True,
  11. tracking=True,
  12. help=_("Type of ticket (e.g., Incident, Improvement)"),
  13. default=lambda self: self._default_request_type_id()
  14. )
  15. request_type_code = fields.Char(
  16. related='request_type_id.code',
  17. string=_('Request Type Code'),
  18. store=True,
  19. readonly=True,
  20. help=_("Code of the request type for conditional logic")
  21. )
  22. affected_module_id = fields.Many2one(
  23. 'helpdesk.affected.module',
  24. string=_('Affected Module'),
  25. required=False,
  26. domain=[('active', '=', True)],
  27. help=_("Odoo module where the issue or improvement occurs")
  28. )
  29. affected_user_email = fields.Char(
  30. string=_('Affected User Email'),
  31. tracking=True,
  32. help=_("Email address of the affected user (from another Odoo instance or external)")
  33. )
  34. business_impact = fields.Selection(
  35. [
  36. ('0', _('Critical')),
  37. ('1', _('High')),
  38. ('2', _('Normal')),
  39. ],
  40. string=_('Business Impact'),
  41. default='2',
  42. tracking=True,
  43. help=_("Urgency reported by the client")
  44. )
  45. reproduce_steps = fields.Html(
  46. string=_('Steps to Reproduce'),
  47. help=_("Detailed steps to reproduce the issue (only for Incidents)")
  48. )
  49. business_goal = fields.Html(
  50. string=_('Business Goal'),
  51. help=_("Business objective for this improvement (only for Improvements)")
  52. )
  53. client_authorization = fields.Boolean(
  54. string=_('Client Authorization'),
  55. default=False,
  56. help=_("Checkbox from web form indicating client authorization")
  57. )
  58. estimated_hours = fields.Float(
  59. string=_('Estimated Hours'),
  60. help=_("Hours quoted after analysis")
  61. )
  62. approval_status = fields.Selection(
  63. [
  64. ('draft', _('N/A')),
  65. ('waiting', _('Waiting for Approval')),
  66. ('approved', _('Approved')),
  67. ('rejected', _('Rejected')),
  68. ],
  69. string=_('Approval Status'),
  70. default='draft',
  71. tracking=True,
  72. help=_("Status of the approval workflow")
  73. )
  74. customer_approval_status = fields.Selection(
  75. [
  76. ('pending', _('Pending Approval')),
  77. ('approved', _('Approved')),
  78. ('rejected', _('Rejected')),
  79. ],
  80. string=_('Customer Approval'),
  81. default='pending',
  82. tracking=True,
  83. help=_("Customer approval status for stages that require it")
  84. )
  85. customer_rejection_reason = fields.Text(
  86. string=_('Customer Rejection Reason'),
  87. help=_("Reason provided by customer for rejecting the ticket")
  88. )
  89. has_template = fields.Boolean(
  90. string=_('Has Template'),
  91. compute='_compute_has_template',
  92. help=_("Indicates if the team has a template assigned")
  93. )
  94. attachment_ids = fields.One2many(
  95. 'ir.attachment',
  96. 'res_id',
  97. string=_('Attachments'),
  98. domain=[('res_model', '=', 'helpdesk.ticket')],
  99. help=_("Files attached to this ticket")
  100. )
  101. @api.depends('team_id.workflow_template_id', 'team_id.workflow_template_id.field_ids')
  102. def _compute_has_template(self):
  103. """Compute if team has form fields configured in workflow template"""
  104. for ticket in self:
  105. ticket.has_template = bool(
  106. ticket.team_id and
  107. ticket.team_id.workflow_template_id and
  108. ticket.team_id.workflow_template_id.field_ids
  109. )
  110. @api.model
  111. def _default_request_type_id(self):
  112. """Default to 'Incident' type if available"""
  113. incident_type = self.env.ref(
  114. 'helpdesk_extras.type_incident',
  115. raise_if_not_found=False
  116. )
  117. return incident_type.id if incident_type else False
  118. def _get_template_fields(self):
  119. """Get form fields for this ticket's team"""
  120. self.ensure_one()
  121. if not self.team_id or not self.team_id.workflow_template_id:
  122. return self.env['helpdesk.workflow.template.field']
  123. return self.team_id.workflow_template_id.field_ids.sorted('sequence')
  124. @api.onchange('request_type_id', 'business_impact')
  125. def _onchange_compute_priority(self):
  126. """
  127. Auto-calculate priority based on request type and business impact.
  128. Only applies when both fields have values.
  129. Mapping:
  130. - Incident + Critical = 3 (Urgent)
  131. - Incident + High = 2 (High)
  132. - Incident + Normal = 1 (Normal)
  133. - Improvement + Critical = 2 (High)
  134. - Improvement + High = 1 (Normal)
  135. - Improvement + Normal = 0 (Low)
  136. """
  137. priority = self._compute_priority_from_impact()
  138. if priority is not None:
  139. self.priority = priority
  140. def _compute_priority_from_impact(self, request_type_id=None, business_impact=None):
  141. """
  142. Helper method to compute priority from request type and business impact.
  143. Can be used by onchange, create, and write methods.
  144. Returns the priority string or None if not applicable.
  145. """
  146. # Use provided values or instance values
  147. if request_type_id is None:
  148. request_type = self.request_type_id
  149. else:
  150. request_type = self.env['helpdesk.request.type'].browse(request_type_id) if isinstance(request_type_id, int) else request_type_id
  151. if business_impact is None:
  152. business_impact = self.business_impact
  153. if not request_type or not business_impact:
  154. return None
  155. type_code = request_type.code if hasattr(request_type, 'code') else ''
  156. if not type_code:
  157. return None
  158. # Priority mapping based on business_impact
  159. # business_impact: '0' = Critical, '1' = High, '2' = Normal
  160. # priority: '0' = Low, '1' = Normal, '2' = High, '3' = Urgent
  161. priority_map = {
  162. # Incidents get +1 priority boost
  163. ('incident', '0'): '3', # Incident + Critical = Urgent
  164. ('incident', '1'): '2', # Incident + High = High
  165. ('incident', '2'): '1', # Incident + Normal = Normal
  166. # Improvements have standard mapping
  167. ('improvement', '0'): '2', # Improvement + Critical = High
  168. ('improvement', '1'): '1', # Improvement + High = Normal
  169. ('improvement', '2'): '0', # Improvement + Normal = Low
  170. }
  171. key = (type_code, business_impact)
  172. return priority_map.get(key)
  173. @api.model_create_multi
  174. def create(self, vals_list):
  175. """Override create to auto-calculate priority for website form submissions."""
  176. for vals in vals_list:
  177. # Only calculate if priority not explicitly set and we have both fields
  178. if 'priority' not in vals or vals.get('priority') == '0':
  179. request_type_id = vals.get('request_type_id')
  180. business_impact = vals.get('business_impact')
  181. if request_type_id and business_impact:
  182. priority = self._compute_priority_from_impact(request_type_id, business_impact)
  183. if priority is not None:
  184. vals['priority'] = priority
  185. return super().create(vals_list)
  186. def write(self, vals):
  187. """Override write to recalculate priority and validate customer approval."""
  188. # Validar aprobación del cliente antes de cambiar de etapa
  189. if 'stage_id' in vals:
  190. for ticket in self:
  191. # Si la etapa actual requiere aprobación del cliente
  192. if ticket.stage_id and ticket.stage_id.requires_customer_approval:
  193. # Verificar que el ticket esté aprobado antes de permitir el cambio
  194. if ticket.customer_approval_status != 'approved':
  195. # Validation message
  196. raise ValidationError(_(
  197. "Cannot move ticket to next stage. Customer approval required.\n"
  198. "Current approval status: %s"
  199. ) % dict(ticket._fields['customer_approval_status'].selection).get(ticket.customer_approval_status))
  200. result = super().write(vals)
  201. # Resetear estado de aprobación al entrar a una nueva etapa que requiere aprobación
  202. if 'stage_id' in vals:
  203. for ticket in self:
  204. if ticket.stage_id and ticket.stage_id.requires_customer_approval:
  205. # Si el ticket acaba de entrar a una etapa que requiere aprobación,
  206. # resetear el estado a 'pending' (a menos que ya esté aprobado desde antes)
  207. if ticket.customer_approval_status == 'approved':
  208. # Mantener aprobado si ya lo estaba
  209. pass
  210. else:
  211. # Resetear a pending para la nueva etapa
  212. super(HelpdeskTicket, ticket).write({
  213. 'customer_approval_status': 'pending',
  214. 'customer_rejection_reason': False,
  215. })
  216. # If either field was updated, recalculate priority for affected records
  217. if 'request_type_id' in vals or 'business_impact' in vals:
  218. for record in self:
  219. priority = record._compute_priority_from_impact()
  220. if priority is not None and record.priority != priority:
  221. # Use super().write to avoid recursion
  222. super(HelpdeskTicket, record).write({'priority': priority})
  223. return result