Prechádzať zdrojové kódy

feat(helpdesk_extras): consolidate templates into workflow template

- Remove legacy helpdesk.template and helpdesk.template.field models
- Remove template_id field from helpdesk.team
- Add documentation_html computed field with dynamic documentation tab
- Fix form regeneration to not preserve legacy fields
- Add Spanish translations for all new workflow template UI elements
- Update security rules and manifest
odoo 2 mesiacov pred
rodič
commit
0a60d475f2

+ 1 - 2
helpdesk_extras/__manifest__.py

@@ -1,6 +1,6 @@
 {
     "name": "Helpdesk Extras",
-    "version": "18.0.1.0.12",
+    "version": "18.0.1.1.0",
     "category": "Services/Helpdesk",
     "summary": "Funcionalidades extras para Helpdesk - Compartir equipos y widget de horas",
     "description": """
@@ -31,7 +31,6 @@ Funcionalidades adicionales para el módulo de Helpdesk:
         "wizard/helpdesk_team_share_wizard_views.xml",
         "wizard/helpdesk_workflow_template_apply_wizard_views.xml",
         "views/helpdesk_request_type_views.xml",
-        "views/helpdesk_template_views.xml",
         "views/helpdesk_workflow_template_views.xml",
         "views/helpdesk_team_views.xml",
         "views/helpdesk_ticket_views.xml",

+ 316 - 0
helpdesk_extras/i18n/es_MX.po

@@ -1136,3 +1136,319 @@ msgstr "Preventa"
 msgid "Sales Management"
 msgstr "Gestión de Ventas"
 
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Form Fields"
+msgstr "Campos del Formulario"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template__field_ids
+msgid "Form Fields"
+msgstr "Campos del Formulario"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template__field_count
+msgid "Form Fields Count"
+msgstr "Cantidad de Campos"
+
+#. module: helpdesk_extras
+#: model:ir.model,name:helpdesk_extras.model_helpdesk_workflow_template_field
+msgid "Workflow Template Form Field"
+msgstr "Campo de Formulario de Plantilla de Flujo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__workflow_template_id
+msgid "Workflow Template"
+msgstr "Plantilla de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__field_id
+msgid "Field"
+msgstr "Campo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__field_name
+msgid "Field Name"
+msgstr "Nombre del Campo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__field_type
+msgid "Field Type"
+msgstr "Tipo de Campo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__label_custom
+msgid "Custom Label"
+msgstr "Etiqueta Personalizada"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__placeholder
+msgid "Placeholder"
+msgstr "Texto de Ayuda"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__default_value
+msgid "Default Value"
+msgstr "Valor por Defecto"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__help_text
+msgid "Help Text"
+msgstr "Texto de Ayuda"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__widget
+msgid "Widget"
+msgstr "Widget"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__selection_type
+msgid "Selection Type"
+msgstr "Tipo de Selección"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__selection_options
+msgid "Selection Options"
+msgstr "Opciones de Selección"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__rows
+msgid "Height (Rows)"
+msgstr "Altura (Filas)"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__input_type
+msgid "Input Type"
+msgstr "Tipo de Entrada"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__sequence
+msgid "Sequence"
+msgstr "Secuencia"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__required
+msgid "Required"
+msgstr "Obligatorio"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__model_required
+msgid "Model Required"
+msgstr "Obligatorio del Modelo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__visibility_dependency
+msgid "Visibility Dependency"
+msgstr "Dependencia de Visibilidad"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__visibility_condition
+msgid "Visibility Condition Value"
+msgstr "Valor de Condición de Visibilidad"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__visibility_comparator
+msgid "Visibility Comparator"
+msgstr "Comparador de Visibilidad"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__visibility_between
+msgid "Visibility Between (End Value)"
+msgstr "Visibilidad Entre (Valor Final)"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,selection_type
+msgid "Dropdown List"
+msgstr "Lista Desplegable"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,selection_type
+msgid "Radio"
+msgstr "Botones de Radio"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,input_type
+msgid "Text"
+msgstr "Texto"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,input_type
+msgid "Email"
+msgstr "Correo Electrónico"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,input_type
+msgid "Telephone"
+msgstr "Teléfono"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,input_type
+msgid "Url"
+msgstr "URL"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Is equal to"
+msgstr "Es igual a"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Is not equal to"
+msgstr "No es igual a"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Contains"
+msgstr "Contiene"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Doesn't contain"
+msgstr "No contiene"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Is set"
+msgstr "Está definido"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Is not set"
+msgstr "No está definido"
+
+#. module: helpdesk_extras
+#. odoo-python
+#: code:addons/helpdesk_extras/models/helpdesk_workflow_template_field.py:0
+msgid "Cannot delete model required field(s): %s. This field is mandatory for the model and cannot be removed. Try hiding it with the 'Visibility' option instead and add it a default value."
+msgstr "No se puede eliminar el/los campo(s) obligatorio(s) del modelo: %s. Este campo es obligatorio para el modelo y no puede ser eliminado. Intenta ocultarlo con la opción 'Visibilidad' y agrega un valor por defecto."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Field Configuration"
+msgstr "Configuración del Campo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Display Options"
+msgstr "Opciones de Visualización"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Visibility Conditions"
+msgstr "Condiciones de Visibilidad"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Form Field"
+msgstr "Campo de Formulario"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Custom label (optional)"
+msgstr "Etiqueta personalizada (opcional)"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Placeholder text"
+msgstr "Texto de ejemplo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Default value"
+msgstr "Valor por defecto"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Help text (HTML)"
+msgstr "Texto de ayuda (HTML)"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Select field for visibility condition"
+msgstr "Seleccionar campo para condición de visibilidad"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Select comparator"
+msgstr "Seleccionar comparador"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Enter value to compare"
+msgstr "Ingresa el valor para comparar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Select value"
+msgstr "Seleccionar valor"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "End value for range (date/datetime)"
+msgstr "Valor final para rango (fecha/hora)"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "e.g., Basic Support, Premium Support"
+msgstr "ej., Soporte Básico, Soporte Premium"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Describe this workflow template..."
+msgstr "Describe esta plantilla de flujo de trabajo..."
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_stage__name
+msgid "Stage Name"
+msgstr "Nombre de Etapa"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_stage__fold
+msgid "Folded"
+msgstr "Plegada"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_stage__description
+msgid "Stage Description"
+msgstr "Descripción de Etapa"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__name
+msgid "SLA Name"
+msgstr "Nombre del SLA"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__stage_template_id
+msgid "Target Stage"
+msgstr "Etapa Objetivo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__time
+msgid "Time (Hours)"
+msgstr "Tiempo (Horas)"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__priority
+msgid "Priority"
+msgstr "Prioridad"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__exclude_stage_template_ids
+msgid "Excluded Stages"
+msgstr "Etapas Excluidas"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__tag_ids
+msgid "Tags"
+msgstr "Etiquetas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Documentation"
+msgstr "Documentación"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template__documentation_html
+msgid "Documentation"
+msgstr "Documentación"
+

+ 0 - 1
helpdesk_extras/models/__init__.py

@@ -4,7 +4,6 @@ from . import helpdesk_team_collaborator
 from . import helpdesk_team
 from . import helpdesk_request_type
 from . import helpdesk_ticket
-from . import helpdesk_template
 from . import helpdesk_workflow_template
 from . import helpdesk_workflow_template_stage
 from . import helpdesk_workflow_template_sla

+ 14 - 41
helpdesk_extras/models/helpdesk_team.py

@@ -21,44 +21,30 @@ class HelpdeskTeamExtras(models.Model):
         export_string_translation=False,
         help="Partners with access to this helpdesk team",
     )
-    template_id = fields.Many2one(
-        'helpdesk.template',
-        string='Template',
-        help="Template to use for tickets in this team. If set, template fields will be shown in ticket form."
-    )
     workflow_template_id = fields.Many2one(
         'helpdesk.workflow.template',
         string='Workflow Template',
-        help="Workflow template with stages and SLA policies. Use 'Apply Template' button to create stages and SLAs."
+        help="Workflow template with stages, SLA policies, and form fields."
     )
 
     @api.model_create_multi
     def create(self, vals_list):
-        """Override create to regenerate form XML if template/workflow fields are set"""
+        """Override create to regenerate form XML if workflow template has fields"""
         teams = super().create(vals_list)
-        # After create, if form fields are available (from template_id OR workflow_template_id), regenerate
         for team in teams.filtered(lambda t: t.use_website_helpdesk_form and t._has_form_fields() and t.website_form_view_id):
             team._regenerate_form_from_template()
         return teams
 
     def _has_form_fields(self):
-        """Check if team has form fields configured (from template_id or workflow_template_id)"""
+        """Check if team has form fields configured"""
         self.ensure_one()
-        # Check workflow_template_id.field_ids first (new), then template_id (legacy)
-        if self.workflow_template_id and self.workflow_template_id.field_ids:
-            return True
-        if self.template_id and self.template_id.field_ids:
-            return True
-        return False
+        return self.workflow_template_id and self.workflow_template_id.field_ids
 
     def _get_form_fields(self):
-        """Get form fields from workflow_template_id (preferred) or template_id (legacy)"""
+        """Get form fields from workflow_template_id"""
         self.ensure_one()
-        # Prefer workflow_template_id.field_ids (new), fallback to template_id (legacy)
         if self.workflow_template_id and self.workflow_template_id.field_ids:
             return self.workflow_template_id.field_ids.sorted('sequence')
-        if self.template_id and self.template_id.field_ids:
-            return self.template_id.field_ids.sorted('sequence')
         return self.env['helpdesk.workflow.template.field']
 
     def _ensure_submit_form_view(self):
@@ -72,10 +58,9 @@ class HelpdeskTeamExtras(models.Model):
         return result
 
     def write(self, vals):
-        """Override write to regenerate form XML when template or workflow changes"""
+        """Override write to regenerate form XML when workflow template changes"""
         result = super().write(vals)
-        # Regenerate form when template_id OR workflow_template_id changes
-        if 'template_id' in vals or 'workflow_template_id' in vals:
+        if 'workflow_template_id' in vals:
             teams_to_process = self.browse(self.ids).filtered('use_website_helpdesk_form')
             for team in teams_to_process:
                 if not team.website_form_view_id:
@@ -433,8 +418,8 @@ class HelpdeskTeamExtras(models.Model):
         template_fields = self._get_form_fields()
         
         # Determine source for logging
-        source = "workflow_template" if (self.workflow_template_id and self.workflow_template_id.field_ids) else "template"
-        source_id = self.workflow_template_id.id if source == "workflow_template" else (self.template_id.id if self.template_id else None)
+        source = "workflow_template" if (self.workflow_template_id and self.workflow_template_id.field_ids) else "none"
+        source_id = self.workflow_template_id.id if (self.workflow_template_id and self.workflow_template_id.field_ids) else None
         
         _logger.info(f"Regenerating form for team {self.id}, {source} {source_id} with {len(template_fields)} fields")
         for tf in template_fields:
@@ -458,12 +443,7 @@ class HelpdeskTeamExtras(models.Model):
             return
         rows_el = rows_el[0]
 
-        # Get template field names to know which ones are already in template
-        template_field_names = set(tf.field_id.name for tf in template_fields if tf.field_id)
-        
-        # Get existing description, team_id and submit button HTML (to preserve them)
-        # BUT: only preserve description if it's NOT in the template
-        description_html = None
+        # Extract only team_id and submit button from existing form (always needed)
         team_id_html = None
         submit_button_html = None
         
@@ -481,11 +461,7 @@ class HelpdeskTeamExtras(models.Model):
                 continue
             
             field_name = field_input[0].get('name')
-            if field_name == 'description':
-                # Only preserve description if it's NOT in the template
-                if 'description' not in template_field_names:
-                    description_html = etree.tostring(child, encoding='unicode', pretty_print=True)
-            elif field_name == 'team_id':
+            if field_name == 'team_id':
                 # Always preserve team_id (it's always needed, hidden)
                 team_id_html = etree.tostring(child, encoding='unicode', pretty_print=True)
 
@@ -502,16 +478,13 @@ class HelpdeskTeamExtras(models.Model):
                 _logger.error(f"Error building HTML for field {tf.field_id.name if tf.field_id else 'Unknown'}: {e}", exc_info=True)
 
         # Build complete rows container HTML
-        # Order: template fields -> description (if not in template) -> team_id -> submit button
+        # Order: template fields -> team_id -> submit button
+        # NOTE: Only template fields are included - no legacy field preservation
         rows_html_parts = []
         
-        # Add template fields first (this includes description if it's in the template)
+        # Add template fields
         rows_html_parts.extend(template_fields_html)
         
-        # Add description only if it exists AND is NOT in template
-        if description_html:
-            rows_html_parts.append(description_html)
-        
         # Add team_id (always needed, hidden)
         if team_id_html:
             rows_html_parts.append(team_id_html)

+ 0 - 729
helpdesk_extras/models/helpdesk_template.py

@@ -1,729 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-import logging
-
-from odoo import api, fields, models, _
-from odoo.exceptions import UserError
-
-_logger = logging.getLogger(__name__)
-
-
-class HelpdeskTemplate(models.Model):
-    _name = 'helpdesk.template'
-    _description = 'Helpdesk Template'
-    _order = 'name'
-
-    name = fields.Char(
-        string='Name',
-        required=True,
-        translate=True,
-        help="Name of the template"
-    )
-    description = fields.Text(
-        string='Description',
-        translate=True,
-        help="Description of the template"
-    )
-    active = fields.Boolean(
-        string='Active',
-        default=True,
-        help="If unchecked, this template will be hidden and won't be available"
-    )
-    field_ids = fields.One2many(
-        'helpdesk.template.field',
-        'template_id',
-        string='Fields',
-        copy=True,
-        help="Fields included in this template"
-    )
-
-    @api.model
-    def default_get(self, fields_list):
-        """Set default required fields when creating a new template"""
-        res = super().default_get(fields_list)
-        
-        # Only set defaults if creating a new record (not editing existing)
-        if 'field_ids' in fields_list and not self.env.context.get('default_field_ids'):
-            # Get the required fields from form builder (same as website_helpdesk_form_editor.js)
-            required_field_names = ['partner_name', 'partner_email', 'name', 'description']
-            
-            # Get field records
-            ticket_model = self.env['ir.model'].search([('model', '=', 'helpdesk.ticket')], limit=1)
-            if ticket_model:
-                # Find the field records
-                required_fields = self.env['ir.model.fields'].search([
-                    ('model_id', '=', ticket_model.id),
-                    ('name', 'in', required_field_names),
-                    ('website_form_blacklisted', '=', False)
-                ])
-                
-                # Create field mapping
-                field_map = {f.name: f for f in required_fields}
-                
-                # Prepare default field_ids
-                field_ids_commands = []
-                sequence = 10
-                
-                # Add partner_name (required: true, 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: true, 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 (modelRequired: true, sequence 30) - required by model
-                # Note: model_required will be set automatically in create() based on field.required
-                if 'name' in field_map:
-                    name_field = field_map['name']
-                    field_ids_commands.append((0, 0, {
-                        'field_id': name_field.id,
-                        'required': True,  # Mark as required since it's modelRequired
-                        'model_required': name_field.required,  # Auto-detect from field definition
-                        'sequence': sequence
-                    }))
-                    sequence += 10
-                
-                # Add description (required: true, sequence 40)
-                if 'description' in field_map:
-                    field_ids_commands.append((0, 0, {
-                        'field_id': field_map['description'].id,
-                        'required': True,
-                        'sequence': sequence
-                    }))
-                    sequence += 10
-                
-                if field_ids_commands:
-                    res['field_ids'] = field_ids_commands
-                    _logger.info(f"Setting default required fields for new template: {[cmd[2]['field_id'] for cmd in field_ids_commands]}")
-        
-        return res
-
-    @api.model_create_multi
-    def create(self, vals_list):
-        """Override create to automatically add required fields from form builder"""
-        # Get the required fields from form builder (same as website_helpdesk_form_editor.js)
-        # These are the fields that are always required in the form builder:
-        # - partner_name (required: true)
-        # - partner_email (required: true)
-        # - name (modelRequired: true) - required by the model
-        # - description (required: true)
-        required_field_names = ['partner_name', 'partner_email', 'name', 'description']
-        
-        # Get field records
-        ticket_model = self.env['ir.model'].search([('model', '=', 'helpdesk.ticket')], limit=1)
-        if not ticket_model:
-            return super().create(vals_list)
-        
-        # Find the field records
-        required_fields = self.env['ir.model.fields'].search([
-            ('model_id', '=', ticket_model.id),
-            ('name', 'in', required_field_names),
-            ('website_form_blacklisted', '=', False)
-        ])
-        
-        # Create field mapping
-        field_map = {f.name: f for f in required_fields}
-        
-        # Prepare default field_ids for each template
-        for vals in vals_list:
-            if 'field_ids' not in vals or not vals.get('field_ids'):
-                # Only add default fields if no fields are provided
-                field_ids_commands = []
-                sequence = 10
-                
-                # Add partner_name (required: true, 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: true, 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 (modelRequired: true, sequence 30) - required by model
-                # Note: model_required will be set automatically in create() based on field.required
-                if 'name' in field_map:
-                    name_field = field_map['name']
-                    field_ids_commands.append((0, 0, {
-                        'field_id': name_field.id,
-                        'required': True,  # Mark as required since it's modelRequired
-                        'model_required': name_field.required,  # Auto-detect from field definition
-                        'sequence': sequence
-                    }))
-                    sequence += 10
-                
-                # Add description (required: true, sequence 40)
-                if 'description' in field_map:
-                    field_ids_commands.append((0, 0, {
-                        'field_id': field_map['description'].id,
-                        'required': True,
-                        'sequence': sequence
-                    }))
-                    sequence += 10
-                
-                if field_ids_commands:
-                    vals['field_ids'] = field_ids_commands
-                    _logger.info(f"Adding default required fields to new template: {[cmd[2]['field_id'] for cmd in field_ids_commands]}")
-        
-        return super().create(vals_list)
-
-    def write(self, vals):
-        """Override write to regenerate forms in all teams using this template"""
-        result = super().write(vals)
-        
-        # If template fields or active status changed, regenerate forms in all teams using this template
-        # Note: field_ids changes are handled by helpdesk.template.field create/write/unlink methods
-        # but we also check here in case field_ids is directly modified
-        if 'field_ids' in vals or 'active' in vals:
-            # Find all teams using this template
-            teams = self.env['helpdesk.team'].search([
-                ('template_id', 'in', self.ids),
-                ('use_website_helpdesk_form', '=', True)
-            ])
-            
-            # Regenerate form XML for each team
-            for team in teams:
-                # Ensure view exists before regenerating
-                if not team.website_form_view_id:
-                    team._ensure_submit_form_view()
-                # Regenerate or restore form based on template status
-                if team.website_form_view_id:
-                    try:
-                        if team.template_id.active:
-                            team._regenerate_form_from_template()
-                            _logger.info(f"Regenerated form for team {team.id} after template {team.template_id.id} change")
-                        else:
-                            # If template is deactivated, restore default form
-                            team._restore_default_form()
-                            _logger.info(f"Restored default form for team {team.id} after template {team.template_id.id} deactivation")
-                    except Exception as e:
-                        _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
-        
-        return result
-
-
-class HelpdeskTemplateField(models.Model):
-    _name = 'helpdesk.template.field'
-    _description = 'Helpdesk Template Field'
-    _order = 'sequence, id'
-
-    template_id = fields.Many2one(
-        'helpdesk.template',
-        string='Template',
-        required=True,
-        ondelete='cascade',
-        index=True
-    )
-    field_id = fields.Many2one(
-        'ir.model.fields',
-        string='Field',
-        required=True,
-        domain="[('model', '=', 'helpdesk.ticket'), ('website_form_blacklisted', '=', False)]",
-        ondelete='cascade',
-        help="Field from helpdesk.ticket model"
-    )
-    field_name = fields.Char(
-        related='field_id.name',
-        string='Field Name',
-        store=True,
-        readonly=True
-    )
-    field_type = fields.Selection(
-        related='field_id.ttype',
-        string='Field Type',
-        readonly=True
-    )
-    label_custom = fields.Char(
-        string='Custom Label',
-        help="Custom label for the field in the form. If empty, uses the field's default label."
-    )
-    placeholder = fields.Text(
-        string='Placeholder',
-        help="Placeholder text shown when field is empty"
-    )
-    default_value = fields.Char(
-        string='Default Value',
-        help="Default value for the field"
-    )
-    help_text = fields.Html(
-        string='Help Text',
-        help="Help text/description shown below the field (supports HTML formatting)"
-    )
-    widget = fields.Selection(
-        [
-            ('default', 'Default'),
-            ('radio', 'Radio Buttons'),
-            ('checkbox', 'Checkboxes'),
-        ],
-        string='Widget',
-        default='default',
-        help="Widget to use for selection/many2one fields. Default uses dropdown select."
-    )
-    selection_type = fields.Selection(
-        [
-            ('dropdown', 'Dropdown List'),
-            ('radio', 'Radio'),
-        ],
-        string='Selection Type',
-        default='dropdown',
-        help="Display type for selection and many2one fields. Dropdown List shows a select dropdown, Radio shows radio buttons. Same as Odoo formbuilder."
-    )
-    selection_options = fields.Text(
-        string='Selection Options',
-        help="For selection fields (not relations): JSON array of [value, label] pairs. Example: [['option1', 'Option 1'], ['option2', 'Option 2']]"
-    )
-    rows = fields.Integer(
-        string='Height (Rows)',
-        default=3,
-        help="Number of rows for textarea fields. Default is 3."
-    )
-    input_type = fields.Selection(
-        [
-            ('text', 'Text'),
-            ('email', 'Email'),
-            ('tel', 'Telephone'),
-            ('url', 'Url'),
-        ],
-        string='Input Type',
-        default='text',
-        help="Input type for text fields. Determines the HTML input type attribute."
-    )
-    sequence = fields.Integer(
-        string='Sequence',
-        default=10,
-        help="Order in which fields are displayed"
-    )
-    required = fields.Boolean(
-        string='Required',
-        default=False,
-        help="Make this field required in addition to its base configuration"
-    )
-    model_required = fields.Boolean(
-        string='Model Required',
-        default=False,
-        readonly=True,
-        help="This field is mandatory for the model and cannot be removed"
-    )
-    # Visibility conditions
-    visibility_dependency = fields.Many2one(
-        'ir.model.fields',
-        string='Visibility Dependency',
-        domain="[('model', '=', 'helpdesk.ticket'), ('website_form_blacklisted', '=', False)]",
-        help="Field on which visibility depends"
-    )
-    visibility_condition = fields.Char(
-        string='Visibility Condition Value',
-        help="Value to compare against the dependency field (for text, number, date, etc.)"
-    )
-    visibility_comparator = fields.Selection(
-        [
-            # Basic comparators
-            ('equal', 'Is equal to'),
-            ('!equal', 'Is not equal to'),
-            ('contains', 'Contains'),
-            ('!contains', "Doesn't contain"),
-            ('set', 'Is set'),
-            ('!set', 'Is not set'),
-            # Numeric comparators
-            ('greater', 'Is greater than'),
-            ('less', 'Is less than'),
-            ('greater or equal', 'Is greater than or equal to'),
-            ('less or equal', 'Is less than or equal to'),
-            # Date/Datetime comparators
-            ('dateEqual', 'Is equal to (date)'),
-            ('date!equal', 'Is not equal to (date)'),
-            ('after', 'Is after'),
-            ('before', 'Is before'),
-            ('equal or after', 'Is after or equal to'),
-            ('equal or before', 'Is before or equal to'),
-            ('between', 'Is between (included)'),
-            ('!between', 'Is not between (excluded)'),
-            # Selection/Many2one comparators
-            ('selected', 'Is equal to (selected)'),
-            ('!selected', 'Is not equal to (not selected)'),
-            # File comparators
-            ('fileSet', 'Is set (file)'),
-            ('!fileSet', 'Is not set (file)'),
-        ],
-        string='Visibility Comparator',
-        default='equal',
-        help="Comparison operator for visibility condition"
-    )
-    
-    # Computed field to determine dependency field type
-    visibility_dependency_type = fields.Char(
-        string='Dependency Field Type',
-        compute='_compute_visibility_dependency_type',
-        store=False,
-        help="Type of the visibility dependency field"
-    )
-    
-    # Field for many2one dependency - store ID as Integer (not Many2one to avoid model validation)
-    # The widget will handle the dynamic model change and display
-    visibility_condition_m2o_id = fields.Integer(
-        string='Visibility Condition (Many2one ID)',
-        help="ID of the selected record when dependency is a many2one field (model stored separately)"
-    )
-    visibility_condition_m2o_model = fields.Char(
-        string='M2O Model',
-        related='visibility_dependency.relation',
-        store=False,
-        readonly=True,
-        help="Model name for the many2one condition"
-    )
-    
-    # Field for selection dependency - computed selection options
-    visibility_condition_selection = fields.Selection(
-        selection='_get_visibility_condition_selection_options',
-        string='Visibility Condition (Selection)',
-        help="Selected value when dependency is a selection field"
-    )
-    
-    # Field for range conditions (between/!between) - second value for date/datetime ranges
-    visibility_between = fields.Char(
-        string='Visibility Between (End Value)',
-        help="Second value for 'between' and '!between' comparators (for date/datetime ranges)"
-    )
-    
-    def _get_visibility_condition_selection_options(self):
-        """Return selection options based on visibility_dependency field"""
-        # Handle empty recordset (when called from fields_get)
-        if not self:
-            return []
-        
-        # Handle multiple records (shouldn't happen, but be safe)
-        if len(self) > 1:
-            return []
-        
-        record = self[0] if self else None
-        if not record or not record.visibility_dependency or record.visibility_dependency.ttype != 'selection':
-            return []
-        
-        # Get selection options from ir.model.fields.selection
-        selection_records = self.env['ir.model.fields.selection'].search([
-            ('field_id', '=', record.visibility_dependency.id)
-        ], order='sequence, id')
-        
-        if selection_records:
-            return [(sel.value, sel.name) for sel in selection_records]
-        
-        # Fallback: try to get from field definition (for old-style selection)
-        try:
-            model = self.env[record.visibility_dependency.model]
-            field = model._fields.get(record.visibility_dependency.name)
-            if field and hasattr(field, 'selection') and field.selection:
-                if callable(field.selection):
-                    return field.selection(model)
-                return field.selection
-        except:
-            pass
-        
-        return []
-    
-    @api.depends('visibility_dependency')
-    def _compute_visibility_dependency_type(self):
-        """Compute the type of the visibility dependency field"""
-        for record in self:
-            if record.visibility_dependency:
-                record.visibility_dependency_type = record.visibility_dependency.ttype
-            else:
-                record.visibility_dependency_type = False
-    
-    @api.onchange('visibility_condition_m2o_id', 'visibility_dependency')
-    def _onchange_visibility_condition_m2o_id(self):
-        """Sync many2one ID to visibility_condition"""
-        if self.visibility_dependency and self.visibility_dependency.ttype == 'many2one':
-            if self.visibility_condition_m2o_id:
-                self.visibility_condition = str(self.visibility_condition_m2o_id)
-            else:
-                self.visibility_condition = False
-    
-    @api.onchange('visibility_condition_selection')
-    def _onchange_visibility_condition_selection(self):
-        """Sync selection value to visibility_condition"""
-        if self.visibility_condition_selection:
-            self.visibility_condition = self.visibility_condition_selection
-    
-    @api.onchange('visibility_dependency')
-    def _onchange_visibility_dependency(self):
-        """Clear condition values when dependency changes"""
-        if not self.visibility_dependency:
-            self.visibility_condition = False
-            self.visibility_condition_m2o_id = False
-            self.visibility_condition_selection = False
-        elif self.visibility_dependency.ttype not in ['many2one', 'selection']:
-            self.visibility_condition_m2o_id = False
-            self.visibility_condition_selection = False
-        elif self.visibility_dependency.ttype == 'many2one':
-            # Load current value into m2o_id if exists
-            if self.visibility_condition and self.visibility_condition.isdigit():
-                try:
-                    model_name = self.visibility_dependency.relation
-                    if model_name:
-                        model = self.env[model_name]
-                        record = model.browse(int(self.visibility_condition))
-                        if record.exists():
-                            # Store the ID - the widget will handle the model change
-                            self.visibility_condition_m2o_id = int(self.visibility_condition)
-                        else:
-                            self.visibility_condition_m2o_id = False
-                    else:
-                        self.visibility_condition_m2o_id = False
-                except:
-                    self.visibility_condition_m2o_id = False
-        elif self.visibility_dependency.ttype == 'selection':
-            # Load current value into selection if exists
-            if self.visibility_condition:
-                self.visibility_condition_selection = self.visibility_condition
-    
-    @api.onchange('visibility_comparator')
-    def _onchange_visibility_comparator(self):
-        """Clear visibility_between when comparator changes away from between/!between"""
-        if self.visibility_comparator not in ['between', '!between']:
-            self.visibility_between = False
-
-    _sql_constraints = [
-        ('unique_template_field', 'unique(template_id, field_id)',
-         'A field can only be added once to a template')
-    ]
-
-    @api.model
-    def _register_hook(self):
-        """Register label_custom field in ir.model.fields if it doesn't exist"""
-        super()._register_hook()
-        try:
-            model = self.env['ir.model'].search([('model', '=', 'helpdesk.template.field')], limit=1)
-            if model:
-                field_model = self.env['ir.model.fields']
-                existing_field = field_model.search([
-                    ('model_id', '=', model.id),
-                    ('name', '=', 'label_custom')
-                ], limit=1)
-                
-                if not existing_field:
-                    field_model.create({
-                        'model_id': model.id,
-                        'name': 'label_custom',
-                        'field_description': 'Custom Label',
-                        'ttype': 'char',
-                        'state': 'manual',
-                        'required': False,
-                        'readonly': False,
-                        'store': True,
-                    })
-                    _logger.info("Campo label_custom registrado en _register_hook")
-        except Exception as e:
-            _logger.error(f"Error registrando label_custom en _register_hook: {str(e)}", exc_info=True)
-
-    @api.model
-    def _migrate_label_custom_field(self):
-        """
-        Migration method to ensure label_custom field exists in database.
-        This method should be called after module update to fix any missing field issues.
-        """
-        try:
-            # Check if column exists in database
-            self.env.cr.execute("""
-                SELECT column_name 
-                FROM information_schema.columns 
-                WHERE table_name = 'helpdesk_template_field' 
-                AND column_name = 'label_custom'
-            """)
-            column_exists = self.env.cr.fetchone()
-            
-            if not column_exists:
-                _logger.warning("Column 'label_custom' does not exist. Adding it...")
-                # Add column manually if it doesn't exist
-                self.env.cr.execute("""
-                    ALTER TABLE helpdesk_template_field 
-                    ADD COLUMN label_custom VARCHAR
-                """)
-                self.env.cr.commit()
-                _logger.info("Column 'label_custom' added successfully")
-            else:
-                _logger.info("Column 'label_custom' already exists")
-            
-            # Update ir.model.fields to ensure field is registered
-            field_model = self.env['ir.model.fields']
-            model_id = self.env['ir.model'].search([('model', '=', 'helpdesk.template.field')], limit=1)
-            
-            if model_id:
-                existing_field = field_model.search([
-                    ('model_id', '=', model_id.id),
-                    ('name', '=', 'label_custom')
-                ], limit=1)
-                
-                if not existing_field:
-                    _logger.warning("Field 'label_custom' not found in ir.model.fields. Creating it...")
-                    field_model.create({
-                        'model_id': model_id.id,
-                        'name': 'label_custom',
-                        'field_description': 'Custom Label',
-                        'ttype': 'char',
-                        'state': 'manual',
-                    })
-                    _logger.info("Field 'label_custom' registered in ir.model.fields")
-                else:
-                    _logger.info("Field 'label_custom' already registered in ir.model.fields")
-            
-            # Clear cache to ensure changes are reflected
-            self.env.registry.clear_cache()
-            
-        except Exception as e:
-            _logger.error(f"Error in _migrate_label_custom_field: {str(e)}", exc_info=True)
-            # Don't raise to avoid breaking module update
-
-    @api.model_create_multi
-    def create(self, vals_list):
-        """Override create to mark model required fields and regenerate forms when template field is added"""
-        # Mark model required fields automatically based on field definition
-        for vals in vals_list:
-            if 'field_id' in vals and vals['field_id']:
-                field = self.env['ir.model.fields'].browse(vals['field_id'])
-                # Check if field is required at model level (not just in form)
-                # A field is model required if:
-                # 1. It's in the helpdesk.ticket model
-                # 2. It has required=True in ir.model.fields (mandatory at model level)
-                # 3. It's not blacklisted for website forms
-                if (field.model == 'helpdesk.ticket' and 
-                    field.required and 
-                    not field.website_form_blacklisted):
-                    vals['model_required'] = True
-                    _logger.info(f"Auto-marked field {field.name} as model_required (required at model level)")
-        
-        fields_created = super().create(vals_list)
-        
-        # Get unique templates that were modified
-        templates = fields_created.mapped('template_id')
-        
-        # Regenerate forms in all teams using these templates
-        for template in templates:
-            if not template:
-                continue
-            teams = self.env['helpdesk.team'].search([
-                ('template_id', '=', template.id),
-                ('use_website_helpdesk_form', '=', True)
-            ])
-            for team in teams:
-                # Ensure view exists before regenerating
-                if not team.website_form_view_id:
-                    team._ensure_submit_form_view()
-                # Regenerate form if view exists
-                if team.website_form_view_id:
-                    try:
-                        team._regenerate_form_from_template()
-                        _logger.info(f"Regenerated form for team {team.id} after adding field to template {template.id}")
-                    except Exception as e:
-                        _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
-        
-        return fields_created
-
-    def write(self, vals):
-        """Override write to mark model required fields and regenerate forms when template field is modified"""
-        # Mark/unmark model_required automatically based on field definition
-        if 'field_id' in vals and vals['field_id']:
-            field = self.env['ir.model.fields'].browse(vals['field_id'])
-            # Check if field is required at model level
-            if (field.model == 'helpdesk.ticket' and 
-                field.required and 
-                not field.website_form_blacklisted):
-                vals['model_required'] = True
-                _logger.info(f"Auto-marked field {field.name} as model_required (required at model level)")
-            else:
-                # Field is not model required, unmark it
-                vals['model_required'] = False
-        elif 'field_id' in vals and not vals['field_id']:
-            # Field_id is being cleared, unmark model_required
-            vals['model_required'] = False
-        
-        result = super().write(vals)
-        
-        # If any field configuration changed, regenerate forms
-        if any(key in vals for key in ['field_id', 'sequence', 'required', 'visibility_dependency', 
-                                       'visibility_condition', 'visibility_comparator', 'label_custom', 
-                                       'model_required', 'placeholder', 'default_value', 'help_text', 
-                                       'widget', 'selection_options', 'rows', 'input_type', 'selection_type']):
-            # Get unique templates that were modified
-            templates = self.mapped('template_id')
-            
-            # Regenerate forms in all teams using these templates
-            for template in templates:
-                if not template:
-                    continue
-                teams = self.env['helpdesk.team'].search([
-                    ('template_id', '=', template.id),
-                    ('use_website_helpdesk_form', '=', True)
-                ])
-                for team in teams:
-                    # Ensure view exists before regenerating
-                    if not team.website_form_view_id:
-                        team._ensure_submit_form_view()
-                    # Regenerate form if view exists
-                    if team.website_form_view_id:
-                        try:
-                            team._regenerate_form_from_template()
-                            _logger.info(f"Regenerated form for team {team.id} after modifying field in template {template.id}")
-                        except Exception as e:
-                            _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
-        
-        return result
-
-    def unlink(self):
-        """Override unlink to prevent deletion of model required fields and regenerate forms"""
-        # Prevent deletion of model required fields
-        model_required_fields = self.filtered('model_required')
-        if model_required_fields:
-            field_names = [f.field_id.name if f.field_id else 'Unknown' for f in model_required_fields]
-            raise UserError(
-                _("Cannot delete model required field(s): %s. This field is mandatory for the model and cannot be removed. "
-                  "Try hiding it with the 'Visibility' option instead and add it a default value.")
-                % ', '.join(field_names)
-            )
-        
-        # Get templates before deletion
-        templates = self.mapped('template_id')
-        
-        result = super().unlink()
-        
-        # Regenerate forms in all teams using these templates
-        for template in templates:
-            if not template:
-                continue
-            teams = self.env['helpdesk.team'].search([
-                ('template_id', '=', template.id),
-                ('use_website_helpdesk_form', '=', True)
-            ])
-            for team in teams:
-                # Ensure view exists before regenerating
-                if not team.website_form_view_id:
-                    team._ensure_submit_form_view()
-                # Regenerate form if view exists
-                if team.website_form_view_id:
-                    try:
-                        team._regenerate_form_from_template()
-                        _logger.info(f"Regenerated form for team {team.id} after removing field from template {template.id}")
-                    except Exception as e:
-                        _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
-        
-        return result

+ 0 - 2
helpdesk_extras/models/helpdesk_template_field.py

@@ -1,2 +0,0 @@
-# Este archivo está vacío porque el modelo está en helpdesk_template.py
-# Se mantiene para referencia si se necesita separar en el futuro

+ 11 - 7
helpdesk_extras/models/helpdesk_ticket.py

@@ -87,11 +87,15 @@ class HelpdeskTicket(models.Model):
         help=_("Files attached to this ticket")
     )
 
-    @api.depends('team_id.template_id')
+    @api.depends('team_id.workflow_template_id', 'team_id.workflow_template_id.field_ids')
     def _compute_has_template(self):
-        """Compute if team has a template"""
+        """Compute if team has form fields configured in workflow template"""
         for ticket in self:
-            ticket.has_template = bool(ticket.team_id and ticket.team_id.template_id)
+            ticket.has_template = bool(
+                ticket.team_id and 
+                ticket.team_id.workflow_template_id and 
+                ticket.team_id.workflow_template_id.field_ids
+            )
 
     @api.model
     def _default_request_type_id(self):
@@ -103,11 +107,11 @@ class HelpdeskTicket(models.Model):
         return incident_type.id if incident_type else False
 
     def _get_template_fields(self):
-        """Get template fields for this ticket's team"""
+        """Get form fields for this ticket's team"""
         self.ensure_one()
-        if not self.team_id or not self.team_id.template_id:
-            return self.env['helpdesk.template.field']
-        return self.team_id.template_id.field_ids.sorted('sequence')
+        if not self.team_id or not self.team_id.workflow_template_id:
+            return self.env['helpdesk.workflow.template.field']
+        return self.team_id.workflow_template_id.field_ids.sorted('sequence')
 
     @api.onchange('request_type_id', 'business_impact')
     def _onchange_compute_priority(self):

+ 379 - 0
helpdesk_extras/models/helpdesk_workflow_template.py

@@ -74,6 +74,385 @@ class HelpdeskWorkflowTemplate(models.Model):
         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):

+ 0 - 4
helpdesk_extras/security/ir.model.access.csv

@@ -11,10 +11,6 @@ access_helpdesk_team_share_collaborator_wizard_portal,helpdesk.team.share.collab
 access_helpdesk_request_type_internal,helpdesk.request.type.internal,helpdesk_extras.model_helpdesk_request_type,base.group_user,1,0,0,0
 access_helpdesk_request_type_manager,helpdesk.request.type.manager,helpdesk_extras.model_helpdesk_request_type,helpdesk.group_helpdesk_manager,1,1,1,1
 access_helpdesk_request_type_portal,helpdesk.request.type.portal,helpdesk_extras.model_helpdesk_request_type,base.group_portal,1,0,0,0
-access_helpdesk_template_internal,helpdesk.template.internal,helpdesk_extras.model_helpdesk_template,base.group_user,1,0,0,0
-access_helpdesk_template_manager,helpdesk.template.manager,helpdesk_extras.model_helpdesk_template,helpdesk.group_helpdesk_manager,1,1,1,1
-access_helpdesk_template_field_internal,helpdesk.template.field.internal,helpdesk_extras.model_helpdesk_template_field,base.group_user,1,0,0,0
-access_helpdesk_template_field_manager,helpdesk.template.field.manager,helpdesk_extras.model_helpdesk_template_field,helpdesk.group_helpdesk_manager,1,1,1,1
 access_helpdesk_workflow_template_internal,helpdesk.workflow.template.internal,helpdesk_extras.model_helpdesk_workflow_template,base.group_user,1,0,0,0
 access_helpdesk_workflow_template_manager,helpdesk.workflow.template.manager,helpdesk_extras.model_helpdesk_workflow_template,helpdesk.group_helpdesk_manager,1,1,1,1
 access_helpdesk_workflow_template_stage_internal,helpdesk.workflow.template.stage.internal,helpdesk_extras.model_helpdesk_workflow_template_stage,base.group_user,1,0,0,0

+ 0 - 6
helpdesk_extras/views/helpdesk_team_views.xml

@@ -26,12 +26,6 @@
                                 context="{'active_id': id, 'default_team_id': id}"/>
                     </setting>
                 </div>
-                <h2>Template</h2>
-                <div class="row mt16 o_settings_container">
-                    <setting string="Ticket Template" help="Template to use for tickets in this team">
-                        <field name="template_id" options="{'no_create': True}"/>
-                    </setting>
-                </div>
                 <h2>Collaborators</h2>
                 <div class="row mt16 o_settings_container">
                     <setting string="Team Collaborators" help="Partners with access to this helpdesk team">

+ 0 - 117
helpdesk_extras/views/helpdesk_template_views.xml

@@ -1,117 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-
-    <record id="helpdesk_template_view_list" model="ir.ui.view">
-        <field name="name">helpdesk.template.list</field>
-        <field name="model">helpdesk.template</field>
-        <field name="arch" type="xml">
-            <list string="Templates">
-                <field name="name"/>
-                <field name="active" widget="boolean_toggle"/>
-                <field name="field_ids" widget="many2many_tags"/>
-            </list>
-        </field>
-    </record>
-
-    <record id="helpdesk_template_view_form" model="ir.ui.view">
-        <field name="name">helpdesk.template.form</field>
-        <field name="model">helpdesk.template</field>
-        <field name="arch" type="xml">
-            <form string="Template">
-                <sheet>
-                    <group>
-                        <group>
-                            <field name="name" placeholder="e.g., Incident Template, Improvement Template"/>
-                            <field name="active"/>
-                        </group>
-                    </group>
-                    <notebook>
-                        <page string="Fields" name="fields">
-                            <field name="field_ids" nolabel="1" widget="helpdesk_template_field_ids">
-                                <list string="Template Fields" editable="bottom">
-                                    <field name="sequence" widget="handle"/>
-                                    <field name="field_id" 
-                                           domain="[('model', '=', 'helpdesk.ticket'), ('website_form_blacklisted', '=', False)]"
-                                           options="{'no_create': True, 'no_open': True}" 
-                                           required="1"/>
-                                    <field name="field_name" readonly="1"/>
-                                    <field name="field_type" readonly="1"/>
-                                    <field name="required"/>
-                                    <field name="model_required" invisible="1"/>
-                                    <field name="label_custom" placeholder="Custom label (optional)"/>
-                                    <field name="placeholder" placeholder="Placeholder text"/>
-                                    <field name="default_value" placeholder="Default value"/>
-                                    <field name="help_text" widget="html" placeholder="Help text (HTML)"/>
-                                    <field name="rows" 
-                                           string="Height (Rows)"
-                                           invisible="field_type not in ['text', 'html']"/>
-                                    <field name="input_type" 
-                                           string="Input Type"
-                                           invisible="field_type != 'char'"/>
-                                    <field name="selection_type" 
-                                           string="Selection Type"
-                                           invisible="field_type not in ['selection', 'many2one']"/>
-                                    <field name="widget" 
-                                           column_invisible="1"
-                                           invisible="field_type not in ['one2many', 'many2many']"/>
-                                    <field name="selection_options" 
-                                           widget="text" 
-                                           column_invisible="1"
-                                           placeholder='[["value1", "Label 1"], ["value2", "Label 2"]]'
-                                           invisible="field_type != 'selection' or field_id.relation"/>
-                                    <field name="visibility_dependency" 
-                                           domain="[('model', '=', 'helpdesk.ticket'), ('website_form_blacklisted', '=', False)]"
-                                           options="{'no_create': True, 'no_open': True}" 
-                                           placeholder="Select field for visibility condition"/>
-                                    <field name="visibility_comparator" 
-                                           placeholder="Select comparator"/>
-                                    <field name="visibility_condition" 
-                                           placeholder="Enter value to compare"
-                                           invisible="visibility_dependency_type in ['many2one', 'selection']"/>
-                                    <field name="visibility_condition_m2o_id" 
-                                           widget="dynamic_many2one"
-                                           placeholder="Select value"
-                                           invisible="visibility_dependency_type != 'many2one'"
-                                           options="{'no_create': True, 'no_open': True}"/>
-                                    <field name="visibility_condition_selection" 
-                                           placeholder="Select value"
-                                           invisible="visibility_dependency_type != 'selection'"/>
-                                    <field name="visibility_between" 
-                                           placeholder="End value for range (date/datetime)"
-                                           invisible="visibility_comparator not in ['between', '!between']"/>
-                                    <field name="visibility_dependency_type" invisible="1"/>
-                                    <field name="visibility_condition_m2o_model" invisible="1"/>
-                                </list>
-                            </field>
-                        </page>
-                        <page string="Description" name="description">
-                            <field name="description" placeholder="Describe the purpose of this template..."/>
-                        </page>
-                    </notebook>
-                </sheet>
-            </form>
-        </field>
-    </record>
-
-    <record id="helpdesk_template_action" model="ir.actions.act_window">
-        <field name="name">Templates</field>
-        <field name="res_model">helpdesk.template</field>
-        <field name="view_mode">list,form</field>
-        <field name="help" type="html">
-            <p class="o_view_nocontent_smiling_face">
-                Create your first template!
-            </p>
-            <p>
-                Templates allow you to configure which fields are shown in tickets and their visibility conditions.
-            </p>
-        </field>
-    </record>
-
-    <menuitem id="helpdesk_template_menu"
-              name="Templates"
-              parent="helpdesk.helpdesk_menu_config"
-              action="helpdesk_template_action"
-              sequence="25"
-              groups="helpdesk.group_helpdesk_manager"/>
-
-</odoo>

+ 3 - 0
helpdesk_extras/views/helpdesk_workflow_template_views.xml

@@ -216,6 +216,9 @@
                                 </form>
                             </field>
                         </page>
+                        <page string="Documentation" name="documentation">
+                            <field name="documentation_html" readonly="1" nolabel="1"/>
+                        </page>
                     </notebook>
                 </sheet>
             </form>