# -*- 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 HelpdeskWorkflowTemplateField(models.Model): """ Form field configuration for workflow templates. Migrated from helpdesk.template.field to consolidate templates and workflows. """ _name = 'helpdesk.workflow.template.field' _description = 'Workflow Template Form Field' _order = 'sequence, id' workflow_template_id = fields.Many2one( 'helpdesk.workflow.template', string='Workflow 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." ) selection_options = fields.Text( string='Selection Options', help="For selection fields (not relations): JSON array of [value, label] pairs." ) 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" ) 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 for 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 visibility_condition_m2o_id = fields.Integer( string='Visibility Condition (Many2one ID)', help="ID of the selected record when dependency is a many2one field" ) 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 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 visibility_between = fields.Char( string='Visibility Between (End Value)', help="Second value for 'between' and '!between' comparators" ) def _get_visibility_condition_selection_options(self): """Return selection options based on visibility_dependency field""" if not self: return [] 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 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': 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(): 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': 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_workflow_template_field', 'unique(workflow_template_id, field_id)', 'A field can only be added once to a workflow template') ] @api.model_create_multi def create(self, vals_list): """Override create to mark model required fields and regenerate forms""" for vals in vals_list: if 'field_id' in vals and vals['field_id']: field = self.env['ir.model.fields'].browse(vals['field_id']) 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") fields_created = super().create(vals_list) # Regenerate forms in all teams using these workflow templates workflow_templates = fields_created.mapped('workflow_template_id') self._regenerate_team_forms(workflow_templates) return fields_created def write(self, vals): """Override write to mark model required fields and regenerate forms""" if 'field_id' in vals and vals['field_id']: field = self.env['ir.model.fields'].browse(vals['field_id']) if (field.model == 'helpdesk.ticket' and field.required and not field.website_form_blacklisted): vals['model_required'] = True else: vals['model_required'] = False elif 'field_id' in vals and not vals['field_id']: vals['model_required'] = False result = super().write(vals) # If any field configuration changed, regenerate forms regenerate_keys = ['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'] if any(key in vals for key in regenerate_keys): workflow_templates = self.mapped('workflow_template_id') self._regenerate_team_forms(workflow_templates) return result def unlink(self): """Prevent deletion of model required fields and regenerate forms""" 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) ) workflow_templates = self.mapped('workflow_template_id') result = super().unlink() self._regenerate_team_forms(workflow_templates) return result def _regenerate_team_forms(self, workflow_templates): """Helper to regenerate forms in teams using these workflow templates""" for wf_template in workflow_templates: if not wf_template: continue teams = self.env['helpdesk.team'].search([ ('workflow_template_id', '=', wf_template.id), ('use_website_helpdesk_form', '=', True) ]) for team in teams: if not team.website_form_view_id: team._ensure_submit_form_view() if team.website_form_view_id: try: team._regenerate_form_from_template() _logger.info(f"Regenerated form for team {team.id} after workflow template field change") except Exception as e: _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)