| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- # -*- 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)
|