| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486 |
- # -*- coding: utf-8 -*-
- from odoo import api, fields, models, Command
- class HelpdeskWorkflowTemplate(models.Model):
- _name = 'helpdesk.workflow.template'
- _description = 'Helpdesk Workflow Template'
- _order = 'sequence, name'
- name = fields.Char(
- string='Template Name',
- required=True,
- translate=True,
- help='Name of the workflow template'
- )
- sequence = fields.Integer(
- string='Sequence',
- default=10,
- help='Order of templates'
- )
- description = fields.Text(
- string='Description',
- translate=True,
- help='Description of the workflow template'
- )
- active = fields.Boolean(
- string='Active',
- default=True,
- help='If unchecked, this template will be hidden'
- )
- stage_template_ids = fields.One2many(
- 'helpdesk.workflow.template.stage',
- 'template_id',
- string='Stages',
- help='Stages included in this workflow template'
- )
- sla_template_ids = fields.One2many(
- 'helpdesk.workflow.template.sla',
- 'template_id',
- string='SLA Policies',
- help='SLA policies included in this workflow template'
- )
- field_ids = fields.One2many(
- 'helpdesk.workflow.template.field',
- 'workflow_template_id',
- string='Form Fields',
- copy=True,
- help='Form fields for website ticket submission'
- )
- stage_count = fields.Integer(
- string='Stages Count',
- compute='_compute_counts',
- store=False
- )
- sla_count = fields.Integer(
- string='SLA Policies Count',
- compute='_compute_counts',
- store=False
- )
- field_count = fields.Integer(
- string='Form Fields Count',
- compute='_compute_counts',
- store=False
- )
- team_ids = fields.One2many(
- 'helpdesk.team',
- 'workflow_template_id',
- string='Teams Using This Template',
- readonly=True
- )
- team_count = fields.Integer(
- string='Teams Count',
- compute='_compute_counts',
- store=False
- )
- documentation_html = fields.Html(
- string='Documentation',
- compute='_compute_documentation_html',
- store=False,
- sanitize=False,
- help='Auto-generated documentation of this workflow template'
- )
- @api.depends('name', 'stage_template_ids', 'sla_template_ids', 'field_ids', 'team_ids')
- def _compute_documentation_html(self):
- """Generate dynamic documentation HTML for the workflow template"""
- for template in self:
- if not template.id:
- template.documentation_html = ''
- continue
-
- html_parts = []
-
- # Header with summary
- html_parts.append(self._build_doc_header(template))
-
- # Stage flow with SLAs
- if template.stage_template_ids:
- html_parts.append(self._build_doc_stages(template))
-
- # SLA policies table
- if template.sla_template_ids:
- html_parts.append(self._build_doc_slas(template))
-
- # Form fields grouped by visibility
- if template.field_ids:
- html_parts.append(self._build_doc_fields(template))
-
- # Teams using this template
- if template.team_ids:
- html_parts.append(self._build_doc_teams(template))
-
- template.documentation_html = ''.join(html_parts)
-
- def _build_doc_header(self, template):
- """Build documentation header with summary"""
- return f'''
- <div class="o_documentation_header mb-4">
- <h3>📋 Documentación - {template.name or "Sin nombre"}</h3>
- <hr style="border-top: 2px solid #875A7B;"/>
- <div class="d-flex gap-3 mb-3">
- <span class="badge bg-primary fs-6">{len(template.stage_template_ids)} Etapas</span>
- <span class="badge bg-success fs-6">{len(template.sla_template_ids)} SLAs</span>
- <span class="badge bg-info fs-6">{len(template.field_ids)} Campos</span>
- <span class="badge bg-secondary fs-6">{len(template.team_ids)} Equipos</span>
- </div>
- </div>
- '''
-
- def _build_doc_stages(self, template):
- """Build stage flow diagram with integrated SLAs"""
- stages = template.stage_template_ids.sorted('sequence')
-
- # Create SLA lookup by stage
- sla_by_stage = {}
- for sla in template.sla_template_ids:
- stage_id = sla.stage_template_id.id
- if stage_id not in sla_by_stage:
- sla_by_stage[stage_id] = []
- sla_by_stage[stage_id].append(sla)
-
- # Build flow diagram
- flow_items = []
- for i, stage in enumerate(stages):
- # Stage box styling
- if stage.fold:
- bg_color = '#6c757d' # Gray for closed stages
- icon = '📦'
- elif i == 0:
- bg_color = '#17a2b8' # Blue for first stage
- icon = '📥'
- else:
- bg_color = '#28a745' # Green for normal stages
- icon = '⚙️'
-
- # SLA info for this stage
- sla_html = ''
- if stage.id in sla_by_stage:
- slas = sla_by_stage[stage.id]
- sla_texts = []
- for sla in slas[:2]: # Show max 2 SLAs
- priority_text = self._get_priority_text(sla.priority)
- sla_texts.append(f"⏱️ {sla.time:.0f}h ({priority_text})")
- sla_html = '<br/><small class="text-muted">' + '<br/>'.join(sla_texts) + '</small>'
-
- stage_html = f'''
- <div class="text-center" style="min-width: 100px;">
- <div class="rounded p-2" style="background-color: {bg_color}; color: white;">
- <strong>{icon} {stage.name}</strong>
- </div>
- {sla_html}
- </div>
- '''
- flow_items.append(stage_html)
-
- flow_html = '<span class="mx-2 fs-4">→</span>'.join(flow_items)
-
- return f'''
- <div class="o_documentation_stages mb-4">
- <h4>🔄 Flujo de Trabajo</h4>
- <div class="d-flex align-items-start flex-wrap gap-2 p-3 bg-light rounded">
- {flow_html}
- </div>
- <small class="text-muted">Las etapas en gris son etapas de cierre (plegadas en Kanban)</small>
- </div>
- '''
-
- def _build_doc_slas(self, template):
- """Build SLA policies table"""
- slas = template.sla_template_ids.sorted('sequence')
-
- rows = []
- for sla in slas:
- priority_badge = self._get_priority_badge(sla.priority)
- excluded = ', '.join(sla.exclude_stage_template_ids.mapped('name')) or '-'
-
- rows.append(f'''
- <tr>
- <td><strong>{sla.name}</strong></td>
- <td>{sla.stage_template_id.name or '-'}</td>
- <td><code>{sla.time:.0f}h</code></td>
- <td>{priority_badge}</td>
- <td><small class="text-muted">{excluded}</small></td>
- </tr>
- ''')
-
- return f'''
- <div class="o_documentation_slas mb-4">
- <h4>⏱️ Políticas SLA</h4>
- <table class="table table-sm table-bordered">
- <thead class="table-light">
- <tr>
- <th>Nombre</th>
- <th>Etapa Objetivo</th>
- <th>Tiempo</th>
- <th>Prioridad</th>
- <th>Pausado en</th>
- </tr>
- </thead>
- <tbody>
- {''.join(rows)}
- </tbody>
- </table>
- </div>
- '''
-
- def _build_doc_fields(self, template):
- """Build form fields table grouped by visibility dependency"""
- fields = template.field_ids.sorted('sequence')
-
- # Group fields by visibility dependency
- groups = {} # {dependency_id: {'name': str, 'condition': str, 'fields': []}}
- common_fields = []
-
- for field in fields:
- if not field.visibility_dependency:
- common_fields.append(field)
- else:
- dep_id = field.visibility_dependency.id
- if dep_id not in groups:
- # Get the condition display
- condition_display = self._get_visibility_display(field)
- groups[dep_id] = {
- 'name': field.visibility_dependency.field_description or field.visibility_dependency.name,
- 'condition': condition_display,
- 'fields': []
- }
- groups[dep_id]['fields'].append(field)
-
- html_parts = ['<div class="o_documentation_fields mb-4">', '<h4>📝 Campos del Formulario</h4>']
-
- # Common fields
- if common_fields:
- html_parts.append(self._build_fields_table('Campos Comunes', 'Siempre visibles', common_fields, 'bg-primary'))
-
- # Grouped fields
- for dep_id, group in groups.items():
- title = f"Campos para: {group['condition']}"
- html_parts.append(self._build_fields_table(title, '', group['fields'], 'bg-warning'))
-
- html_parts.append('</div>')
- return ''.join(html_parts)
-
- def _build_fields_table(self, title, subtitle, fields, badge_class):
- """Build a single fields table"""
- rows = []
- for field in fields:
- required_icon = '✅' if field.required else '❌'
- model_required = ' 🔒' if field.model_required else ''
- label = field.label_custom or (field.field_id.field_description if field.field_id else field.field_name)
- 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 '-')
-
- rows.append(f'''
- <tr>
- <td><code>{field.field_name}</code></td>
- <td>{label}</td>
- <td class="text-center">{required_icon}{model_required}</td>
- <td>{help_text}</td>
- </tr>
- ''')
-
- subtitle_html = f'<small class="text-muted">({subtitle})</small>' if subtitle else ''
-
- return f'''
- <div class="mb-3">
- <h6><span class="badge {badge_class}">{title}</span> {subtitle_html}</h6>
- <table class="table table-sm table-bordered">
- <thead class="table-light">
- <tr>
- <th style="width: 20%;">Campo</th>
- <th style="width: 25%;">Etiqueta</th>
- <th style="width: 10%;" class="text-center">Oblig.</th>
- <th>Descripción</th>
- </tr>
- </thead>
- <tbody>
- {''.join(rows)}
- </tbody>
- </table>
- </div>
- '''
-
- def _build_doc_teams(self, template):
- """Build teams using this template"""
- teams = template.team_ids
-
- rows = []
- for team in teams:
- form_icon = '✅' if team.use_website_helpdesk_form else '❌'
- rows.append(f'''
- <tr>
- <td><strong>{team.name}</strong></td>
- <td class="text-center">{form_icon}</td>
- </tr>
- ''')
-
- return f'''
- <div class="o_documentation_teams mb-4">
- <h4>👥 Equipos Usando Este Template</h4>
- <table class="table table-sm table-bordered" style="max-width: 400px;">
- <thead class="table-light">
- <tr>
- <th>Equipo</th>
- <th class="text-center">Formulario Web</th>
- </tr>
- </thead>
- <tbody>
- {''.join(rows)}
- </tbody>
- </table>
- </div>
- '''
-
- def _get_visibility_display(self, field):
- """Get human readable visibility condition"""
- if not field.visibility_dependency:
- return 'Siempre visible'
-
- dep_name = field.visibility_dependency.field_description or field.visibility_dependency.name
-
- comparator_map = {
- 'equal': 'es igual a',
- '!equal': 'no es igual a',
- 'contains': 'contiene',
- '!contains': 'no contiene',
- 'set': 'está definido',
- '!set': 'no está definido',
- 'selected': 'es igual a',
- '!selected': 'no es igual a',
- }
-
- comp_text = comparator_map.get(field.visibility_comparator, field.visibility_comparator)
-
- # Get condition value display
- condition_value = field.visibility_condition
- if field.visibility_dependency.ttype == 'many2one' and field.visibility_condition_m2o_id:
- try:
- related_model = self.env[field.visibility_dependency.relation]
- related_record = related_model.browse(field.visibility_condition_m2o_id)
- if related_record.exists():
- condition_value = related_record.display_name
- except:
- pass
-
- if field.visibility_comparator in ['set', '!set']:
- return f"{dep_name} {comp_text}"
-
- return f"{dep_name} {comp_text} '{condition_value}'"
-
- def _get_priority_text(self, priority):
- """Get priority text"""
- priority_map = {
- '0': 'Baja',
- '1': 'Normal',
- '2': 'Alta',
- '3': 'Urgente',
- }
- return priority_map.get(priority, 'Todas')
-
- def _get_priority_badge(self, priority):
- """Get priority badge HTML"""
- priority_map = {
- '0': ('Baja', 'bg-secondary'),
- '1': ('Normal', 'bg-info'),
- '2': ('Alta', 'bg-warning text-dark'),
- '3': ('Urgente', 'bg-danger'),
- }
- text, cls = priority_map.get(priority, ('Todas', 'bg-light text-dark'))
- return f'<span class="badge {cls}">{text}</span>'
- @api.model
- def default_get(self, fields_list):
- """Set default required form fields when creating a new workflow template"""
- res = super().default_get(fields_list)
-
- # Only set defaults if field_ids is in fields_list and not already provided
- if 'field_ids' in fields_list and not self.env.context.get('default_field_ids'):
- # Required fields for the website form builder
- required_field_names = ['partner_name', 'partner_email', 'name', 'description']
-
- # Get field records from helpdesk.ticket model
- ticket_model = self.env['ir.model'].search([('model', '=', 'helpdesk.ticket')], limit=1)
- if ticket_model:
- required_fields = self.env['ir.model.fields'].search([
- ('model_id', '=', ticket_model.id),
- ('name', 'in', required_field_names),
- ('website_form_blacklisted', '=', False)
- ])
-
- field_map = {f.name: f for f in required_fields}
- field_ids_commands = []
- sequence = 10
-
- # Add partner_name (required, sequence 10)
- if 'partner_name' in field_map:
- field_ids_commands.append((0, 0, {
- 'field_id': field_map['partner_name'].id,
- 'required': True,
- 'sequence': sequence
- }))
- sequence += 10
-
- # Add partner_email (required, sequence 20)
- if 'partner_email' in field_map:
- field_ids_commands.append((0, 0, {
- 'field_id': field_map['partner_email'].id,
- 'required': True,
- 'sequence': sequence
- }))
- sequence += 10
-
- # Add name (model_required, sequence 30)
- if 'name' in field_map:
- name_field = field_map['name']
- field_ids_commands.append((0, 0, {
- 'field_id': name_field.id,
- 'required': True,
- 'model_required': name_field.required,
- 'sequence': sequence
- }))
- sequence += 10
-
- # Add description (required, sequence 40)
- if 'description' in field_map:
- field_ids_commands.append((0, 0, {
- 'field_id': field_map['description'].id,
- 'required': True,
- 'sequence': sequence
- }))
-
- if field_ids_commands:
- res['field_ids'] = field_ids_commands
-
- return res
- @api.depends('stage_template_ids', 'sla_template_ids', 'field_ids', 'team_ids')
- def _compute_counts(self):
- for template in self:
- template.stage_count = len(template.stage_template_ids)
- template.sla_count = len(template.sla_template_ids)
- template.field_count = len(template.field_ids)
- template.team_count = len(template.team_ids)
- def action_view_teams(self):
- """Open teams using this template"""
- self.ensure_one()
- action = self.env['ir.actions.actions']._for_xml_id('helpdesk.helpdesk_team_action')
- action.update({
- 'domain': [('workflow_template_id', '=', self.id)],
- 'context': {
- 'default_workflow_template_id': self.id,
- 'search_default_workflow_template_id': self.id,
- },
- })
- return action
- def copy_data(self, default=None):
- """Override copy to duplicate stages and SLAs"""
- defaults = super().copy_data(default=default)
- # Note: Stages and SLAs will be copied automatically via ondelete='cascade'
- # We just need to update the name
- for template, vals in zip(self, defaults):
- vals['name'] = self.env._("%s (copy)", template.name)
- return defaults
|