helpdesk_workflow_template.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. # -*- coding: utf-8 -*-
  2. from odoo import api, fields, models, Command
  3. class HelpdeskWorkflowTemplate(models.Model):
  4. _name = 'helpdesk.workflow.template'
  5. _description = 'Helpdesk Workflow Template'
  6. _order = 'sequence, name'
  7. name = fields.Char(
  8. string='Template Name',
  9. required=True,
  10. translate=True,
  11. help='Name of the workflow template'
  12. )
  13. sequence = fields.Integer(
  14. string='Sequence',
  15. default=10,
  16. help='Order of templates'
  17. )
  18. description = fields.Text(
  19. string='Description',
  20. translate=True,
  21. help='Description of the workflow template'
  22. )
  23. active = fields.Boolean(
  24. string='Active',
  25. default=True,
  26. help='If unchecked, this template will be hidden'
  27. )
  28. stage_template_ids = fields.One2many(
  29. 'helpdesk.workflow.template.stage',
  30. 'template_id',
  31. string='Stages',
  32. help='Stages included in this workflow template'
  33. )
  34. sla_template_ids = fields.One2many(
  35. 'helpdesk.workflow.template.sla',
  36. 'template_id',
  37. string='SLA Policies',
  38. help='SLA policies included in this workflow template'
  39. )
  40. field_ids = fields.One2many(
  41. 'helpdesk.workflow.template.field',
  42. 'workflow_template_id',
  43. string='Form Fields',
  44. copy=True,
  45. help='Form fields for website ticket submission'
  46. )
  47. stage_count = fields.Integer(
  48. string='Stages Count',
  49. compute='_compute_counts',
  50. store=False
  51. )
  52. sla_count = fields.Integer(
  53. string='SLA Policies Count',
  54. compute='_compute_counts',
  55. store=False
  56. )
  57. field_count = fields.Integer(
  58. string='Form Fields Count',
  59. compute='_compute_counts',
  60. store=False
  61. )
  62. team_ids = fields.One2many(
  63. 'helpdesk.team',
  64. 'workflow_template_id',
  65. string='Teams Using This Template',
  66. readonly=True
  67. )
  68. team_count = fields.Integer(
  69. string='Teams Count',
  70. compute='_compute_counts',
  71. store=False
  72. )
  73. documentation_html = fields.Html(
  74. string='Documentation',
  75. compute='_compute_documentation_html',
  76. store=False,
  77. sanitize=False,
  78. help='Auto-generated documentation of this workflow template'
  79. )
  80. @api.depends('name', 'stage_template_ids', 'sla_template_ids', 'field_ids', 'team_ids')
  81. def _compute_documentation_html(self):
  82. """Generate dynamic documentation HTML for the workflow template"""
  83. for template in self:
  84. if not template.id:
  85. template.documentation_html = ''
  86. continue
  87. html_parts = []
  88. # Header with summary
  89. html_parts.append(self._build_doc_header(template))
  90. # Stage flow with SLAs
  91. if template.stage_template_ids:
  92. html_parts.append(self._build_doc_stages(template))
  93. # SLA policies table
  94. if template.sla_template_ids:
  95. html_parts.append(self._build_doc_slas(template))
  96. # Form fields grouped by visibility
  97. if template.field_ids:
  98. html_parts.append(self._build_doc_fields(template))
  99. # Teams using this template
  100. if template.team_ids:
  101. html_parts.append(self._build_doc_teams(template))
  102. template.documentation_html = ''.join(html_parts)
  103. def _build_doc_header(self, template):
  104. """Build documentation header with summary"""
  105. return f'''
  106. <div class="o_documentation_header mb-4">
  107. <h3>📋 Documentación - {template.name or "Sin nombre"}</h3>
  108. <hr style="border-top: 2px solid #875A7B;"/>
  109. <div class="d-flex gap-3 mb-3">
  110. <span class="badge bg-primary fs-6">{len(template.stage_template_ids)} Etapas</span>
  111. <span class="badge bg-success fs-6">{len(template.sla_template_ids)} SLAs</span>
  112. <span class="badge bg-info fs-6">{len(template.field_ids)} Campos</span>
  113. <span class="badge bg-secondary fs-6">{len(template.team_ids)} Equipos</span>
  114. </div>
  115. </div>
  116. '''
  117. def _build_doc_stages(self, template):
  118. """Build stage flow diagram with integrated SLAs"""
  119. stages = template.stage_template_ids.sorted('sequence')
  120. # Create SLA lookup by stage
  121. sla_by_stage = {}
  122. for sla in template.sla_template_ids:
  123. stage_id = sla.stage_template_id.id
  124. if stage_id not in sla_by_stage:
  125. sla_by_stage[stage_id] = []
  126. sla_by_stage[stage_id].append(sla)
  127. # Build flow diagram
  128. flow_items = []
  129. for i, stage in enumerate(stages):
  130. # Stage box styling
  131. if stage.fold:
  132. bg_color = '#6c757d' # Gray for closed stages
  133. icon = '📦'
  134. elif i == 0:
  135. bg_color = '#17a2b8' # Blue for first stage
  136. icon = '📥'
  137. else:
  138. bg_color = '#28a745' # Green for normal stages
  139. icon = '⚙️'
  140. # SLA info for this stage
  141. sla_html = ''
  142. if stage.id in sla_by_stage:
  143. slas = sla_by_stage[stage.id]
  144. sla_texts = []
  145. for sla in slas[:2]: # Show max 2 SLAs
  146. priority_text = self._get_priority_text(sla.priority)
  147. sla_texts.append(f"⏱️ {sla.time:.0f}h ({priority_text})")
  148. sla_html = '<br/><small class="text-muted">' + '<br/>'.join(sla_texts) + '</small>'
  149. stage_html = f'''
  150. <div class="text-center" style="min-width: 100px;">
  151. <div class="rounded p-2" style="background-color: {bg_color}; color: white;">
  152. <strong>{icon} {stage.name}</strong>
  153. </div>
  154. {sla_html}
  155. </div>
  156. '''
  157. flow_items.append(stage_html)
  158. flow_html = '<span class="mx-2 fs-4">→</span>'.join(flow_items)
  159. return f'''
  160. <div class="o_documentation_stages mb-4">
  161. <h4>🔄 Flujo de Trabajo</h4>
  162. <div class="d-flex align-items-start flex-wrap gap-2 p-3 bg-light rounded">
  163. {flow_html}
  164. </div>
  165. <small class="text-muted">Las etapas en gris son etapas de cierre (plegadas en Kanban)</small>
  166. </div>
  167. '''
  168. def _build_doc_slas(self, template):
  169. """Build SLA policies table"""
  170. slas = template.sla_template_ids.sorted('sequence')
  171. rows = []
  172. for sla in slas:
  173. priority_badge = self._get_priority_badge(sla.priority)
  174. excluded = ', '.join(sla.exclude_stage_template_ids.mapped('name')) or '-'
  175. rows.append(f'''
  176. <tr>
  177. <td><strong>{sla.name}</strong></td>
  178. <td>{sla.stage_template_id.name or '-'}</td>
  179. <td><code>{sla.time:.0f}h</code></td>
  180. <td>{priority_badge}</td>
  181. <td><small class="text-muted">{excluded}</small></td>
  182. </tr>
  183. ''')
  184. return f'''
  185. <div class="o_documentation_slas mb-4">
  186. <h4>⏱️ Políticas SLA</h4>
  187. <table class="table table-sm table-bordered">
  188. <thead class="table-light">
  189. <tr>
  190. <th>Nombre</th>
  191. <th>Etapa Objetivo</th>
  192. <th>Tiempo</th>
  193. <th>Prioridad</th>
  194. <th>Pausado en</th>
  195. </tr>
  196. </thead>
  197. <tbody>
  198. {''.join(rows)}
  199. </tbody>
  200. </table>
  201. </div>
  202. '''
  203. def _build_doc_fields(self, template):
  204. """Build form fields table grouped by visibility dependency"""
  205. fields = template.field_ids.sorted('sequence')
  206. # Group fields by visibility dependency
  207. groups = {} # {dependency_id: {'name': str, 'condition': str, 'fields': []}}
  208. common_fields = []
  209. for field in fields:
  210. if not field.visibility_dependency:
  211. common_fields.append(field)
  212. else:
  213. dep_id = field.visibility_dependency.id
  214. if dep_id not in groups:
  215. # Get the condition display
  216. condition_display = self._get_visibility_display(field)
  217. groups[dep_id] = {
  218. 'name': field.visibility_dependency.field_description or field.visibility_dependency.name,
  219. 'condition': condition_display,
  220. 'fields': []
  221. }
  222. groups[dep_id]['fields'].append(field)
  223. html_parts = ['<div class="o_documentation_fields mb-4">', '<h4>📝 Campos del Formulario</h4>']
  224. # Common fields
  225. if common_fields:
  226. html_parts.append(self._build_fields_table('Campos Comunes', 'Siempre visibles', common_fields, 'bg-primary'))
  227. # Grouped fields
  228. for dep_id, group in groups.items():
  229. title = f"Campos para: {group['condition']}"
  230. html_parts.append(self._build_fields_table(title, '', group['fields'], 'bg-warning'))
  231. html_parts.append('</div>')
  232. return ''.join(html_parts)
  233. def _build_fields_table(self, title, subtitle, fields, badge_class):
  234. """Build a single fields table"""
  235. rows = []
  236. for field in fields:
  237. required_icon = '✅' if field.required else '❌'
  238. model_required = ' 🔒' if field.model_required else ''
  239. label = field.label_custom or (field.field_id.field_description if field.field_id else field.field_name)
  240. help_text = f'<small class="text-muted">{field.help_text[:50]}...</small>' if field.help_text and len(field.help_text) > 50 else (f'<small class="text-muted">{field.help_text}</small>' if field.help_text else '-')
  241. rows.append(f'''
  242. <tr>
  243. <td><code>{field.field_name}</code></td>
  244. <td>{label}</td>
  245. <td class="text-center">{required_icon}{model_required}</td>
  246. <td>{help_text}</td>
  247. </tr>
  248. ''')
  249. subtitle_html = f'<small class="text-muted">({subtitle})</small>' if subtitle else ''
  250. return f'''
  251. <div class="mb-3">
  252. <h6><span class="badge {badge_class}">{title}</span> {subtitle_html}</h6>
  253. <table class="table table-sm table-bordered">
  254. <thead class="table-light">
  255. <tr>
  256. <th style="width: 20%;">Campo</th>
  257. <th style="width: 25%;">Etiqueta</th>
  258. <th style="width: 10%;" class="text-center">Oblig.</th>
  259. <th>Descripción</th>
  260. </tr>
  261. </thead>
  262. <tbody>
  263. {''.join(rows)}
  264. </tbody>
  265. </table>
  266. </div>
  267. '''
  268. def _build_doc_teams(self, template):
  269. """Build teams using this template"""
  270. teams = template.team_ids
  271. rows = []
  272. for team in teams:
  273. form_icon = '✅' if team.use_website_helpdesk_form else '❌'
  274. rows.append(f'''
  275. <tr>
  276. <td><strong>{team.name}</strong></td>
  277. <td class="text-center">{form_icon}</td>
  278. </tr>
  279. ''')
  280. return f'''
  281. <div class="o_documentation_teams mb-4">
  282. <h4>👥 Equipos Usando Este Template</h4>
  283. <table class="table table-sm table-bordered" style="max-width: 400px;">
  284. <thead class="table-light">
  285. <tr>
  286. <th>Equipo</th>
  287. <th class="text-center">Formulario Web</th>
  288. </tr>
  289. </thead>
  290. <tbody>
  291. {''.join(rows)}
  292. </tbody>
  293. </table>
  294. </div>
  295. '''
  296. def _get_visibility_display(self, field):
  297. """Get human readable visibility condition"""
  298. if not field.visibility_dependency:
  299. return 'Siempre visible'
  300. dep_name = field.visibility_dependency.field_description or field.visibility_dependency.name
  301. comparator_map = {
  302. 'equal': 'es igual a',
  303. '!equal': 'no es igual a',
  304. 'contains': 'contiene',
  305. '!contains': 'no contiene',
  306. 'set': 'está definido',
  307. '!set': 'no está definido',
  308. 'selected': 'es igual a',
  309. '!selected': 'no es igual a',
  310. }
  311. comp_text = comparator_map.get(field.visibility_comparator, field.visibility_comparator)
  312. # Get condition value display
  313. condition_value = field.visibility_condition
  314. if field.visibility_dependency.ttype == 'many2one' and field.visibility_condition_m2o_id:
  315. try:
  316. related_model = self.env[field.visibility_dependency.relation]
  317. related_record = related_model.browse(field.visibility_condition_m2o_id)
  318. if related_record.exists():
  319. condition_value = related_record.display_name
  320. except:
  321. pass
  322. if field.visibility_comparator in ['set', '!set']:
  323. return f"{dep_name} {comp_text}"
  324. return f"{dep_name} {comp_text} '{condition_value}'"
  325. def _get_priority_text(self, priority):
  326. """Get priority text"""
  327. priority_map = {
  328. '0': 'Baja',
  329. '1': 'Normal',
  330. '2': 'Alta',
  331. '3': 'Urgente',
  332. }
  333. return priority_map.get(priority, 'Todas')
  334. def _get_priority_badge(self, priority):
  335. """Get priority badge HTML"""
  336. priority_map = {
  337. '0': ('Baja', 'bg-secondary'),
  338. '1': ('Normal', 'bg-info'),
  339. '2': ('Alta', 'bg-warning text-dark'),
  340. '3': ('Urgente', 'bg-danger'),
  341. }
  342. text, cls = priority_map.get(priority, ('Todas', 'bg-light text-dark'))
  343. return f'<span class="badge {cls}">{text}</span>'
  344. @api.model
  345. def default_get(self, fields_list):
  346. """Set default required form fields when creating a new workflow template"""
  347. res = super().default_get(fields_list)
  348. # Only set defaults if field_ids is in fields_list and not already provided
  349. if 'field_ids' in fields_list and not self.env.context.get('default_field_ids'):
  350. # Required fields for the website form builder
  351. required_field_names = ['partner_name', 'partner_email', 'name', 'description']
  352. # Get field records from helpdesk.ticket model
  353. ticket_model = self.env['ir.model'].search([('model', '=', 'helpdesk.ticket')], limit=1)
  354. if ticket_model:
  355. required_fields = self.env['ir.model.fields'].search([
  356. ('model_id', '=', ticket_model.id),
  357. ('name', 'in', required_field_names),
  358. ('website_form_blacklisted', '=', False)
  359. ])
  360. field_map = {f.name: f for f in required_fields}
  361. field_ids_commands = []
  362. sequence = 10
  363. # Add partner_name (required, sequence 10)
  364. if 'partner_name' in field_map:
  365. field_ids_commands.append((0, 0, {
  366. 'field_id': field_map['partner_name'].id,
  367. 'required': True,
  368. 'sequence': sequence
  369. }))
  370. sequence += 10
  371. # Add partner_email (required, sequence 20)
  372. if 'partner_email' in field_map:
  373. field_ids_commands.append((0, 0, {
  374. 'field_id': field_map['partner_email'].id,
  375. 'required': True,
  376. 'sequence': sequence
  377. }))
  378. sequence += 10
  379. # Add name (model_required, sequence 30)
  380. if 'name' in field_map:
  381. name_field = field_map['name']
  382. field_ids_commands.append((0, 0, {
  383. 'field_id': name_field.id,
  384. 'required': True,
  385. 'model_required': name_field.required,
  386. 'sequence': sequence
  387. }))
  388. sequence += 10
  389. # Add description (required, sequence 40)
  390. if 'description' in field_map:
  391. field_ids_commands.append((0, 0, {
  392. 'field_id': field_map['description'].id,
  393. 'required': True,
  394. 'sequence': sequence
  395. }))
  396. if field_ids_commands:
  397. res['field_ids'] = field_ids_commands
  398. return res
  399. @api.depends('stage_template_ids', 'sla_template_ids', 'field_ids', 'team_ids')
  400. def _compute_counts(self):
  401. for template in self:
  402. template.stage_count = len(template.stage_template_ids)
  403. template.sla_count = len(template.sla_template_ids)
  404. template.field_count = len(template.field_ids)
  405. template.team_count = len(template.team_ids)
  406. def action_view_teams(self):
  407. """Open teams using this template"""
  408. self.ensure_one()
  409. action = self.env['ir.actions.actions']._for_xml_id('helpdesk.helpdesk_team_action')
  410. action.update({
  411. 'domain': [('workflow_template_id', '=', self.id)],
  412. 'context': {
  413. 'default_workflow_template_id': self.id,
  414. 'search_default_workflow_template_id': self.id,
  415. },
  416. })
  417. return action
  418. def copy_data(self, default=None):
  419. """Override copy to duplicate stages and SLAs"""
  420. defaults = super().copy_data(default=default)
  421. # Note: Stages and SLAs will be copied automatically via ondelete='cascade'
  422. # We just need to update the name
  423. for template, vals in zip(self, defaults):
  424. vals['name'] = self.env._("%s (copy)", template.name)
  425. return defaults