|
|
@@ -0,0 +1,378 @@
|
|
|
+# -*- 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)
|