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