Przeglądaj źródła

feat: Major update to hr_efficiency module - Added new fields (wage, utilization_rate, overhead, precio_por_hora) - Extended hr.employee and res.company models - Added profitability indicators - Updated formulas and views - Enhanced calculation logic

root 5 miesięcy temu
rodzic
commit
7905335e6f

+ 13 - 2
hr_efficiency/README.md

@@ -98,8 +98,16 @@ El módulo se puede acceder desde:
 - `planned_hours`: Horas planeadas totales
 - `planned_billable_hours`: Horas planeadas en proyectos facturables
 - `planned_non_billable_hours`: Horas planeadas en proyectos no facturables
+- `actual_billable_hours`: Horas reales en proyectos facturables
+- `actual_non_billable_hours`: Horas reales en proyectos no facturables
+- `expected_hours_to_date`: Horas esperadas hasta la fecha actual
+- `wage`: Salario bruto del empleado al momento del cálculo
+- `utilization_rate`: Tasa de utilización del empleado al momento del cálculo
+- `overhead`: Overhead de la compañía al momento del cálculo
 - `actual_billable_hours`: Horas registradas en proyectos facturables
 - `actual_non_billable_hours`: Horas registradas en proyectos no facturables
+- `expected_hours_to_date`: Horas esperadas hasta la fecha actual
+- `wage`: Salario bruto del empleado según su contrato al momento del cálculo
 
 ### Visualización de Datos
 
@@ -176,8 +184,11 @@ Modelo principal que almacena los registros de eficiencia mensual por empleado.
 - `planned_non_billable_hours`: Horas planeadas en proyectos no facturables
 - `actual_billable_hours`: Horas reales en proyectos facturables
 - `actual_non_billable_hours`: Horas reales en proyectos no facturables
-- `efficiency_rate`: Porcentaje de eficiencia general
-- `billable_efficiency_rate`: Porcentaje de eficiencia en proyectos facturables
+- `wage`: Salario bruto del empleado según su contrato al momento del cálculo
+- `currency_id`: Moneda del salario
+- `utilization_rate`: Tasa de utilización del empleado al momento del cálculo
+- `overhead`: Overhead de la compañía al momento del cálculo
+- `overall_efficiency`: Porcentaje de eficiencia general basado en indicadores configurados
 
 ### hr.efficiency.calculation.wizard
 Wizard para ejecutar cálculos manuales de eficiencia.

+ 2 - 0
hr_efficiency/__manifest__.py

@@ -47,6 +47,8 @@ Features:
         'views/hr_efficiency_indicator_views.xml',
         'views/hr_efficiency_dynamic_field_views.xml',
         'views/hr_efficiency_dynamic_views.xml',
+        'views/hr_employee_views.xml',
+        'views/res_company_views.xml',
         'views/planning_views.xml',
         'report/hr_efficiency_report_views.xml',
     ],

+ 144 - 19
hr_efficiency/data/hr_efficiency_indicators.xml

@@ -1,30 +1,155 @@
 <?xml version="1.0" encoding="utf-8"?>
 <odoo>
     <data noupdate="1">
-        <!-- Default Planning Indicator -->
-        <record id="indicator_planning" model="hr.efficiency.indicator">
-            <field name="name">Planning Efficiency</field>
+
+        <!-- ================================================== -->
+        <!-- Indicadores de Capacidad (Capacity KPIs)           -->
+        <!-- ================================================== -->
+
+        <record id="indicator_occupancy_rate" model="hr.efficiency.indicator">
+            <field name="name">Occupancy Rate</field>
             <field name="sequence">10</field>
             <field name="active">True</field>
-            <field name="formula">(planned_hours / available_hours) * 100 if available_hours > 0 else 0</field>
-            <field name="target_percentage">90.0</field>
-            <field name="weight">50.0</field>
-            <field name="description">Measures how well the employee's time is planned compared to available hours. Target: at least 90% of available time should be planned.</field>
-            <field name="color_threshold_green">90.0</field>
-            <field name="color_threshold_yellow">70.0</field>
+            <field name="indicator_type">percentage</field>
+            <field name="formula">(total_actual_hours / available_hours) if available_hours > 0 else 0</field>
+            <field name="target_percentage">95.0</field>
+            <field name="weight">0.0</field>
+            <field name="description">Tasa de Ocupación: Mide qué tan "ocupado" está el equipo en general, considerando horas facturables y no facturables. (Total Horas Registradas / Horas Disponibles)</field>
+            <field name="color_threshold_green">85.0</field>
+            <field name="color_threshold_yellow">75.0</field>
         </record>
 
-        <!-- Default Time Tracking Indicator -->
-        <record id="indicator_time_tracking" model="hr.efficiency.indicator">
-            <field name="name">Time Tracking Efficiency</field>
+        <record id="indicator_utilization_rate" model="hr.efficiency.indicator">
+            <field name="name">Utilization Rate</field>
             <field name="sequence">20</field>
             <field name="active">True</field>
-            <field name="formula">((actual_billable_hours + actual_non_billable_hours) / planned_hours) * 100 if planned_hours > 0 else 0</field>
-            <field name="target_percentage">90.0</field>
-            <field name="weight">50.0</field>
-            <field name="description">Measures how well the employee tracks their time compared to planned hours. Target: at least 90% of planned time should be tracked.</field>
-            <field name="color_threshold_green">90.0</field>
-            <field name="color_threshold_yellow">70.0</field>
+            <field name="indicator_type">percentage</field>
+            <field name="formula">(actual_billable_hours / available_hours) if available_hours > 0 else 0</field>
+            <field name="target_percentage">80.0</field>
+            <field name="weight">0.0</field>
+            <field name="description">Tasa de Utilización: Mide qué tan "productivo" (generando ingresos) está el equipo. (Horas Facturables Registradas / Horas Disponibles)</field>
+            <field name="color_threshold_green">85.0</field>
+            <field name="color_threshold_yellow">75.0</field>
+        </record>
+
+        <!-- ================================================== -->
+        <!-- Indicadores de Eficiencia (Efficiency KPIs)        -->
+        <!-- ================================================== -->
+
+        <record id="indicator_billability_rate" model="hr.efficiency.indicator">
+            <field name="name">Billability Rate</field>
+            <field name="sequence">30</field>
+            <field name="active">True</field>
+            <field name="indicator_type">percentage</field>
+            <field name="formula">(actual_billable_hours / total_actual_hours) if total_actual_hours > 0 else 0</field>
+            <field name="target_percentage">85.0</field>
+            <field name="weight">0.0</field>
+            <field name="description">Tasa de Facturabilidad: De todo el tiempo trabajado, ¿qué porcentaje fue facturable? (Horas Facturables Registradas / Total Horas Registradas)</field>
+            <field name="color_threshold_green">85.0</field>
+            <field name="color_threshold_yellow">75.0</field>
         </record>
+
+        <!-- ================================================== -->
+        <!-- Indicadores de Planificación (Planning KPIs)       -->
+        <!-- ================================================== -->
+
+        <record id="indicator_planned_utilization" model="hr.efficiency.indicator">
+            <field name="name">Planned Utilization</field>
+            <field name="sequence">40</field>
+            <field name="active">True</field>
+            <field name="indicator_type">percentage</field>
+            <field name="formula">(planned_billable_hours / available_hours) if available_hours > 0 else 0</field>
+            <field name="target_percentage">80.0</field>
+            <field name="weight">0.0</field>
+            <field name="description">Utilización Planeada: ¿Cuál era el objetivo de utilización para el equipo? Permite comparar meta vs. realidad. (Horas Facturables Planeadas / Horas Disponibles)</field>
+            <field name="color_threshold_green">85.0</field>
+            <field name="color_threshold_yellow">75.0</field>
+        </record>
+
+        <record id="indicator_planning_coverage" model="hr.efficiency.indicator">
+            <field name="name">Planning Coverage</field>
+            <field name="sequence">50</field>
+            <field name="active">True</field>
+            <field name="indicator_type">percentage</field>
+            <field name="formula">(planned_hours / available_hours) if available_hours > 0 else 0</field>
+            <field name="target_percentage">95.0</field>
+            <field name="weight">0.0</field>
+            <field name="description">Cobertura de Planificación: Mide qué porcentaje del tiempo disponible ha sido planificado, sin importar si es facturable o no. (Total Horas Planeadas / Horas Disponibles)</field>
+            <field name="color_threshold_green">85.0</field>
+            <field name="color_threshold_yellow">75.0</field>
+        </record>
+
+        <record id="indicator_estimation_accuracy" model="hr.efficiency.indicator">
+            <field name="name">Estimation Accuracy Plan Adherence</field>
+            <field name="sequence">60</field>
+            <field name="active">True</field>
+            <field name="indicator_type">percentage</field>
+            <field name="formula">(total_actual_hours / planned_hours) if planned_hours > 0 else 0</field>
+            <field name="target_percentage">100.0</field>
+            <field name="weight">0.0</field>
+            <field name="description">Precisión de la Estimación: ¿Qué tan acertada fue la planificación general vs. la realidad? Un valor cercano a 100% es ideal. (Total Horas Registradas / Total Horas Planeadas)</field>
+            <field name="color_threshold_green">85.0</field>
+            <field name="color_threshold_yellow">75.0</field>
+        </record>
+
+        <record id="indicator_billable_plan_compliance" model="hr.efficiency.indicator">
+            <field name="name">Billable Plan Compliance</field>
+            <field name="sequence">70</field>
+            <field name="active">True</field>
+            <field name="indicator_type">percentage</field>
+            <field name="formula">(actual_billable_hours / planned_billable_hours) if planned_billable_hours > 0 else 0</field>
+            <field name="target_percentage">100.0</field>
+            <field name="weight">0.0</field>
+            <field name="description">Cumplimiento del Plan Facturable: ¿Se cumplió con el objetivo específico de horas facturables? (Horas Facturables Registradas / Horas Facturables Planeadas)</field>
+            <field name="color_threshold_green">85.0</field>
+            <field name="color_threshold_yellow">75.0</field>
+        </record>
+        
+        <!-- ================================================== -->
+        <!-- Indicadores de Rentabilidad (Profitability KPIs)   -->
+        <!-- ================================================== -->
+
+        <record id="indicator_break_even_hours" model="hr.efficiency.indicator">
+            <field name="name">Break-Even Hours Needed</field>
+            <field name="sequence">80</field>
+            <field name="active">True</field>
+            <field name="indicator_type">hours</field>
+            <!-- FORMULA RESTAURADA: Calcula el costo productivo basado en la tasa de utilización. -->
+            <field name="formula">(wage * (utilization_rate / 100) * (1 + (overhead / 100))) / precio_por_hora if precio_por_hora > 0 else 0</field>
+            <field name="target_percentage">0.0</field>
+            <field name="weight">0.0</field>
+            <field name="description">Horas de Punto de Equilibrio: ¿Cuántas horas facturables se necesitan para cubrir el costo productivo (costo ponderado por utilización)? El resultado es un número de horas.</field>
+            <field name="color_threshold_green">85.0</field>
+            <field name="color_threshold_yellow">75.0</field>
+        </record>
+
+        <record id="indicator_planned_profitability_coverage" model="hr.efficiency.indicator">
+            <field name="name">Planned Profitability Coverage</field>
+            <field name="sequence">90</field>
+            <field name="active">True</field>
+            <field name="indicator_type">percentage</field>
+            <!-- FORMULA AJUSTADA: Compara las horas planeadas contra las horas de punto de equilibrio (costo productivo). -->
+            <field name="formula">(planned_billable_hours / ((wage * (utilization_rate / 100) * (1 + (overhead / 100))) / precio_por_hora)) if wage > 0 and precio_por_hora > 0 else 0</field>
+            <field name="target_percentage">100.0</field>
+            <field name="weight">0.0</field>
+            <field name="description">Cobertura de Rentabilidad Planeada: Mide si las horas facturables planeadas son suficientes para alcanzar el punto de equilibrio. Más de 100% indica un plan rentable. (Horas Facturables Planeadas / Horas de Punto de Equilibrio)</field>
+            <field name="color_threshold_green">100.0</field>
+            <field name="color_threshold_yellow">85.0</field>
+        </record>
+
+        <record id="indicator_actual_profitability_achievement" model="hr.efficiency.indicator">
+            <field name="name">Actual Profitability Achievement</field>
+            <field name="sequence">100</field>
+            <field name="active">True</field>
+            <field name="indicator_type">percentage</field>
+            <!-- FORMULA AJUSTADA: Compara las horas reales contra las horas de punto de equilibrio (costo productivo). -->
+            <field name="formula">(actual_billable_hours / ((wage * (utilization_rate / 100) * (1 + (overhead / 100))) / precio_por_hora)) if wage > 0 and precio_por_hora > 0 else 0</field>
+            <field name="target_percentage">100.0</field>
+            <field name="weight">0.0</field>
+            <field name="description">Logro de Rentabilidad Real: Mide el progreso real hacia el punto de equilibrio basado en las horas facturables registradas. Más de 100% indica que ya se ha alcanzado la rentabilidad. (Horas Facturables Registradas / Horas de Punto de Equilibrio)</field>
+            <field name="color_threshold_green">100.0</field>
+            <field name="color_threshold_yellow">85.0</field>
+        </record>
+
     </data>
-</odoo>
+</odoo>

+ 2 - 0
hr_efficiency/models/__init__.py

@@ -4,3 +4,5 @@
 from . import hr_efficiency
 from . import hr_efficiency_indicator
 from . import hr_efficiency_dynamic_field
+from . import hr_employee
+from . import res_company

+ 503 - 120
hr_efficiency/models/hr_efficiency.py

@@ -31,6 +31,14 @@ class HrEfficiency(models.Model):
     # Available hours (what employee should work)
     available_hours = fields.Float('Available Hours', digits=(10, 2), help="Total hours employee should work in the month")
     
+    # Employee contract information
+    wage = fields.Float('Gross Salary', digits=(10, 2), aggregator='sum', help="Employee's gross salary from their contract at the time of calculation")
+    currency_id = fields.Many2one('res.currency', 'Currency', help="Currency for the wage field")
+    
+    # Employee utilization and company overhead (stored at calculation time)
+    utilization_rate = fields.Float('Utilization Rate (%)', digits=(5, 2), default=100.0, aggregator='avg', help="Employee's utilization rate at the time of calculation")
+    overhead = fields.Float('Company Overhead (%)', digits=(5, 2), default=40.0, aggregator='avg', help="Company's overhead percentage at the time of calculation")
+    
     # Planned hours (what was planned)
     planned_hours = fields.Float('Planned Hours', digits=(10, 2), help="Total hours planned for the month")
     planned_billable_hours = fields.Float('Planned Billable Hours', digits=(10, 2), help="Hours planned on billable projects")
@@ -40,16 +48,19 @@ class HrEfficiency(models.Model):
     actual_billable_hours = fields.Float('Actual Billable Hours', digits=(10, 2), help="Hours actually worked on billable projects")
     actual_non_billable_hours = fields.Float('Actual Non-Billable Hours', digits=(10, 2), help="Hours actually worked on non-billable projects")
     
-    # Computed fields
-    total_actual_hours = fields.Float('Total Actual Hours', compute='_compute_total_actual_hours', store=True)
-    expected_hours_to_date = fields.Float('Expected Hours to Date', compute='_compute_expected_hours_to_date', store=True, help='Hours that should be registered based on planning until current date')
+    # Calculated fields (stored for performance)
+    total_actual_hours = fields.Float('Total Actual Hours', digits=(10, 2), help="Total actual hours (billable + non-billable)")
+    expected_hours_to_date = fields.Float('Expected Hours to Date', digits=(10, 2), help='Hours that should be registered based on planning until current date')
+    
+    # Precio por Hora (stored field)
+    precio_por_hora = fields.Float('Precio por Hora', digits=(10, 2), help='Precio que cobramos al cliente por hora (costo + overhead + 30% rentabilidad)', aggregator='avg')
     
     # Dynamic indicator fields (will be created automatically)
     # These fields are managed dynamically based on hr.efficiency.indicator records
     
-    # Overall efficiency (always present)
-    overall_efficiency = fields.Float('Overall Efficiency (%)', compute='_compute_indicators', store=True, help='Overall efficiency based on configured indicators')
-    overall_efficiency_display = fields.Char('Overall Efficiency Display', compute='_compute_overall_efficiency_display', store=True, help='Overall efficiency formatted for display with badge widget')
+    # Overall efficiency (always present) - Now stored fields calculated on save
+    overall_efficiency = fields.Float('Overall Efficiency (%)', digits=(5, 2), help='Overall efficiency based on configured indicators')
+    overall_efficiency_display = fields.Char('Overall Efficiency Display', help='Overall efficiency formatted for display with badge widget')
     
     display_name = fields.Char('Display Name', compute='_compute_display_name', store=True)
     
@@ -71,72 +82,102 @@ class HrEfficiency(models.Model):
             else:
                 record.date = False
 
-    @api.depends('actual_billable_hours', 'actual_non_billable_hours')
-    def _compute_total_actual_hours(self):
-        for record in self:
-            record.total_actual_hours = record.actual_billable_hours + record.actual_non_billable_hours
 
-    @api.depends('available_hours', 'month_year', 'employee_id', 'employee_id.contract_id')
-    def _compute_expected_hours_to_date(self):
+
+
+
+    def _calculate_expected_hours_to_date(self, record):
         """
         Calculate expected hours to date based on available hours and working days
         """
-        for record in self:
-            if not record.month_year or not record.available_hours or not record.employee_id.contract_id:
-                record.expected_hours_to_date = 0.0
-                continue
+        if not record.month_year or not record.available_hours or not record.employee_id.contract_id:
+            return 0.0
+        
+        try:
+            # Parse month_year (format: YYYY-MM)
+            year, month = record.month_year.split('-')
+            start_date = date(int(year), int(month), 1)
+            end_date = (start_date + relativedelta(months=1)) - relativedelta(days=1)
             
-            try:
-                # Parse month_year (format: YYYY-MM)
-                year, month = record.month_year.split('-')
-                start_date = date(int(year), int(month), 1)
-                end_date = (start_date + relativedelta(months=1)) - relativedelta(days=1)
-                
-                # Get current date
-                current_date = date.today()
-                
-                # If current date is outside the month, use end of month
-                if current_date > end_date:
-                    calculation_date = end_date
-                elif current_date < start_date:
-                    calculation_date = start_date
-                else:
-                    calculation_date = current_date
+            # Get current date
+            current_date = date.today()
+            
+            # If current date is outside the month, use end of month
+            if current_date > end_date:
+                calculation_date = end_date
+            elif current_date < start_date:
+                calculation_date = start_date
+            else:
+                calculation_date = current_date
+            
+            # Calculate working days in the month
+            total_working_days = self._count_working_days(start_date, end_date, record.employee_id)
+            
+            # Calculate working days until current date
+            working_days_until_date = self._count_working_days(start_date, calculation_date, record.employee_id)
+            
+            if total_working_days > 0:
+                # Calculate expected hours based on available hours (what employee should work)
+                expected_hours = (record.available_hours / total_working_days) * working_days_until_date
                 
-                # Calculate working days in the month
-                total_working_days = self._count_working_days(start_date, end_date, record.employee_id)
+                # Ensure we don't exceed available hours for the month
+                expected_hours = min(expected_hours, record.available_hours)
                 
-                # Calculate working days until current date
-                working_days_until_date = self._count_working_days(start_date, calculation_date, record.employee_id)
+                return float_round(expected_hours, 2)
+            else:
+                return 0.0
                 
-                if total_working_days > 0:
-                    # Calculate expected hours based on available hours (what employee should work)
-                    expected_hours = (record.available_hours / total_working_days) * working_days_until_date
-                    
-                    # Ensure we don't exceed available hours for the month
-                    expected_hours = min(expected_hours, record.available_hours)
-                    
-                    record.expected_hours_to_date = float_round(expected_hours, 2)
-                else:
-                    record.expected_hours_to_date = 0.0
-                    
-            except (ValueError, AttributeError) as e:
-                record.expected_hours_to_date = 0.0
-
+        except (ValueError, AttributeError) as e:
+            return 0.0
 
-    @api.depends('overall_efficiency')
-    def _compute_overall_efficiency_display(self):
+    def _calculate_precio_por_hora(self, record):
         """
-        Compute display value for overall efficiency that works with badge widget
+        Calculate precio por hora: (wage / available_hours) * (1 + overhead/100) * 1.30
         """
-        for record in self:
-            if record.overall_efficiency == 0:
-                record.overall_efficiency_display = '0.00'
+        try:
+            wage = getattr(record, 'wage', 0.0) or 0.0
+            available_hours = getattr(record, 'available_hours', 0.0) or 0.0
+            overhead = getattr(record, 'overhead', 40.0) or 40.0
+            
+            if available_hours > 0:
+                # Calculate cost per hour
+                cost_per_hour = wage / available_hours
+                # Apply overhead and 30% profit margin
+                precio_por_hora = cost_per_hour * (1 + (overhead / 100)) * 1.30
+                return float_round(precio_por_hora, 2)
             else:
-                record.overall_efficiency_display = f"{record.overall_efficiency:.2f}"
+                return 0.0
+                
+        except (ValueError, AttributeError, ZeroDivisionError) as e:
+            return 0.0
 
-    @api.depends('available_hours', 'planned_hours', 'total_actual_hours')
-    def _compute_indicators(self):
+
+    def _calculate_all_indicators(self):
+        """
+        Calculate all indicators and overall efficiency for stored fields
+        This method is called on create/write instead of using computed fields
+        """
+        # Prepare values to update without triggering write recursively
+        values_to_update = {}
+        
+        # Pre-fetch necessary fields to avoid CacheMiss during iteration
+        fields_to_load = [
+            'available_hours', 'planned_hours', 'planned_billable_hours',
+            'planned_non_billable_hours', 'actual_billable_hours',
+            'actual_non_billable_hours', 'total_actual_hours',
+            'expected_hours_to_date', 'wage', 'utilization_rate', 'overhead',
+            'precio_por_hora', 'employee_id', 'company_id', 'month_year', 'active',
+        ]
+        
+        # Load fields for all records in 'self' with error handling
+        try:
+            self.read(fields_to_load)
+        except Exception as e:
+            import logging
+            _logger = logging.getLogger(__name__)
+            _logger.warning(f"Error loading fields: {e}")
+            # Continue with empty cache if needed
+        
         for record in self:
             # Get all manual fields for this model
             manual_fields = self.env['ir.model.fields'].search([
@@ -145,39 +186,117 @@ class HrEfficiency(models.Model):
                 ('ttype', '=', 'float')
             ])
             
-            # Prepare efficiency data
+            # Prepare efficiency data with safe field access
             efficiency_data = {
-                'available_hours': record.available_hours,
-                'planned_hours': record.planned_hours,
-                'planned_billable_hours': record.planned_billable_hours,
-                'planned_non_billable_hours': record.planned_non_billable_hours,
-                'actual_billable_hours': record.actual_billable_hours,
-                'actual_non_billable_hours': record.actual_non_billable_hours,
-                'expected_hours_to_date': record.expected_hours_to_date,
+                'available_hours': getattr(record, 'available_hours', 0.0) or 0.0,
+                'planned_hours': getattr(record, 'planned_hours', 0.0) or 0.0,
+                'planned_billable_hours': getattr(record, 'planned_billable_hours', 0.0) or 0.0,
+                'planned_non_billable_hours': getattr(record, 'planned_non_billable_hours', 0.0) or 0.0,
+                'actual_billable_hours': getattr(record, 'actual_billable_hours', 0.0) or 0.0,
+                'actual_non_billable_hours': getattr(record, 'actual_non_billable_hours', 0.0) or 0.0,
+                'total_actual_hours': getattr(record, 'total_actual_hours', 0.0) or 0.0,
+                'expected_hours_to_date': getattr(record, 'expected_hours_to_date', 0.0) or 0.0,
+                'wage': getattr(record, 'wage', 0.0) or 0.0,
+                'utilization_rate': getattr(record, 'utilization_rate', 100.0) or 100.0,
+                'overhead': getattr(record, 'overhead', 40.0) or 40.0,
+                'precio_por_hora': getattr(record, 'precio_por_hora', 0.0) or 0.0,
             }
             
-            # Calculate all indicators dynamically
-            for field in manual_fields:
-                # Find the corresponding indicator
-                indicator = self.env['hr.efficiency.indicator'].search([
-                    ('name', 'ilike', field.field_description)
-                ], limit=1)
+            # STEP 1: Calculate total_actual_hours FIRST (needed for indicators)
+            try:
+                total_actual_hours = getattr(record, 'actual_billable_hours', 0.0) + getattr(record, 'actual_non_billable_hours', 0.0)
+            except Exception as e:
+                import logging
+                _logger = logging.getLogger(__name__)
+                _logger.error(f"Error calculating total_actual_hours for record {record.id}: {e}")
+                total_actual_hours = 0.0
+            record_values = {'total_actual_hours': float_round(total_actual_hours, 2)}
+            
+            # STEP 2: Calculate expected_hours_to_date
+            expected_hours = self._calculate_expected_hours_to_date(record)
+            record_values['expected_hours_to_date'] = expected_hours
+            
+            # STEP 3: Calculate precio_por_hora (needed for profitability indicators)
+            precio_por_hora = self._calculate_precio_por_hora(record)
+            record_values['precio_por_hora'] = precio_por_hora
+            
+            # STEP 4: Update efficiency_data with ALL calculated base fields
+            efficiency_data.update({
+                'total_actual_hours': total_actual_hours,
+                'expected_hours_to_date': expected_hours,
+                'precio_por_hora': precio_por_hora,
+            })
+            
+            # STEP 5: Calculate all indicators dynamically (in sequence order)
+            active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
+            
+            for indicator in active_indicators:
+                field_name = self._get_indicator_field_name(indicator.name)
                 
-                if indicator:
+                # Check if the field exists in the record
+                if field_name in record._fields:
                     # Calculate indicator value using the indicator formula
                     indicator_value = indicator.evaluate_formula(efficiency_data)
                     
-                    # Set the value on the record - handle both computed and stored fields
-                    if field.name in record._fields:
-                        record[field.name] = float_round(indicator_value, 2)
-                    elif hasattr(record, field.name):
-                        setattr(record, field.name, float_round(indicator_value, 2))
+                    # Store the value to update later
+                    record_values[field_name] = float_round(indicator_value, 2)
+            
+            # Calculate overall efficiency
+            overall_efficiency = self._calculate_overall_efficiency(record)
+            record_values['overall_efficiency'] = overall_efficiency
+            
+            # Calculate overall efficiency display
+            if overall_efficiency == 0:
+                record_values['overall_efficiency_display'] = '0.00'
+            else:
+                record_values['overall_efficiency_display'] = f"{overall_efficiency:.2f}"
             
-            # Overall efficiency based on configured indicators
-            record.overall_efficiency = self._calculate_overall_efficiency(record)
+            # Store values for this record
+            values_to_update[record.id] = record_values
+        
+        # Update all records at once to avoid recursion
+        for record_id, values in values_to_update.items():
+            record = self.browse(record_id)
+            # Use direct SQL update to avoid triggering write method
+            if values:
+                record.env.cr.execute(
+                    "UPDATE hr_efficiency SET " + 
+                    ", ".join([f"{key} = %s" for key in values.keys()]) + 
+                    " WHERE id = %s",
+                    list(values.values()) + [record_id]
+                )
         
-        # Update stored manual fields after computing
-        self._update_stored_manual_fields()
+        # Update aggregation fields after calculating indicators
+        self._update_aggregation_fields()
+    
+    def _update_aggregation_fields(self):
+        """
+        Actualiza los campos de agregación con los valores de los indicadores dinámicos
+        """
+        try:
+            for record in self:
+                # Obtener todos los indicadores activos (en orden de secuencia)
+                active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
+                
+                for indicator in active_indicators:
+                    field_name = self._get_indicator_field_name(indicator.name)
+                    agg_field_name = f"{field_name}_agg"
+                    
+                    # Verificar si el campo de agregación existe
+                    if hasattr(record, agg_field_name):
+                        # Obtener el valor del indicador dinámico
+                        indicator_value = getattr(record, field_name, 0.0) or 0.0
+                        
+                        # Actualizar el campo de agregación usando SQL directo
+                        record.env.cr.execute(
+                            "UPDATE hr_efficiency SET %s = %%s WHERE id = %%s" % agg_field_name,
+                            [indicator_value, record.id]
+                        )
+                        
+        except Exception as e:
+            import logging
+            _logger = logging.getLogger(__name__)
+            _logger.warning(f"Error updating aggregation fields: {str(e)}")
     
     def _update_stored_manual_fields(self):
         """Update stored manual fields with computed values"""
@@ -190,15 +309,19 @@ class HrEfficiency(models.Model):
                 ('store', '=', True),
             ], order='id')
             
-            # Prepare efficiency data
+            # Prepare efficiency data with safe field access
             efficiency_data = {
-                'available_hours': record.available_hours,
-                'planned_hours': record.planned_hours,
-                'planned_billable_hours': record.planned_billable_hours,
-                'planned_non_billable_hours': record.planned_non_billable_hours,
-                'actual_billable_hours': record.actual_billable_hours,
-                'actual_non_billable_hours': record.actual_non_billable_hours,
-                'expected_hours_to_date': record.expected_hours_to_date,
+                'available_hours': record.available_hours or 0.0,
+                'planned_hours': record.planned_hours or 0.0,
+                'planned_billable_hours': record.planned_billable_hours or 0.0,
+                'planned_non_billable_hours': record.planned_non_billable_hours or 0.0,
+                'actual_billable_hours': record.actual_billable_hours or 0.0,
+                'actual_non_billable_hours': record.actual_non_billable_hours or 0.0,
+                'total_actual_hours': record.total_actual_hours or 0.0,
+                'expected_hours_to_date': record.expected_hours_to_date or 0.0,
+                'wage': record.wage or 0.0,
+                'utilization_rate': record.utilization_rate or 100.0,
+                'overhead': record.overhead or 40.0,
             }
             
             # Calculate and update stored manual fields
@@ -220,22 +343,34 @@ class HrEfficiency(models.Model):
         """Recompute all indicator fields for all records"""
         records = self.search([])
         if records:
-            records._compute_indicators()
+            records._calculate_all_indicators()
     
     def _get_indicator_field_name(self, indicator_name):
         """
         Convert indicator name to valid field name
         """
+        import re
+        
         # Remove special characters and convert to lowercase
         field_name = indicator_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')
+        
+        # Replace spaces, hyphens, and other special characters with underscores
+        field_name = re.sub(r'[^a-z0-9_]', '_', field_name)
+        
+        # Remove multiple consecutive underscores
+        field_name = re.sub(r'_+', '_', field_name)
+        
+        # Remove leading and trailing underscores
+        field_name = field_name.strip('_')
         
         # Ensure it starts with x_ for manual fields
         if not field_name.startswith('x_'):
             field_name = 'x_' + field_name
             
+        # Ensure it doesn't exceed 63 characters (PostgreSQL limit)
+        if len(field_name) > 63:
+            field_name = field_name[:63]
+            
         return field_name
     
 
@@ -250,13 +385,7 @@ class HrEfficiency(models.Model):
         
         for indicator in indicators:
             # Check if field already exists in ir.model.fields
-            field_name = indicator.name.lower().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 x_ for manual fields
-            if not field_name.startswith('x_'):
-                field_name = 'x_' + field_name
+            field_name = self._get_indicator_field_name(indicator.name)
             
             existing_field = self.env['ir.model.fields'].search([
                 ('model', '=', 'hr.efficiency'),
@@ -300,13 +429,17 @@ class HrEfficiency(models.Model):
         valid_indicators = 0
         
         efficiency_data = {
-            'available_hours': record.available_hours,
-            'planned_hours': record.planned_hours,
-            'planned_billable_hours': record.planned_billable_hours,
-            'planned_non_billable_hours': record.planned_non_billable_hours,
-            'actual_billable_hours': record.actual_billable_hours,
-            'actual_non_billable_hours': record.actual_non_billable_hours,
-            'expected_hours_to_date': record.expected_hours_to_date,
+            'available_hours': getattr(record, 'available_hours', 0.0) or 0.0,
+            'planned_hours': getattr(record, 'planned_hours', 0.0) or 0.0,
+            'planned_billable_hours': getattr(record, 'planned_billable_hours', 0.0) or 0.0,
+            'planned_non_billable_hours': getattr(record, 'planned_non_billable_hours', 0.0) or 0.0,
+            'actual_billable_hours': getattr(record, 'actual_billable_hours', 0.0) or 0.0,
+            'actual_non_billable_hours': getattr(record, 'actual_non_billable_hours', 0.0) or 0.0,
+            'total_actual_hours': getattr(record, 'total_actual_hours', 0.0) or 0.0,
+            'expected_hours_to_date': getattr(record, 'expected_hours_to_date', 0.0) or 0.0,
+            'wage': getattr(record, 'wage', 0.0) or 0.0,
+            'utilization_rate': getattr(record, 'utilization_rate', 100.0) or 100.0,
+            'overhead': getattr(record, 'overhead', 40.0) or 40.0,
         }
         
         for indicator in indicators:
@@ -355,6 +488,18 @@ class HrEfficiency(models.Model):
         # Calculate actual hours
         actual_billable_hours, actual_non_billable_hours = self._calculate_actual_hours(employee, start_date, end_date)
         
+        # Calculate wage and currency from employee's contract
+        # Use current date for wage calculation (when the record is being created/calculated)
+        wage_date = date.today()
+        wage, currency_id = self._get_employee_wage_and_currency(employee, wage_date)
+        
+        # Get employee's utilization rate and company's overhead at calculation time
+        utilization_rate = employee.utilization_rate or 100.0
+        overhead = employee.company_id.overhead or 40.0
+        
+        # Apply utilization_rate to actual_billable_hours to reflect real billable capacity
+        adjusted_actual_billable_hours = actual_billable_hours * (utilization_rate / 100)
+        
         return {
             'month_year': month_year,
             'employee_id': employee.id,
@@ -363,10 +508,54 @@ class HrEfficiency(models.Model):
             'planned_hours': planned_hours,
             'planned_billable_hours': planned_billable_hours,
             'planned_non_billable_hours': planned_non_billable_hours,
-            'actual_billable_hours': actual_billable_hours,
+            'actual_billable_hours': adjusted_actual_billable_hours,
             'actual_non_billable_hours': actual_non_billable_hours,
+            'wage': wage,
+            'currency_id': currency_id,
+            'utilization_rate': utilization_rate,
+            'overhead': overhead,
         }
 
+    @api.model
+    def _get_employee_wage_and_currency(self, employee, target_date):
+        """
+        Get employee's wage and currency from their contract for a specific date
+        Always take the contract that is active on the target date
+        If no active contract on that date, return 0 with company currency
+        """
+        if not employee:
+            return 0.0, self.env.company.currency_id.id
+        
+        # Get all contracts for the employee
+        contracts = self.env['hr.contract'].search([
+            ('employee_id', '=', employee.id),
+            ('state', '=', 'open')
+        ], order='date_start desc')
+        
+        if not contracts:
+            return 0.0, self.env.company.currency_id.id
+        
+        # Find the contract that is active on the target date
+        for contract in contracts:
+            contract_start = contract.date_start
+            
+            # Check if contract is active on target date
+            if contract.date_end:
+                # Contract has end date
+                if contract_start <= target_date <= contract.date_end:
+                    wage = contract.wage or 0.0
+                    currency_id = contract.currency_id.id if contract.currency_id else self.env.company.currency_id.id
+                    return wage, currency_id
+            else:
+                # Contract has no end date, it's active for any date after start
+                if contract_start <= target_date:
+                    wage = contract.wage or 0.0
+                    currency_id = contract.currency_id.id if contract.currency_id else self.env.company.currency_id.id
+                    return wage, currency_id
+        
+        # If no contract is active on the target date, return defaults
+        return 0.0, self.env.company.currency_id.id
+
     def _calculate_available_hours(self, employee, start_date, end_date):
         """
         Calculate available hours considering holidays and time off
@@ -553,7 +742,7 @@ class HrEfficiency(models.Model):
                     # If no changes in basic fields, check dynamic indicators
                     if not has_changes:
                         # Get all active indicators
-                        active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)])
+                        active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
                         
                         for indicator in active_indicators:
                             field_name = self._get_indicator_field_name(indicator.name)
@@ -591,6 +780,12 @@ class HrEfficiency(models.Model):
                     new_record = self.create(efficiency_data)
                     created_records.append(new_record)
         
+        # Calculate indicators for all newly created records
+        if created_records:
+            # Convert list to recordset
+            created_recordset = self.browse([record.id for record in created_records])
+            created_recordset._calculate_all_indicators()
+        
         return {
             'created': len(created_records),
             'updated': 0,  # No longer updating existing records
@@ -643,19 +838,119 @@ class HrEfficiency(models.Model):
             _logger.info("Running post-install hook for hr_efficiency module")
             
             # Ensure all active indicators have manual fields
-            active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)])
+            active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
             for indicator in active_indicators:
                 self.env['hr.efficiency.indicator']._create_dynamic_field(indicator)
             
             # Update views with dynamic fields
             self._update_views_with_dynamic_fields()
             
+            # Apply default values to existing records
+            self._apply_default_values_to_existing_records()
+            
             _logger.info(f"Post-install hook completed. Processed {len(active_indicators)} indicators")
             
         except Exception as e:
             _logger.error(f"Error in post-install hook: {str(e)}")
             raise
 
+
+    @api.model
+    def create(self, vals_list):
+        """
+        Override create to calculate indicators when records are created
+        """
+        records = super().create(vals_list)
+        
+        # Calculate indicators for newly created records
+        if records:
+            records._calculate_all_indicators()
+        
+        return records
+    
+    def write(self, vals):
+        """
+        Override write to recalculate indicators when records are updated
+        """
+        result = super().write(vals)
+        
+        # Recalculate indicators for updated records
+        self._calculate_all_indicators()
+        
+        return result
+
+    @api.model
+    def _register_hook(self):
+        """
+        Called when the registry is loaded. 
+        Update views with dynamic fields on every module load/restart.
+        """
+        super()._register_hook()
+        try:
+            # Ensure aggregation fields exist for dynamic indicators
+            self._ensure_aggregation_fields_exist()
+            # Update views with current dynamic fields on every module load
+            self._update_views_with_dynamic_fields()
+        except Exception as e:
+            # Log error but don't prevent module loading
+            import logging
+            _logger = logging.getLogger(__name__)
+            _logger.warning(f"Could not update dynamic views on module load: {str(e)}")
+
+    @api.model
+    def _ensure_aggregation_fields_exist(self):
+        """
+        Asegura que existan campos de agregación para todos los indicadores activos
+        """
+        try:
+            active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
+            
+            for indicator in active_indicators:
+                field_name = self._get_indicator_field_name(indicator.name)
+                agg_field_name = f"{field_name}_agg"
+                
+                # Verificar si el campo de agregación ya existe en ir.model.fields
+                agg_field_record = self.env['ir.model.fields'].search([
+                    ('model', '=', 'hr.efficiency'),
+                    ('name', '=', agg_field_name)
+                ], limit=1)
+                
+                if not agg_field_record:
+                    # Determinar tipo de agregación basado en el tipo de indicador
+                    agg_type = 'avg' if indicator.indicator_type == 'percentage' else 'sum'
+                    
+                    # Obtener el model_id para hr.efficiency
+                    model_record = self.env['ir.model'].search([('model', '=', 'hr.efficiency')], limit=1)
+                    
+                    if model_record:
+                        # Crear el campo de agregación
+                        self.env['ir.model.fields'].create({
+                            'name': agg_field_name,
+                            'model_id': model_record.id,
+                            'field_description': f'{indicator.name} (Agregación)',
+                            'ttype': 'float',
+                            'state': 'manual',
+                            'store': True,
+                            'help': f'Campo de agregación para {indicator.name}'
+                        })
+                        
+                        import logging
+                        _logger = logging.getLogger(__name__)
+                        _logger.info(f"Created aggregation field: {agg_field_name} for indicator: {indicator.name}")
+                    else:
+                        import logging
+                        _logger = logging.getLogger(__name__)
+                        _logger.warning(f"Model hr.efficiency not found for aggregation field: {agg_field_name}")
+                    
+                    import logging
+                    _logger = logging.getLogger(__name__)
+                    _logger.info(f"Created aggregation field: {agg_field_name} for indicator: {indicator.name}")
+                    
+        except Exception as e:
+            import logging
+            _logger = logging.getLogger(__name__)
+            _logger.warning(f"Error creating aggregation fields: {str(e)}")
+
     @api.model
     def _update_views_with_dynamic_fields(self):
         """
@@ -665,8 +960,8 @@ class HrEfficiency(models.Model):
         _logger = logging.getLogger(__name__)
         
         try:
-            # Get active indicators ordered by weight (descending)
-            active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='weight desc')
+            # Get active indicators ordered by sequence (consistent with other parts)
+            active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
             
             fields_to_display = []
             
@@ -704,10 +999,23 @@ class HrEfficiency(models.Model):
             # Build dynamic fields XML for list view
             dynamic_fields_xml = ''
             for field_info in fields_to_display:
-                field_xml = f'<field name="{field_info["name"]}" widget="percentage" optional="hide"'
+                # Determine widget based on indicator type
+                indicator = field_info['indicator']
+                widget_map = {
+                    'percentage': 'percentage',  # Changed back to 'percentage' to show % symbol
+                    'hours': 'float_time',
+                    'currency': 'monetary',
+                    'number': 'float'
+                }
+                widget = widget_map.get(indicator.indicator_type, 'float') if indicator else 'float'
+                
+                field_xml = f'<field name="{field_info["name"]}" widget="{widget}" optional="show"'
+                
+                # Add field name as string (tooltip will be shown automatically from field description)
+                if indicator:
+                    field_xml += f' string="{indicator.name}"'
                 
                 # Add decorations based on indicator thresholds
-                indicator = field_info['indicator']
                 if indicator:
                     # Use standard efficiency ranges: >=0.9 green, >=0.8 yellow, >=0.7 orange, <0.7 red or zero
                     field_xml += f' decoration-success="{field_info["name"]} &gt;= 0.9"'
@@ -717,12 +1025,28 @@ class HrEfficiency(models.Model):
                 
                 field_xml += '/>'
                 dynamic_fields_xml += field_xml
+                
+                # Add aggregation field for this indicator
+                agg_field_name = f"{field_info['name']}_agg"
+                agg_type = 'avg' if indicator.indicator_type == 'percentage' else 'sum'
+                agg_label = f"Total {indicator.name}" if agg_type == 'sum' else f"Average {indicator.name}"
+                
+                agg_field_xml = f'<field name="{agg_field_name}" {agg_type}="{agg_label}" optional="hide" invisible="1"/>'
+                dynamic_fields_xml += agg_field_xml
             
             # Update inherited list view
             inherited_list_view = self.env.ref('hr_efficiency.view_hr_efficiency_list_inherited', raise_if_not_found=False)
             if inherited_list_view:
-                new_arch = f"""
+                if dynamic_fields_xml:
+                    new_arch = f"""
 <xpath expr=\"//field[@name='expected_hours_to_date']\" position=\"after\">{dynamic_fields_xml}</xpath>
+"""
+                else:
+                    # If no dynamic fields, remove any existing dynamic fields from the view
+                    new_arch = """
+<xpath expr=\"//field[@name='expected_hours_to_date']\" position=\"after\">
+    <!-- No dynamic fields to display -->
+</xpath>
 """
                 inherited_list_view.write({'arch': new_arch})
                 _logger.info(f"Updated inherited list view with {len(fields_to_display)} dynamic fields")
@@ -730,10 +1054,25 @@ class HrEfficiency(models.Model):
             # Build dynamic fields XML for form view
             form_dynamic_fields_xml = ''
             for field_info in fields_to_display:
-                field_xml = f'<field name="{field_info["name"]}" widget="badge"'
+                # Determine widget based on indicator type
+                indicator = field_info['indicator']
+                widget_map = {
+                    'percentage': 'badge',
+                    'hours': 'float_time',
+                    'currency': 'monetary',
+                    'number': 'float'
+                }
+                widget = widget_map.get(indicator.indicator_type, 'badge') if indicator else 'badge'
+                
+                field_xml = f'<field name="{field_info["name"]}" widget="{widget}"'
+                
+                # Add help text with indicator description (valid in form views)
+                if indicator and indicator.description:
+                    # Escape quotes in description for XML
+                    help_text = indicator.description.replace('"', '&quot;')
+                    field_xml += f' help="{help_text}"'
                 
                 # Add decorations based on indicator thresholds
-                indicator = field_info['indicator']
                 if indicator:
                     # Use standard efficiency ranges: >=0.9 green, >=0.8 yellow, >=0.7 orange, <0.7 red or zero
                     field_xml += f' decoration-success="{field_info["name"]} &gt;= 0.9"'
@@ -747,8 +1086,16 @@ class HrEfficiency(models.Model):
             # Update inherited form view
             inherited_form_view = self.env.ref('hr_efficiency.view_hr_efficiency_form_inherited', raise_if_not_found=False)
             if inherited_form_view:
-                new_form_arch = f"""
+                if form_dynamic_fields_xml:
+                    new_form_arch = f"""
 <xpath expr=\"//field[@name='overall_efficiency']\" position=\"before\">{form_dynamic_fields_xml}</xpath>
+"""
+                else:
+                    # If no dynamic fields, remove any existing dynamic fields from the view
+                    new_form_arch = """
+<xpath expr=\"//field[@name='overall_efficiency']\" position=\"before\">
+    <!-- No dynamic fields to display -->
+</xpath>
 """
                 inherited_form_view.write({'arch': new_form_arch})
                 _logger.info("Updated inherited form view with dynamic fields")
@@ -762,6 +1109,13 @@ class HrEfficiency(models.Model):
         Generate list of months between start_month and end_month (inclusive)
         """
         months = []
+        
+        # Convert date objects to datetime if needed
+        if isinstance(start_month, date):
+            start_month = start_month.strftime('%Y-%m')
+        if isinstance(end_month, date):
+            end_month = end_month.strftime('%Y-%m')
+            
         current = datetime.strptime(start_month, '%Y-%m')
         end = datetime.strptime(end_month, '%Y-%m')
         
@@ -901,3 +1255,32 @@ class HrEfficiency(models.Model):
         except Exception:
             # Fallback to basic weekday check if there's any error
             return check_date.weekday() < 5
+
+    @api.model
+    def _apply_default_values_to_existing_records(self):
+        """Apply default values to existing employee and company records"""
+        import logging
+        _logger = logging.getLogger(__name__)
+        
+        try:
+            # Update employees with utilization_rate = 100.0 if not set
+            employees_to_update = self.env['hr.employee'].search([
+                ('utilization_rate', '=', 0.0)
+            ])
+            if employees_to_update:
+                employees_to_update.write({'utilization_rate': 100.0})
+                _logger.info(f"Updated {len(employees_to_update)} employees with default utilization_rate = 100.0%")
+            
+            # Update companies with overhead = 40.0 if not set
+            companies_to_update = self.env['res.company'].search([
+                ('overhead', '=', 0.0)
+            ])
+            if companies_to_update:
+                companies_to_update.write({'overhead': 40.0})
+                _logger.info(f"Updated {len(companies_to_update)} companies with default overhead = 40.0%")
+                
+            _logger.info("Default values applied successfully to existing records")
+            
+        except Exception as e:
+            _logger.error(f"Error applying default values to existing records: {str(e)}")
+            # Don't raise the exception to avoid breaking the installation

+ 1 - 0
hr_efficiency/models/hr_efficiency_dynamic_field.py

@@ -26,6 +26,7 @@ class HrEfficiencyDynamicField(models.Model):
         ('float_time', 'Float Time'),
         ('number', 'Number'),
         ('text', 'Text'),
+        ('monetary', 'Monetary'),
     ], string='Widget', default='percentage', help='How to display the field')
     
     # Styling

+ 70 - 12
hr_efficiency/models/hr_efficiency_indicator.py

@@ -14,6 +14,15 @@ class HrEfficiencyIndicator(models.Model):
     sequence = fields.Integer('Sequence', default=10)
     active = fields.Boolean('Active', default=True)
     
+    # Indicator type for widget selection
+    indicator_type = fields.Selection([
+        ('percentage', 'Percentage'),
+        ('hours', 'Hours'),
+        ('currency', 'Currency'),
+        ('number', 'Number')
+    ], string='Indicator Type', default='percentage', required=True, 
+       help='Type of indicator that determines how it will be displayed in views')
+    
     # Formula configuration
     formula = fields.Text('Formula', required=True, help="""
     Use the following variables in your formula:
@@ -64,12 +73,18 @@ class HrEfficiencyIndicator(models.Model):
         result = super().write(vals)
         # Update dynamic field for this indicator
         for record in self:
-            self._update_dynamic_field(record)
+            self._update_dynamic_field(record, vals)
         return result
     
     def unlink(self):
         """Delete indicator and delete dynamic field"""
-        # Delete associated dynamic fields
+        # FIRST: Mark indicators as inactive to exclude them from views
+        self.write({'active': False})
+        
+        # SECOND: Update views to remove field references BEFORE deleting fields
+        self.env['hr.efficiency']._update_views_with_dynamic_fields()
+        
+        # THEN: Delete associated dynamic fields and manual fields
         for record in self:
             dynamic_field = self.env['hr.efficiency.dynamic.field'].search([
                 ('indicator_id', '=', record.id)
@@ -86,9 +101,8 @@ class HrEfficiencyIndicator(models.Model):
             ], limit=1)
             if imf:
                 imf.unlink()
+        
         result = super().unlink()
-        # Refresh views after removal
-        self.env['hr.efficiency']._update_views_with_dynamic_fields()
         return result
     
     def _create_dynamic_field(self, indicator):
@@ -108,6 +122,15 @@ class HrEfficiencyIndicator(models.Model):
             if not field_name[0].isalpha():
                 field_name = 'indicator_' + field_name
             
+            # Determine widget based on indicator type
+            widget_map = {
+                'percentage': 'percentage',
+                'hours': 'float_time',
+                'currency': 'monetary',
+                'number': 'float'
+            }
+            widget = widget_map.get(indicator.indicator_type, 'percentage')
+            
             self.env['hr.efficiency.dynamic.field'].create({
                 'name': field_name,
                 'label': indicator.name,
@@ -117,7 +140,7 @@ class HrEfficiencyIndicator(models.Model):
                 'show_in_list': True,
                 'show_in_form': True,
                 'show_in_search': False,
-                'widget': 'percentage',
+                'widget': widget,
                 'decoration_success': indicator.color_threshold_green,
                 'decoration_warning': indicator.color_threshold_yellow,
                 'decoration_danger': 0.0,
@@ -139,14 +162,20 @@ class HrEfficiencyIndicator(models.Model):
                 'model_id': model_rec.id,
                 'ttype': 'float',
                 'field_description': indicator.name,
+                'help': indicator.description or indicator.name,
                 'state': 'manual',
                 'store': True,
                 'compute': False,
             })
         else:
-            # Keep label in sync
+            # Keep label and help in sync
+            update_vals = {}
             if imf.field_description != indicator.name:
-                imf.write({'field_description': indicator.name})
+                update_vals['field_description'] = indicator.name
+            if imf.help != (indicator.description or indicator.name):
+                update_vals['help'] = indicator.description or indicator.name
+            if update_vals:
+                imf.write(update_vals)
 
         # Update views to include the new field
         self.env['hr.efficiency']._update_views_with_dynamic_fields()
@@ -156,16 +185,26 @@ class HrEfficiencyIndicator(models.Model):
         if records:
             records._compute_indicators()
     
-    def _update_dynamic_field(self, indicator):
+    def _update_dynamic_field(self, indicator, vals=None):
         """Update the dynamic field for the indicator"""
         dynamic_field = self.env['hr.efficiency.dynamic.field'].search([
             ('indicator_id', '=', indicator.id)
         ])
         
         if dynamic_field:
+            # Determine widget based on indicator type
+            widget_map = {
+                'percentage': 'percentage',
+                'hours': 'float_time',
+                'currency': 'monetary',
+                'number': 'float'
+            }
+            widget = widget_map.get(indicator.indicator_type, 'percentage')
+            
             dynamic_field.write({
                 'label': indicator.name,
                 'active': indicator.active,
+                'widget': widget,
                 'decoration_success': indicator.color_threshold_green,
                 'decoration_warning': indicator.color_threshold_yellow,
             })
@@ -176,10 +215,24 @@ class HrEfficiencyIndicator(models.Model):
             ('model', '=', efficiency_model),
             ('name', '=', technical_name),
         ], limit=1)
-        if imf and imf.field_description != indicator.name:
-            imf.write({'field_description': indicator.name})
-        # Rebuild views so only active indicators are shown
-        self.env['hr.efficiency']._update_views_with_dynamic_fields()
+        if imf:
+            update_vals = {}
+            if imf.field_description != indicator.name:
+                update_vals['field_description'] = indicator.name
+            if imf.help != (indicator.description or indicator.name):
+                update_vals['help'] = indicator.description or indicator.name
+            if update_vals:
+                imf.write(update_vals)
+        # Update views when active status, sequence, or bulk operations change
+        if 'active' in vals or 'sequence' in vals or len(self) > 1:
+            try:
+                self.env['hr.efficiency']._update_views_with_dynamic_fields()
+            except Exception as e:
+                # Log the error but don't let it rollback the transaction
+                import logging
+                _logger = logging.getLogger(__name__)
+                _logger.error(f"Error updating views after indicator change: {e}")
+                # Continue with the transaction even if view update fails
     
     def evaluate_formula(self, efficiency_data):
         """
@@ -194,7 +247,12 @@ class HrEfficiencyIndicator(models.Model):
                 'planned_non_billable_hours': efficiency_data.get('planned_non_billable_hours', 0),
                 'actual_billable_hours': efficiency_data.get('actual_billable_hours', 0),
                 'actual_non_billable_hours': efficiency_data.get('actual_non_billable_hours', 0),
+                'total_actual_hours': efficiency_data.get('total_actual_hours', 0),
                 'expected_hours_to_date': efficiency_data.get('expected_hours_to_date', 0),
+                'wage': efficiency_data.get('wage', 0),
+                'utilization_rate': efficiency_data.get('utilization_rate', 100.0),
+                'overhead': efficiency_data.get('overhead', 40.0),
+                'precio_por_hora': efficiency_data.get('precio_por_hora', 0),
             }
             
             # Add math functions for safety

+ 15 - 0
hr_efficiency/models/hr_employee.py

@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class HrEmployee(models.Model):
+    _inherit = 'hr.employee'
+
+    utilization_rate = fields.Float(
+        'Utilization Rate (%)', 
+        default=100.0,
+        help="Se refiere al porcentaje de tiempo que un empleado dedica a trabajo facturable o productivo directo",
+        groups="hr.group_hr_user"
+    )

+ 15 - 0
hr_efficiency/models/res_company.py

@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class ResCompany(models.Model):
+    _inherit = 'res.company'
+
+    overhead = fields.Float(
+        'Overhead (%)', 
+        default=40.0,
+        help="Porcentaje de overhead de la compañía",
+        groups="base.group_system"
+    )

+ 7 - 0
hr_efficiency/views/hr_efficiency_indicator_views.xml

@@ -8,6 +8,7 @@
             <list string="Efficiency Indicators">
                 <field name="sequence" widget="handle"/>
                 <field name="name"/>
+                <field name="indicator_type"/>
                 <field name="formula"/>
                 <field name="target_percentage"/>
                 <field name="weight"/>
@@ -29,6 +30,7 @@
                         <group>
                             <field name="name"/>
                             <field name="sequence"/>
+                            <field name="indicator_type"/>
                             <field name="active"/>
                         </group>
                         <group>
@@ -56,8 +58,13 @@
         <field name="arch" type="xml">
             <search string="Search Efficiency Indicators">
                 <field name="name"/>
+                <field name="indicator_type"/>
                 <filter string="Active" name="active" domain="[('active', '=', True)]"/>
                 <filter string="Inactive" name="inactive" domain="[('active', '=', False)]"/>
+                <filter string="Percentage" name="percentage" domain="[('indicator_type', '=', 'percentage')]"/>
+                <filter string="Hours" name="hours" domain="[('indicator_type', '=', 'hours')]"/>
+                <filter string="Currency" name="currency" domain="[('indicator_type', '=', 'currency')]"/>
+                <filter string="Number" name="number" domain="[('indicator_type', '=', 'number')]"/>
             </search>
         </field>
     </record>

+ 19 - 11
hr_efficiency/views/hr_efficiency_views.xml

@@ -9,16 +9,20 @@
                 <list string="Employee Efficiency">
                     <field name="month_year"/>
                     <field name="employee_id"/>
-                    <field name="company_id" groups="base.group_multi_company"/>
-                    <field name="calculation_date"/>
-                    <field name="available_hours" sum="Total Available"/>
-                    <field name="planned_hours" sum="Total Planned"/>
-                    <field name="planned_billable_hours" sum="Total Planned Billable"/>
-                    <field name="planned_non_billable_hours" sum="Total Planned Non-Billable"/>
-                    <field name="actual_billable_hours" sum="Total Actual Billable"/>
-                    <field name="actual_non_billable_hours" sum="Total Actual Non-Billable"/>
-                    <field name="total_actual_hours" sum="Total Actual"/>
-                    <field name="expected_hours_to_date" sum="Total Expected"/>
+                    <field name="wage" sum="Total Gross Salary" optional="show"/>
+                    <field name="utilization_rate" avg="Average Utilization" optional="show"/>
+                    <field name="overhead" avg="Average Overhead" optional="show"/>
+                    <field name="precio_por_hora" avg="Average Precio por Hora" optional="show"/>
+                    <field name="company_id" groups="base.group_multi_company" optional="hide"/>
+                    <field name="calculation_date" optional="hide"/>
+                    <field name="available_hours" sum="Total Available" optional="show"/>
+                    <field name="planned_hours" sum="Total Planned" optional="show"/>
+                    <field name="planned_billable_hours" sum="Total Planned Billable" optional="hide"/>
+                    <field name="planned_non_billable_hours" sum="Total Planned Non-Billable" optional="hide"/>
+                    <field name="actual_billable_hours" sum="Total Actual Billable" optional="show"/>
+                    <field name="actual_non_billable_hours" sum="Total Actual Non-Billable" optional="show"/>
+                    <field name="total_actual_hours" sum="Total Actual" optional="show"/>
+                    <field name="expected_hours_to_date" sum="Total Expected" optional="hide"/>
                     <field name="overall_efficiency_display" widget="badge"
                            decoration-danger="overall_efficiency_display == '0.00' or overall_efficiency &lt; 80"
                            decoration-success="overall_efficiency >= 90"
@@ -43,6 +47,10 @@
                             <group>
                                 <field name="month_year"/>
                                 <field name="employee_id"/>
+                                <field name="wage"/>
+                                <field name="utilization_rate"/>
+                                <field name="overhead"/>
+                                <field name="precio_por_hora"/>
                                 <field name="company_id" groups="base.group_multi_company"/>
                                 <field name="calculation_date"/>
                             </group>
@@ -122,7 +130,7 @@
             <field name="name">Employee Efficiency</field>
             <field name="res_model">hr.efficiency</field>
             <field name="view_mode">list,form</field>
-            <field name="view_id" ref="view_hr_efficiency_list_inherited"/>
+            <field name="view_id" ref="view_hr_efficiency_tree"/>
             <field name="search_view_id" ref="view_hr_efficiency_search"/>
             <field name="context">{'search_default_filter_date': 1}</field>
             <field name="help" type="html">

+ 20 - 0
hr_efficiency/views/hr_employee_views.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Inherit employee form to add utilization rate field after hourly_cost -->
+        <record id="view_employee_form_utilization_rate" model="ir.ui.view">
+            <field name="name">hr.employee.form.utilization.rate</field>
+            <field name="model">hr.employee</field>
+            <field name="inherit_id" ref="hr_hourly_cost.view_employee_form"/>
+            <field name="priority" eval="50"/>
+            <field name="arch" type="xml">
+                <group name="application_group" position="inside">
+                    <label for="utilization_rate"/>
+                    <div name="utilization_rate">
+                        <field name="utilization_rate" class="oe_inline"/>
+                    </div>
+                </group>
+            </field>
+        </record>
+    </data>
+</odoo>

+ 17 - 0
hr_efficiency/views/res_company_views.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Inherit company form to add overhead field after currency_id -->
+        <record id="view_company_form_overhead" model="ir.ui.view">
+            <field name="name">res.company.form.overhead</field>
+            <field name="model">res.company</field>
+            <field name="inherit_id" ref="base.view_company_form"/>
+            <field name="priority" eval="50"/>
+            <field name="arch" type="xml">
+                <field name="currency_id" position="after">
+                    <field name="overhead" groups="base.group_system"/>
+                </field>
+            </field>
+        </record>
+    </data>
+</odoo>