from odoo import models, fields, api, _ from odoo.exceptions import ValidationError class HrEfficiencyDynamicField(models.Model): _name = 'hr.efficiency.dynamic.field' _description = 'Dynamic Field Configuration' _order = 'sequence, name' name = fields.Char('Field Name', required=True, help='Technical field name (e.g., planning_efficiency)') label = fields.Char('Field Label', required=True, help='Display name (e.g., Planning Efficiency)') indicator_id = fields.Many2one('hr.efficiency.indicator', 'Related Indicator', required=True, ondelete='cascade') # Display settings sequence = fields.Integer('Sequence', default=10, help='Order in which fields appear') active = fields.Boolean('Active', default=True, help='Whether this field is visible') # View settings show_in_list = fields.Boolean('Show in List View', default=True) show_in_form = fields.Boolean('Show in Form View', default=True) show_in_search = fields.Boolean('Show in Search View', default=False) # Formatting widget = fields.Selection([ ('percentage', 'Percentage'), ('float_time', 'Float Time'), ('number', 'Number'), ('text', 'Text'), ('monetary', 'Monetary'), ], string='Widget', default='percentage', help='How to display the field') # Styling decoration_success = fields.Float('Success Threshold', help='Value above which to show green') decoration_warning = fields.Float('Warning Threshold', help='Value above which to show yellow') decoration_danger = fields.Float('Danger Threshold', help='Value below which to show red') # Computed fields field_technical_name = fields.Char('Technical Field Name', compute='_compute_technical_name', store=True) @api.depends('name') def _compute_technical_name(self): for record in self: if record.name: # Convert to valid field name field_name = record.name.lower() field_name = field_name.replace(' ', '_').replace('-', '_').replace('(', '').replace(')', '') field_name = field_name.replace('í', 'i').replace('á', 'a').replace('é', 'e').replace('ó', 'o').replace('ú', 'u') field_name = field_name.replace('ñ', 'n') # Ensure it starts with a letter if not field_name[0].isalpha(): field_name = 'indicator_' + field_name record.field_technical_name = field_name @api.constrains('name') def _check_name_unique(self): for record in self: if self.search_count([('name', '=', record.name), ('id', '!=', record.id)]) > 0: raise ValidationError(_('Field name must be unique')) @api.model_create_multi def create(self, vals_list): """Create dynamic fields and update model""" records = super().create(vals_list) self._update_model_fields() return records def write(self, vals): """Update dynamic field and update model""" result = super().write(vals) self._update_model_fields() return result def unlink(self): """Delete dynamic fields and update model""" result = super().unlink() self._update_model_fields() return result @api.model def _update_model_fields(self): """Update the hr.efficiency model with dynamic fields""" efficiency_model = self.env['hr.efficiency'] # Get all active dynamic fields dynamic_fields = self.search([('active', '=', True)], order='sequence') # Clear existing dynamic fields from model (only those starting with 'indicator_') for field_name in list(efficiency_model._fields.keys()): if field_name.startswith('indicator_'): if hasattr(efficiency_model.__class__, field_name): try: delattr(efficiency_model.__class__, field_name) except AttributeError: pass # Field might not exist, ignore # Add new dynamic fields for dynamic_field in dynamic_fields: field_name = dynamic_field.field_technical_name # Create field with appropriate widget field_kwargs = { 'string': dynamic_field.label, 'compute': '_compute_indicators', 'store': True, 'help': f'Dynamic indicator: {dynamic_field.indicator_id.name}', } # Add widget if specified if dynamic_field.widget: field_kwargs['widget'] = dynamic_field.widget # Create the field field = fields.Float(**field_kwargs) setattr(efficiency_model.__class__, field_name, field) def action_toggle_visibility(self): """Toggle field visibility""" for record in self: record.active = not record.active self._update_model_fields() def action_move_up(self): """Move field up in sequence""" for record in self: prev_record = self.search([ ('sequence', '<', record.sequence), ('active', '=', True) ], order='sequence desc', limit=1) if prev_record: record.sequence, prev_record.sequence = prev_record.sequence, record.sequence def action_move_down(self): """Move field down in sequence""" for record in self: next_record = self.search([ ('sequence', '>', record.sequence), ('active', '=', True) ], order='sequence', limit=1) if next_record: record.sequence, next_record.sequence = next_record.sequence, record.sequence