Selaa lähdekoodia

Integrar Workflow Template con Template: unificar campos y workflow

- Agregar campo field_ids a helpdesk.workflow.template
- Modificar helpdesk.template.field para soportar workflow_template_id
- Actualizar helpdesk.team y helpdesk.ticket para usar workflow_template_id
- Agregar pestaña Fields en vista de Workflow Template
- Crear migraciones pre y post para migración de datos
- Migración inteligente: campos de template legacy se migran al workflow template existente del equipo
- Versión: 18.0.1.0.12
odoo 2 kuukautta sitten
vanhempi
commit
9fd520c8b6

+ 1 - 1
helpdesk_extras/__manifest__.py

@@ -1,6 +1,6 @@
 {
     "name": "Helpdesk Extras",
-    "version": "18.0.1.0.11",
+    "version": "18.0.1.0.12",
     "category": "Services/Helpdesk",
     "summary": "Funcionalidades extras para Helpdesk - Compartir equipos y widget de horas",
     "description": """

+ 249 - 0
helpdesk_extras/migrations/18.0.1.0.12/post-migration.py

@@ -0,0 +1,249 @@
+# -*- coding: utf-8 -*-
+"""
+Migration script to integrate helpdesk.template with helpdesk.workflow.template
+
+This script migrates all helpdesk.template records to helpdesk.workflow.template,
+moving field_ids from template to workflow_template.
+"""
+import logging
+from odoo import api, SUPERUSER_ID
+
+_logger = logging.getLogger(__name__)
+
+
+def migrate(cr, version):
+    """Migrate helpdesk.template to helpdesk.workflow.template"""
+    try:
+        env = api.Environment(cr, SUPERUSER_ID, {})
+        
+        _logger.info("=" * 80)
+        _logger.info("Starting migration: helpdesk.template -> helpdesk.workflow.template")
+        _logger.info("=" * 80)
+        
+        # 0. Verify pre-migration completed (workflow_template_id column exists)
+        cr.execute("""
+            SELECT column_name 
+            FROM information_schema.columns 
+            WHERE table_name = 'helpdesk_template_field' 
+            AND column_name = 'workflow_template_id'
+        """)
+        if not cr.fetchone():
+            _logger.error("❌ Pre-migration not completed! workflow_template_id column does not exist.")
+            _logger.error("Module update will fail. Please check pre-migration logs.")
+            raise Exception("Pre-migration required: workflow_template_id column missing")
+        
+        _logger.info("✅ Pre-migration verified: workflow_template_id column exists")
+        
+        # 1. Get all helpdesk.template records
+        templates = env['helpdesk.template'].search([])
+        _logger.info(f"Found {len(templates)} template(s) to migrate")
+        
+        if not templates:
+            _logger.info("No templates to migrate. Migration complete.")
+            cr.commit()
+            return
+        
+        migrated_count = 0
+        skipped_count = 0
+        error_count = 0
+        
+        # 2. For each template, migrate to workflow template
+        for template in templates:
+            try:
+                _logger.info(f"\nProcessing template: {template.name} (ID: {template.id})")
+                
+                # First, check which teams use this template
+                teams_using_template = env['helpdesk.team'].search([
+                    ('template_id', '=', template.id)
+                ])
+                
+                _logger.info(f"  Found {len(teams_using_template)} team(s) using this template")
+                
+                # Group teams by whether they have workflow_template_id or not
+                teams_with_workflow = teams_using_template.filtered('workflow_template_id')
+                teams_without_workflow = teams_using_template.filtered(lambda t: not t.workflow_template_id)
+                
+                workflow_template = None
+                
+                # Strategy 1: If teams have workflow_template_id, migrate fields to their existing workflow
+                if teams_with_workflow:
+                    # Check if all teams use the same workflow_template_id
+                    workflow_ids = teams_with_workflow.mapped('workflow_template_id.id')
+                    unique_workflows = set(workflow_ids)
+                    
+                    if len(unique_workflows) == 1:
+                        # All teams use the same workflow template - migrate fields there
+                        workflow_template = teams_with_workflow[0].workflow_template_id
+                        _logger.info(f"  All teams use workflow template '{workflow_template.name}' (ID: {workflow_template.id})")
+                        _logger.info(f"  Migrating fields from template to existing workflow template...")
+                    else:
+                        # Teams use different workflow templates - use the most common one
+                        from collections import Counter
+                        workflow_counter = Counter(workflow_ids)
+                        most_common_workflow_id = workflow_counter.most_common(1)[0][0]
+                        workflow_template = env['helpdesk.workflow.template'].browse(most_common_workflow_id)
+                        _logger.info(f"  Teams use different workflow templates. Using most common: '{workflow_template.name}' (ID: {workflow_template.id})")
+                        _logger.info(f"  Migrating fields from template to this workflow template...")
+                
+                # Strategy 2: If no teams have workflow_template_id, create/find workflow template
+                if not workflow_template:
+                    # Check if a workflow template with the same name already exists
+                    workflow_template = env['helpdesk.workflow.template'].search([
+                        ('name', '=', template.name)
+                    ], limit=1)
+                    
+                    if workflow_template:
+                        _logger.info(f"  Workflow template '{workflow_template.name}' already exists (ID: {workflow_template.id})")
+                        _logger.info(f"  Migrating fields from template into existing workflow template...")
+                    else:
+                        # Create new workflow template
+                        workflow_template = env['helpdesk.workflow.template'].create({
+                            'name': template.name,
+                            'description': template.description or False,
+                            'active': template.active,
+                        })
+                        _logger.info(f"  Created new workflow template: {workflow_template.name} (ID: {workflow_template.id})")
+                
+                # 3. Migrate field_ids from template to workflow_template
+                field_count = 0
+                for field in template.field_ids:
+                    try:
+                        # Check if field already exists in workflow template
+                        existing_field = env['helpdesk.template.field'].search([
+                            ('workflow_template_id', '=', workflow_template.id),
+                            ('field_id', '=', field.field_id.id)
+                        ], limit=1)
+                        
+                        if existing_field:
+                            _logger.warning(f"    Field '{field.field_id.name}' already exists in workflow template. Skipping.")
+                            continue
+                        
+                        # Update field to point to workflow_template_id instead of template_id
+                        field.write({
+                            'template_id': False,  # Clear legacy reference
+                            'workflow_template_id': workflow_template.id,  # Set new reference
+                        })
+                        field_count += 1
+                        _logger.info(f"    Migrated field: {field.field_id.name}")
+                    except Exception as e:
+                        _logger.error(f"    Error migrating field {field.id}: {e}", exc_info=True)
+                        error_count += 1
+                        continue
+                
+                _logger.info(f"  Migrated {field_count} field(s) to workflow template")
+                
+                # 4. Update teams that use this template
+                team_count = 0
+                for team in teams_using_template:
+                    try:
+                        # If team doesn't have workflow_template_id, assign it
+                        if not team.workflow_template_id:
+                            team.write({
+                                'workflow_template_id': workflow_template.id,
+                                'template_id': False,  # Clear legacy reference
+                            })
+                            team_count += 1
+                            _logger.info(f"    Updated team: {team.name} (ID: {team.id}) - assigned workflow template")
+                        else:
+                            # Team already has workflow_template_id
+                            # If we migrated fields to a different workflow, update it
+                            if team.workflow_template_id.id != workflow_template.id:
+                                _logger.info(f"    Team {team.name} had workflow '{team.workflow_template_id.name}', updating to '{workflow_template.name}'")
+                                team.write({
+                                    'workflow_template_id': workflow_template.id,
+                                    'template_id': False,  # Clear legacy reference
+                                })
+                                team_count += 1
+                            else:
+                                # Same workflow template - just clear template_id
+                                team.write({
+                                    'template_id': False,  # Clear legacy reference
+                                })
+                                _logger.info(f"    Team {team.name} already uses workflow '{workflow_template.name}'. Cleared template_id only.")
+                    except Exception as e:
+                        _logger.error(f"    Error updating team {team.id}: {e}", exc_info=True)
+                        error_count += 1
+                        continue
+                
+                _logger.info(f"  Updated {team_count} team(s)")
+                
+                # 5. Regenerate forms for affected teams (skip in production if too many)
+                teams_affected = env['helpdesk.team'].search([
+                    ('workflow_template_id', '=', workflow_template.id),
+                    ('use_website_helpdesk_form', '=', True)
+                ])
+                
+                # Limit regeneration to avoid timeout in production
+                max_teams_to_regenerate = 50
+                if len(teams_affected) > max_teams_to_regenerate:
+                    _logger.warning(f"    Too many teams ({len(teams_affected)}) to regenerate. Skipping automatic regeneration.")
+                    _logger.warning(f"    Please regenerate forms manually or via cron.")
+                else:
+                    for team in teams_affected:
+                        if team.website_form_view_id:
+                            try:
+                                team._regenerate_form_from_template()
+                                _logger.info(f"    Regenerated form for team: {team.name}")
+                            except Exception as e:
+                                _logger.warning(f"    Could not regenerate form for team {team.id}: {e}")
+                                # Don't fail migration if form regeneration fails
+                                continue
+                
+                migrated_count += 1
+                _logger.info(f"✅ Successfully migrated template: {template.name}")
+                
+            except Exception as e:
+                _logger.error(f"❌ Error migrating template {template.id} ({template.name}): {e}", exc_info=True)
+                error_count += 1
+                skipped_count += 1
+                continue
+        
+        # 6. Summary
+        _logger.info("\n" + "=" * 80)
+        _logger.info("Migration Summary:")
+        _logger.info(f"  Templates processed: {len(templates)}")
+        _logger.info(f"  Successfully migrated: {migrated_count}")
+        _logger.info(f"  Skipped/Errors: {skipped_count}")
+        _logger.info(f"  Total errors: {error_count}")
+        _logger.info("=" * 80)
+        
+        # 7. Verify migration
+        remaining_templates = env['helpdesk.template'].search([])
+        if remaining_templates:
+            _logger.warning(f"⚠️  {len(remaining_templates)} template(s) still exist. They may have errors or were skipped.")
+        else:
+            _logger.info("✅ All templates migrated successfully")
+        
+        # Check for orphaned fields (fields with template_id but no workflow_template_id)
+        orphaned_fields = env['helpdesk.template.field'].search([
+            ('template_id', '!=', False),
+            ('workflow_template_id', '=', False)
+        ])
+        if orphaned_fields:
+            _logger.warning(f"⚠️  Found {len(orphaned_fields)} orphaned field(s) (have template_id but no workflow_template_id)")
+            _logger.warning(f"    These fields will need manual migration or will be cleaned up later.")
+        
+        # Commit only if we have successful migrations or no templates to migrate
+        if migrated_count > 0 or len(templates) == 0:
+            cr.commit()
+            _logger.info("✅ Migration completed successfully")
+        else:
+            # If all templates failed, rollback but don't block module update
+            # (data might be corrupted, but module structure is OK)
+            _logger.error("⚠️  All templates failed to migrate. Rolling back data changes.")
+            try:
+                cr.rollback()
+            except Exception:
+                pass
+            _logger.error("Module update will continue, but data migration needs manual intervention.")
+        
+    except Exception as e:
+        _logger.error(f"❌ Critical error in migration: {e}", exc_info=True)
+        try:
+            cr.rollback()
+        except Exception:
+            pass
+        # Don't raise - allow module update to complete even if migration fails
+        # The module structure is OK, only data migration failed
+        _logger.error("Migration failed but module update will continue. Please review logs and fix manually if needed.")
+

+ 137 - 0
helpdesk_extras/migrations/18.0.1.0.12/pre-migration.py

@@ -0,0 +1,137 @@
+# -*- coding: utf-8 -*-
+"""
+Pre-migration script to prepare database schema for template integration
+
+This script runs BEFORE the module update to:
+1. Make template_id nullable
+2. Add workflow_template_id column
+3. Prepare for constraint changes
+"""
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+def migrate(cr, version):
+    """Prepare database schema for template integration"""
+    try:
+        _logger.info("=" * 80)
+        _logger.info("Starting pre-migration: Preparing database schema")
+        _logger.info("=" * 80)
+        
+        # 1. Check if workflow_template_id column exists
+        cr.execute("""
+            SELECT column_name 
+            FROM information_schema.columns 
+            WHERE table_name = 'helpdesk_template_field' 
+            AND column_name = 'workflow_template_id'
+        """)
+        workflow_col_exists = cr.fetchone()
+        
+        if not workflow_col_exists:
+            _logger.info("Adding workflow_template_id column to helpdesk_template_field...")
+            cr.execute("""
+                ALTER TABLE helpdesk_template_field 
+                ADD COLUMN workflow_template_id INTEGER
+            """)
+            _logger.info("✅ Column workflow_template_id added")
+        else:
+            _logger.info("Column workflow_template_id already exists")
+        
+        # 2. Add foreign key constraint if it doesn't exist
+        cr.execute("""
+            SELECT constraint_name 
+            FROM information_schema.table_constraints 
+            WHERE table_name = 'helpdesk_template_field' 
+            AND constraint_name = 'helpdesk_template_field_workflow_template_id_fkey'
+        """)
+        fk_exists = cr.fetchone()
+        
+        if not fk_exists:
+            _logger.info("Adding foreign key constraint for workflow_template_id...")
+            cr.execute("""
+                ALTER TABLE helpdesk_template_field 
+                ADD CONSTRAINT helpdesk_template_field_workflow_template_id_fkey 
+                FOREIGN KEY (workflow_template_id) 
+                REFERENCES helpdesk_workflow_template(id) 
+                ON DELETE CASCADE
+            """)
+            _logger.info("✅ Foreign key constraint added")
+        else:
+            _logger.info("Foreign key constraint already exists")
+        
+        # 3. Make template_id nullable (if it's not already)
+        cr.execute("""
+            SELECT is_nullable 
+            FROM information_schema.columns 
+            WHERE table_name = 'helpdesk_template_field' 
+            AND column_name = 'template_id'
+        """)
+        template_nullable = cr.fetchone()
+        
+        if template_nullable and template_nullable[0] == 'NO':
+            _logger.info("Making template_id nullable...")
+            # First, drop the NOT NULL constraint by altering the column
+            cr.execute("""
+                ALTER TABLE helpdesk_template_field 
+                ALTER COLUMN template_id DROP NOT NULL
+            """)
+            _logger.info("✅ template_id is now nullable")
+        else:
+            _logger.info("template_id is already nullable")
+        
+        # 4. Add index on workflow_template_id for performance
+        cr.execute("""
+            SELECT indexname 
+            FROM pg_indexes 
+            WHERE tablename = 'helpdesk_template_field' 
+            AND indexname = 'helpdesk_template_field_workflow_template_id_index'
+        """)
+        index_exists = cr.fetchone()
+        
+        if not index_exists:
+            _logger.info("Adding index on workflow_template_id...")
+            cr.execute("""
+                CREATE INDEX helpdesk_template_field_workflow_template_id_index 
+                ON helpdesk_template_field(workflow_template_id)
+            """)
+            _logger.info("✅ Index added")
+        else:
+            _logger.info("Index already exists")
+        
+        # 5. Add CHECK constraint to ensure only one of template_id or workflow_template_id is set
+        cr.execute("""
+            SELECT constraint_name 
+            FROM information_schema.table_constraints 
+            WHERE table_name = 'helpdesk_template_field' 
+            AND constraint_name = 'helpdesk_template_field_check_template_or_workflow'
+        """)
+        check_exists = cr.fetchone()
+        
+        if not check_exists:
+            _logger.info("Adding CHECK constraint for template_or_workflow...")
+            cr.execute("""
+                ALTER TABLE helpdesk_template_field 
+                ADD CONSTRAINT helpdesk_template_field_check_template_or_workflow
+                CHECK ((template_id IS NOT NULL AND workflow_template_id IS NULL) 
+                       OR (template_id IS NULL AND workflow_template_id IS NOT NULL))
+            """)
+            _logger.info("✅ CHECK constraint added")
+        else:
+            _logger.info("CHECK constraint already exists")
+        
+        cr.commit()
+        _logger.info("=" * 80)
+        _logger.info("✅ Pre-migration completed successfully")
+        _logger.info("=" * 80)
+        
+    except Exception as e:
+        _logger.error(f"❌ Error in pre-migration: {e}", exc_info=True)
+        try:
+            cr.rollback()
+        except Exception:
+            pass  # Ignore rollback errors
+        # Raise to prevent module update if pre-migration fails
+        # This ensures database is in consistent state
+        raise
+

+ 100 - 31
helpdesk_extras/models/helpdesk_team.py

@@ -37,8 +37,8 @@ class HelpdeskTeamExtras(models.Model):
         """Override create to regenerate form XML if template is set"""
         teams = super().create(vals_list)
         # After create, if template is set and form view exists, regenerate
-        # This handles the case when team is created with template_id already set
-        for team in teams.filtered(lambda t: t.use_website_helpdesk_form and t.template_id and t.website_form_view_id):
+        # This handles the case when team is created with template_id or workflow_template_id already set
+        for team in teams.filtered(lambda t: t.use_website_helpdesk_form and t.website_form_view_id and (t.template_id or (t.workflow_template_id and t.workflow_template_id.field_ids))):
             team._regenerate_form_from_template()
         return teams
 
@@ -47,7 +47,7 @@ class HelpdeskTeamExtras(models.Model):
         result = super()._ensure_submit_form_view()
         # After view is created, if template is set, regenerate form
         # Note: super() may have created views, so we need to refresh to get updated website_form_view_id
-        for team in self.filtered(lambda t: t.use_website_helpdesk_form and t.template_id):
+        for team in self.filtered(lambda t: t.use_website_helpdesk_form and (t.template_id or (t.workflow_template_id and t.workflow_template_id.field_ids))):
             # Refresh to get updated website_form_view_id after super() created it
             team.invalidate_recordset(['website_form_view_id'])
             if team.website_form_view_id:
@@ -57,7 +57,7 @@ class HelpdeskTeamExtras(models.Model):
     def write(self, vals):
         """Override write to regenerate form XML when template changes"""
         result = super().write(vals)
-        if 'template_id' in vals:
+        if 'template_id' in vals or 'workflow_template_id' in vals:
             # Regenerate form XML when template is assigned/changed
             # After super().write(), refresh teams to get updated values
             teams_to_process = self.browse(self.ids).filtered('use_website_helpdesk_form')
@@ -66,11 +66,12 @@ class HelpdeskTeamExtras(models.Model):
                 # This handles the case when template is assigned but view doesn't exist yet
                 if not team.website_form_view_id:
                     # Call _ensure_submit_form_view which will create the view if needed
-                    # This method already handles template regeneration if template_id is set
+                    # This method handles template regeneration for both template_id and workflow_template_id
                     team._ensure_submit_form_view()
                 else:
                     # View exists, regenerate or restore form based on template
-                    if team.template_id:
+                    has_template = team.template_id or (team.workflow_template_id and team.workflow_template_id.field_ids)
+                    if has_template:
                         team._regenerate_form_from_template()
                     else:
                         # If template is removed, restore default form
@@ -393,9 +394,22 @@ class HelpdeskTeamExtras(models.Model):
         return result
 
     def _regenerate_form_from_template(self):
-        """Regenerate the website form XML based on the template"""
+        """Regenerate the website form XML based on the template (supports both legacy template_id and workflow_template_id)"""
         self.ensure_one()
-        if not self.template_id or not self.website_form_view_id:
+        
+        # Get template fields from either legacy template or workflow template
+        template_fields = self.env['helpdesk.template.field']
+        template_source = None
+        
+        # Priority: workflow_template_id over template_id (legacy)
+        if self.workflow_template_id and self.workflow_template_id.field_ids:
+            template_fields = self.workflow_template_id.field_ids.sorted('sequence')
+            template_source = f"workflow_template {self.workflow_template_id.id}"
+        elif self.template_id and self.template_id.field_ids:
+            template_fields = self.template_id.field_ids.sorted('sequence')
+            template_source = f"template {self.template_id.id}"
+        
+        if not template_fields or not self.website_form_view_id:
             return
 
         # Get base form structure (from default template)
@@ -422,12 +436,9 @@ class HelpdeskTeamExtras(models.Model):
         
         # Create environment with website language for translations
         env_lang = self.env(context=dict(self.env.context, lang=lang))
-
-        # Get template fields sorted by sequence
-        template_fields = self.template_id.field_ids.sorted('sequence')
         
         # Log template fields for debugging
-        _logger.info(f"Regenerating form for team {self.id}, template {self.template_id.id} with {len(template_fields)} fields")
+        _logger.info(f"Regenerating form for team {self.id}, {template_source} with {len(template_fields)} fields")
         for tf in template_fields:
             _logger.info(f"  - Field: {tf.field_id.name if tf.field_id else 'None'} (type: {tf.field_id.ttype if tf.field_id else 'None'})")
         
@@ -1303,21 +1314,67 @@ class HelpdeskTeamExtras(models.Model):
         
         # Mapping: stage_template_id -> real_stage_id
         stage_mapping = {}
+        stages_created = 0
+        stages_reused = 0
         
-        # 1. Create real stages from template stages
+        # 1. Create or reuse real stages from template stages
         for stage_template in template.stage_template_ids.sorted('sequence'):
-            stage_vals = {
-                'name': stage_template.name,
-                'sequence': stage_template.sequence,
-                'fold': stage_template.fold,
-                'description': stage_template.description or False,
-                'template_id': stage_template.template_id_email.id if stage_template.template_id_email else False,
-                'legend_blocked': stage_template.legend_blocked,
-                'legend_done': stage_template.legend_done,
-                'legend_normal': stage_template.legend_normal,
-                'team_ids': [(4, self.id)],
-            }
-            real_stage = self.env['helpdesk.stage'].create(stage_vals)
+            # Check if a stage with the same name already exists for this team
+            existing_stage = self.env['helpdesk.stage'].search([
+                ('name', '=', stage_template.name),
+                ('team_ids', 'in', [self.id])
+            ], limit=1)
+            
+            if existing_stage:
+                # Reuse existing stage
+                stages_reused += 1
+                _logger.info(
+                    f"Reusing existing stage '{existing_stage.name}' (ID: {existing_stage.id}) "
+                    f"for team '{self.name}' instead of creating duplicate"
+                )
+                real_stage = existing_stage
+                # Update stage properties from template if needed
+                update_vals = {}
+                if stage_template.sequence != existing_stage.sequence:
+                    update_vals['sequence'] = stage_template.sequence
+                if stage_template.fold != existing_stage.fold:
+                    update_vals['fold'] = stage_template.fold
+                if stage_template.description and stage_template.description != existing_stage.description:
+                    update_vals['description'] = stage_template.description
+                if stage_template.template_id_email and stage_template.template_id_email.id != existing_stage.template_id.id:
+                    update_vals['template_id'] = stage_template.template_id_email.id
+                if stage_template.legend_blocked != existing_stage.legend_blocked:
+                    update_vals['legend_blocked'] = stage_template.legend_blocked
+                if stage_template.legend_done != existing_stage.legend_done:
+                    update_vals['legend_done'] = stage_template.legend_done
+                if stage_template.legend_normal != existing_stage.legend_normal:
+                    update_vals['legend_normal'] = stage_template.legend_normal
+                
+                if update_vals:
+                    existing_stage.write(update_vals)
+                
+                # Ensure stage is linked to this team (in case it wasn't)
+                if self.id not in existing_stage.team_ids.ids:
+                    existing_stage.team_ids = [(4, self.id)]
+            else:
+                # Create new stage
+                stage_vals = {
+                    'name': stage_template.name,
+                    'sequence': stage_template.sequence,
+                    'fold': stage_template.fold,
+                    'description': stage_template.description or False,
+                    'template_id': stage_template.template_id_email.id if stage_template.template_id_email else False,
+                    'legend_blocked': stage_template.legend_blocked,
+                    'legend_done': stage_template.legend_done,
+                    'legend_normal': stage_template.legend_normal,
+                    'team_ids': [(4, self.id)],
+                }
+                real_stage = self.env['helpdesk.stage'].create(stage_vals)
+                stages_created += 1
+                _logger.info(
+                    f"Created new stage '{real_stage.name}' (ID: {real_stage.id}) for team '{self.name}'"
+                )
+            
             stage_mapping[stage_template.id] = real_stage.id
         
         # 2. Create real SLAs from template SLAs
@@ -1364,17 +1421,29 @@ class HelpdeskTeamExtras(models.Model):
         if template.sla_template_ids and not self.use_sla:
             self.use_sla = True
         
+        # Build notification message
+        if stages_reused > 0:
+            message = _(
+                'Successfully applied template "%s": %d stage(s) reused, %d new stage(s) created, and %d SLA policy(ies) created.',
+                template.name,
+                stages_reused,
+                stages_created,
+                len(template.sla_template_ids)
+            )
+        else:
+            message = _(
+                'Successfully created %d stage(s) and %d SLA policy(ies) from template "%s".',
+                len(stage_mapping),
+                len(template.sla_template_ids),
+                template.name
+            )
+        
         return {
             'type': 'ir.actions.client',
             'tag': 'display_notification',
             'params': {
                 'title': _('Workflow Template Applied'),
-                'message': _(
-                    'Successfully created %d stage(s) and %d SLA policy(ies) from template "%s".',
-                    len(stage_mapping),
-                    len(template.sla_template_ids),
-                    template.name
-                ),
+                'message': message,
                 'type': 'success',
                 'sticky': False,
             }

+ 87 - 10
helpdesk_extras/models/helpdesk_template.py

@@ -231,9 +231,18 @@ class HelpdeskTemplateField(models.Model):
     template_id = fields.Many2one(
         'helpdesk.template',
         string='Template',
-        required=True,
+        required=False,  # Made optional to support workflow_template_id
+        ondelete='cascade',
+        index=True,
+        help="Legacy template (deprecated - use workflow_template_id instead)"
+    )
+    workflow_template_id = fields.Many2one(
+        'helpdesk.workflow.template',
+        string='Workflow Template',
+        required=False,  # Made optional to support template_id (legacy)
         ondelete='cascade',
-        index=True
+        index=True,
+        help="Workflow template containing this field"
     )
     field_id = fields.Many2one(
         'ir.model.fields',
@@ -505,7 +514,11 @@ class HelpdeskTemplateField(models.Model):
 
     _sql_constraints = [
         ('unique_template_field', 'unique(template_id, field_id)',
-         'A field can only be added once to a template')
+         'A field can only be added once to a template'),
+        ('unique_workflow_template_field', 'unique(workflow_template_id, field_id)',
+         'A field can only be added once to a workflow template'),
+        # Note: CHECK constraint added via pre-migration script to avoid issues during module update
+        # The constraint is: (template_id IS NOT NULL AND workflow_template_id IS NULL) OR (template_id IS NULL AND workflow_template_id IS NOT NULL)
     ]
 
     @api.model
@@ -614,10 +627,11 @@ class HelpdeskTemplateField(models.Model):
         
         fields_created = super().create(vals_list)
         
-        # Get unique templates that were modified
+        # Get unique templates that were modified (both legacy and workflow)
         templates = fields_created.mapped('template_id')
+        workflow_templates = fields_created.mapped('workflow_template_id')
         
-        # Regenerate forms in all teams using these templates
+        # Regenerate forms in all teams using legacy templates
         for template in templates:
             if not template:
                 continue
@@ -637,6 +651,26 @@ class HelpdeskTemplateField(models.Model):
                     except Exception as e:
                         _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
         
+        # Regenerate forms in all teams using workflow templates
+        for workflow_template in workflow_templates:
+            if not workflow_template:
+                continue
+            teams = self.env['helpdesk.team'].search([
+                ('workflow_template_id', '=', workflow_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 workflow template {workflow_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):
@@ -663,11 +697,13 @@ class HelpdeskTemplateField(models.Model):
         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
+                                       'widget', 'selection_options', 'rows', 'input_type', 'selection_type',
+                                       'template_id', 'workflow_template_id']):
+            # Get unique templates that were modified (both legacy and workflow)
             templates = self.mapped('template_id')
+            workflow_templates = self.mapped('workflow_template_id')
             
-            # Regenerate forms in all teams using these templates
+            # Regenerate forms in all teams using legacy templates
             for template in templates:
                 if not template:
                     continue
@@ -686,6 +722,26 @@ class HelpdeskTemplateField(models.Model):
                             _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)
+            
+            # Regenerate forms in all teams using workflow templates
+            for workflow_template in workflow_templates:
+                if not workflow_template:
+                    continue
+                teams = self.env['helpdesk.team'].search([
+                    ('workflow_template_id', '=', workflow_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 workflow template {workflow_template.id}")
+                        except Exception as e:
+                            _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
         
         return result
 
@@ -701,12 +757,13 @@ class HelpdeskTemplateField(models.Model):
                 % ', '.join(field_names)
             )
         
-        # Get templates before deletion
+        # Get templates before deletion (both legacy and workflow)
         templates = self.mapped('template_id')
+        workflow_templates = self.mapped('workflow_template_id')
         
         result = super().unlink()
         
-        # Regenerate forms in all teams using these templates
+        # Regenerate forms in all teams using legacy templates
         for template in templates:
             if not template:
                 continue
@@ -726,4 +783,24 @@ class HelpdeskTemplateField(models.Model):
                     except Exception as e:
                         _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
         
+        # Regenerate forms in all teams using workflow templates
+        for workflow_template in workflow_templates:
+            if not workflow_template:
+                continue
+            teams = self.env['helpdesk.team'].search([
+                ('workflow_template_id', '=', workflow_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 workflow template {workflow_template.id}")
+                    except Exception as e:
+                        _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
+        
         return result

+ 15 - 6
helpdesk_extras/models/helpdesk_ticket.py

@@ -87,11 +87,13 @@ class HelpdeskTicket(models.Model):
         help=_("Files attached to this ticket")
     )
 
-    @api.depends('team_id.template_id')
+    @api.depends('team_id.template_id', 'team_id.workflow_template_id')
     def _compute_has_template(self):
-        """Compute if team has a template"""
+        """Compute if team has a template (supports both legacy template_id and workflow_template_id)"""
         for ticket in self:
-            ticket.has_template = bool(ticket.team_id and ticket.team_id.template_id)
+            has_legacy_template = bool(ticket.team_id and ticket.team_id.template_id)
+            has_workflow_template = bool(ticket.team_id and ticket.team_id.workflow_template_id and ticket.team_id.workflow_template_id.field_ids)
+            ticket.has_template = has_legacy_template or has_workflow_template
 
     @api.model
     def _default_request_type_id(self):
@@ -103,11 +105,18 @@ 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 template fields for this ticket's team (supports both legacy template_id and workflow_template_id)"""
         self.ensure_one()
-        if not self.team_id or not self.team_id.template_id:
+        if not self.team_id:
             return self.env['helpdesk.template.field']
-        return self.team_id.template_id.field_ids.sorted('sequence')
+        
+        # Priority: workflow_template_id over template_id (legacy)
+        if self.team_id.workflow_template_id and self.team_id.workflow_template_id.field_ids:
+            return self.team_id.workflow_template_id.field_ids.sorted('sequence')
+        elif self.team_id.template_id and self.team_id.template_id.field_ids:
+            return self.team_id.template_id.field_ids.sorted('sequence')
+        
+        return self.env['helpdesk.template.field']
 
     @api.onchange('request_type_id', 'business_impact')
     def _onchange_compute_priority(self):

+ 14 - 1
helpdesk_extras/models/helpdesk_workflow_template.py

@@ -41,6 +41,13 @@ class HelpdeskWorkflowTemplate(models.Model):
         string='SLA Policies',
         help='SLA policies included in this workflow template'
     )
+    field_ids = fields.One2many(
+        'helpdesk.template.field',
+        'workflow_template_id',
+        string='Fields',
+        copy=True,
+        help='Fields included in this workflow template for ticket forms'
+    )
     stage_count = fields.Integer(
         string='Stages Count',
         compute='_compute_counts',
@@ -51,6 +58,11 @@ class HelpdeskWorkflowTemplate(models.Model):
         compute='_compute_counts',
         store=False
     )
+    field_count = fields.Integer(
+        string='Fields Count',
+        compute='_compute_counts',
+        store=False
+    )
     team_ids = fields.One2many(
         'helpdesk.team',
         'workflow_template_id',
@@ -63,12 +75,13 @@ class HelpdeskWorkflowTemplate(models.Model):
         store=False
     )
 
-    @api.depends('stage_template_ids', 'sla_template_ids', 'team_ids')
+    @api.depends('stage_template_ids', 'sla_template_ids', 'team_ids', 'field_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.team_count = len(template.team_ids)
+            template.field_count = len(template.field_ids)
 
     def action_view_teams(self):
         """Open teams using this template"""

+ 4 - 4
helpdesk_extras/views/helpdesk_team_views.xml

@@ -17,7 +17,7 @@
             <xpath expr="//div[@id='channels']" position="before">
                 <h2>Workflow Template</h2>
                 <div class="row mt16 o_settings_container">
-                    <setting string="Workflow Template" help="Select a workflow template to quickly set up stages and SLA policies">
+                    <setting string="Workflow Template" help="Select a workflow template to set up fields, stages and SLA policies. This replaces the legacy Template field.">
                         <field name="workflow_template_id" options="{'no_create': True}"/>
                         <button name="%(helpdesk_extras.helpdesk_workflow_template_apply_wizard_action)d"
                                 type="action"
@@ -26,10 +26,10 @@
                                 context="{'active_id': id, 'default_team_id': id}"/>
                     </setting>
                 </div>
-                <h2>Template</h2>
+                <h2>Template (Legacy - Deprecated)</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 string="Ticket Template (Legacy)" help="⚠️ DEPRECATED: Use Workflow Template instead. This field will be removed in a future version. If you have a template here, it will be migrated automatically.">
+                        <field name="template_id" options="{'no_create': True}" attrs="{'readonly': [('workflow_template_id', '!=', False)]}"/>
                     </setting>
                 </div>
                 <h2>Collaborators</h2>

+ 66 - 2
helpdesk_extras/views/helpdesk_workflow_template_views.xml

@@ -8,6 +8,7 @@
             <list string="Workflow Templates" decoration-muted="active == False">
                 <field name="sequence" widget="handle" invisible="1"/>
                 <field name="name"/>
+                <field name="field_count" string="Fields" sum="Total Fields"/>
                 <field name="stage_count" string="Stages" sum="Total Stages"/>
                 <field name="sla_count" string="SLA Policies" sum="Total SLAs"/>
                 <field name="team_count" string="Teams Using"/>
@@ -39,11 +40,15 @@
                             </div>
                             <div class="o_kanban_card_content">
                                 <div class="row mb-2">
-                                    <div class="col-6">
+                                    <div class="col-4">
+                                        <div class="text-muted small">Fields</div>
+                                        <div class="fw-bold"><field name="field_count"/></div>
+                                    </div>
+                                    <div class="col-4">
                                         <div class="text-muted small">Stages</div>
                                         <div class="fw-bold"><field name="stage_count"/></div>
                                     </div>
-                                    <div class="col-6">
+                                    <div class="col-4">
                                         <div class="text-muted small">SLA Policies</div>
                                         <div class="fw-bold"><field name="sla_count"/></div>
                                     </div>
@@ -83,6 +88,64 @@
                     </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="Stages" name="stages">
                             <field name="stage_template_ids" nolabel="1">
                                 <list string="Stages" editable="bottom" default_order="sequence">
@@ -144,6 +207,7 @@
                 <filter string="Active" name="active" domain="[('active', '=', True)]"/>
                 <filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
                 <separator/>
+                <filter string="Has Fields" name="has_fields" domain="[('field_ids', '!=', False)]"/>
                 <filter string="Has Stages" name="has_stages" domain="[('stage_template_ids', '!=', False)]"/>
                 <filter string="Has SLAs" name="has_slas" domain="[('sla_template_ids', '!=', False)]"/>
                 <group expand="0" string="Group By">