Browse Source

Merge commit 'c4020d66a3f3eea08746325571e0d51ab15d5888' as 'hr_efficiency'

Roberto pineda 6 months ago
parent
commit
cbe7b89aef

+ 292 - 0
hr_efficiency/README.md

@@ -0,0 +1,292 @@
+# HR Efficiency Module
+
+## Descripción
+
+El módulo `hr_efficiency` permite llevar un registro de la eficiencia de los empleados comparando las horas planeadas vs las horas realmente trabajadas. Proporciona análisis mensuales detallados que incluyen:
+
+- **Horas disponibles** (considerando vacaciones y tiempo libre)
+- **Horas planeadas** (desde el módulo de planeación)
+- **Horas planeadas en proyectos facturables**
+- **Horas registradas en proyectos facturables**
+- **Horas planeadas en proyectos no facturables**
+- **Horas registradas en proyectos no facturables**
+
+## Características
+
+### 🔄 **Cálculo Automático**
+- Ejecución semanal automática para los últimos 3 meses y próximos 6 meses
+- Cron job configurado para ejecutarse cada semana
+- Actualización automática de registros existentes
+
+### 🛠️ **Cálculo Manual**
+- Wizard para ejecución manual del cálculo
+- Selección de período personalizado
+- Filtrado por empleados específicos
+- Resultados detallados del proceso
+
+### 📊 **Reportes y Análisis**
+- Vista de lista con indicadores visuales de eficiencia
+- Reportes pivot y gráficos para análisis
+- **Indicadores Configurables**: Sistema flexible de indicadores con fórmulas y ponderaciones
+- Filtros por indicadores (Good Planning, Good Time Tracking, High Overall Efficiency)
+- Filtros por tipo de proyecto (facturable, no facturable)
+- Filtros de meses dinámicos (últimos 2, actual, próximos 2)
+- Agrupación por empleado, mes, departamento
+
+### 📈 **Seguimiento Histórico Inteligente**
+- Nuevos registros se crean **solo cuando hay cambios** en los datos
+- Registros anteriores se archivan automáticamente (campo `active = False`)
+- Campo `calculation_date` para rastrear cuándo se realizó cada cálculo
+- Permite ver la evolución de la eficiencia a lo largo del tiempo
+- Solo los registros más recientes están activos para evitar duplicados
+
+### 🔗 **Integración Completa**
+- **Planning**: Extrae horas planeadas de `planning.slot`
+- **Timesheets**: Obtiene horas reales de `account.analytic.line`
+- **Projects**: Distingue entre proyectos facturables y no facturables
+- **Holidays**: Considera vacaciones y tiempo libre en el cálculo de disponibilidad
+
+### ⚙️ **Indicadores Configurables**
+- **Indicador 1 - Planeación**: Al menos 90% del tiempo disponible debe estar planeado
+- **Indicador 2 - Tiempo Registrado**: Al menos 90% del tiempo planeado debe estar registrado
+- **Fórmulas Personalizables**: Cada indicador puede usar fórmulas con variables de eficiencia
+- **Ponderaciones**: Cada indicador tiene un peso en el cálculo de eficiencia general
+- **Umbrales de Color**: Configuración de colores para diferentes niveles de rendimiento
+- **Gestión desde Planning**: Configuración disponible en Planning → Settings → Efficiency Indicators
+
+## Instalación
+
+1. Copiar el módulo a `/extra-addons/custom/hr_efficiency`
+2. Actualizar la lista de módulos en Odoo
+3. Instalar el módulo "HR Efficiency"
+
+## Uso
+
+### Acceso al Módulo
+
+El módulo se puede acceder desde:
+- **Menú HR**: Employees → Efficiency
+- **Menú Planning**: Planning → Reporting → Efficiency
+
+### Cálculo Manual
+
+1. Ir a **Employees → Efficiency → Calculate Efficiency**
+2. Configurar el período (formato: YYYY-MM)
+3. Seleccionar empleados (opcional, por defecto todos)
+4. Ejecutar el cálculo
+5. Revisar resultados y acceder a los registros
+
+### Configuración de Indicadores
+
+#### Acceso a la Configuración
+1. Ir a **Planning → Settings → Efficiency Indicators**
+2. Aquí puedes gestionar todos los indicadores de eficiencia
+
+#### Indicadores por Defecto
+- **Planning Efficiency**: `(planned_hours / available_hours) * 100`
+  - Objetivo: 90%
+  - Peso: 50%
+  - Mide qué tan bien se planea el tiempo disponible
+
+- **Time Tracking Efficiency**: `((actual_billable_hours + actual_non_billable_hours) / planned_hours) * 100`
+  - Objetivo: 90%
+  - Peso: 50%
+  - Mide qué tan bien se registra el tiempo planeado
+
+#### Variables Disponibles en Fórmulas
+- `available_hours`: Horas disponibles
+- `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 registradas en proyectos facturables
+- `actual_non_billable_hours`: Horas registradas en proyectos no facturables
+
+### Visualización de Datos
+
+#### Vista de Lista
+- Muestra todos los registros de eficiencia
+- Indicadores visuales:
+  - 🟢 Verde: Eficiencia ≥ 90%
+  - 🟡 Amarillo: Eficiencia 70-89%
+  - 🔴 Rojo: Eficiencia < 70%
+
+#### Filtros Disponibles
+- **Filtros de Indicadores**:
+  - Good Planning (≥ 90%)
+  - Good Time Tracking (≥ 90%)
+  - High Overall Efficiency (≥ 90%)
+
+- **Filtros de Tipo de Proyecto**:
+  - Proyectos Facturables
+  - Proyectos No Facturables
+
+- **Filtros de Fecha** (similar a ventas):
+  - **Meses**: abril, mayo, junio, julio, agosto (por defecto)
+  - **Trimestres**: Trimestre 1, 2, 3, 4
+  - **Años**: 2023, 2024, 2025
+
+- **Filtros de Estado**:
+  - Archivado
+  - No Archivado
+
+#### Reportes
+- **Efficiency Analysis**: Reportes pivot y gráficos
+- Agrupación por mes, empleado, departamento
+- Métricas de eficiencia y utilización
+
+## Estructura del Módulo
+
+```
+hr_efficiency/
+├── __init__.py
+├── __manifest__.py
+├── models/
+│   ├── __init__.py
+│   └── hr_efficiency.py
+├── wizard/
+│   ├── __init__.py
+│   ├── hr_efficiency_calculation_wizard.py
+│   └── hr_efficiency_calculation_wizard_views.xml
+├── views/
+│   ├── hr_efficiency_views.xml
+│   └── planning_views.xml
+├── report/
+│   ├── __init__.py
+│   ├── hr_efficiency_report.py
+│   └── hr_efficiency_report_views.xml
+├── security/
+│   └── ir.model.access.csv
+├── data/
+│   └── hr_efficiency_cron.xml
+└── README.md
+```
+
+## Modelos
+
+### hr.efficiency
+Modelo principal que almacena los registros de eficiencia mensual por empleado.
+
+**Campos principales:**
+- `month_year`: Mes y año (formato: YYYY-MM)
+- `employee_id`: Empleado
+- `calculation_date`: Fecha y hora cuando se realizó el cálculo (para seguimiento histórico)
+- `available_hours`: Horas disponibles
+- `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
+- `efficiency_rate`: Porcentaje de eficiencia general
+- `billable_efficiency_rate`: Porcentaje de eficiencia en proyectos facturables
+
+### hr.efficiency.calculation.wizard
+Wizard para ejecutar cálculos manuales de eficiencia.
+
+**Comportamiento de Registros:**
+- **Creación inteligente**: Nuevos registros se crean solo cuando hay cambios en los datos
+- **Archivo automático**: Registros anteriores se archivan (campo `active = False`) antes de crear nuevos
+- **Seguimiento histórico**: Permite ver cómo evolucionan los datos de eficiencia a lo largo del tiempo
+- **Sin duplicados activos**: Solo el registro más reciente está activo para cada empleado y mes
+- **Timestamp de cálculo**: Cada registro incluye la fecha y hora exacta cuando se realizó el cálculo
+Wizard para cálculo manual de eficiencia.
+
+### hr.efficiency.report
+Modelo de reporte para análisis y visualización de datos.
+
+## Cálculos
+
+### Horas Disponibles
+```python
+# Horas totales del calendario del empleado
+total_work_hours = employee._list_work_time_per_day(start_date, end_date)
+
+# Restar horas de vacaciones aprobadas
+time_off_hours = get_approved_leaves_hours(employee, start_date, end_date)
+
+available_hours = total_work_hours - time_off_hours
+```
+
+### Horas Planeadas
+```python
+# Obtener slots de planning del empleado
+planning_slots = env['planning.slot'].search([
+    ('employee_id', '=', employee.id),
+    ('start_datetime', '>=', start_datetime),
+    ('end_datetime', '<=', end_datetime),
+    ('state', 'in', ['draft', 'published'])
+])
+
+# Separar por tipo de proyecto
+for slot in planning_slots:
+    if slot.project_id.allow_billable:
+        planned_billable += slot.allocated_hours
+    else:
+        planned_non_billable += slot.allocated_hours
+```
+
+### Horas Reales
+```python
+# Obtener timesheets del empleado (excluyendo vacaciones)
+timesheets = env['account.analytic.line'].search([
+    ('employee_id', '=', employee.id),
+    ('date', '>=', start_date),
+    ('date', '<=', end_date),
+    ('project_id', '!=', False),
+    ('holiday_id', '=', False)  # Excluir timesheets de vacaciones
+])
+
+# Separar por tipo de proyecto
+for timesheet in timesheets:
+    if timesheet.project_id.allow_billable:
+        actual_billable += timesheet.unit_amount
+    else:
+        actual_non_billable += timesheet.unit_amount
+```
+
+## Configuración
+
+### Cron Job
+El módulo incluye un cron job configurado para ejecutarse semanalmente:
+- **Nombre**: HR Efficiency: Calculate Efficiency (Weekly)
+- **Frecuencia**: Semanal
+- **Prioridad**: 10
+- **Método**: `_cron_calculate_efficiency()`
+
+### Permisos
+- **Usuarios HR**: Lectura, escritura y creación de registros
+- **Managers HR**: Acceso completo incluyendo eliminación
+- **Reportes**: Solo lectura para análisis
+
+## Dependencias
+
+- `hr`: Gestión de empleados
+- `hr_timesheet`: Timesheets
+- `hr_holidays`: Gestión de vacaciones
+- `project`: Gestión de proyectos
+- `planning`: Planeación de recursos
+- `project_timesheet_holidays`: Integración de vacaciones con timesheets
+
+## Notas Técnicas
+
+### Consideraciones de Rendimiento
+- El cálculo se ejecuta por empleado y mes para optimizar el rendimiento
+- Los registros existentes se actualizan en lugar de crear duplicados
+- El cron job está configurado para ejecutarse en horarios de baja actividad
+
+### Integración con Vacaciones
+- El módulo considera automáticamente las vacaciones aprobadas
+- Excluye timesheets generados automáticamente por vacaciones
+- Calcula la disponibilidad real considerando el calendario del empleado
+
+### Proyectos Facturables
+- Utiliza el campo `allow_billable` de `project.project`
+- Distingue entre proyectos facturables y no facturables
+- Permite análisis separado de eficiencia por tipo de proyecto
+
+## Soporte
+
+Para reportar problemas o solicitar mejoras, contactar al equipo de desarrollo.
+
+## Licencia
+
+LGPL-3

+ 6 - 0
hr_efficiency/__init__.py

@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
+from . import wizard
+from . import report

+ 57 - 0
hr_efficiency/__manifest__.py

@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+    'name': 'HR Efficiency',
+    'version': '1.0',
+    'category': 'Human Resources',
+    'summary': 'Employee Efficiency Tracking',
+    'description': """
+Employee Efficiency Tracking
+============================
+
+This module tracks employee efficiency by comparing planned hours vs actual hours worked.
+It provides monthly reports showing:
+- Available hours (considering holidays and time off)
+- Planned hours (from planning module)
+- Planned hours on billable projects
+- Actual hours worked on billable projects
+- Planned hours on non-billable projects
+- Actual hours worked on non-billable projects
+
+Features:
+- Automatic weekly calculation for last 3 months and next 6 months
+- Manual calculation wizard
+- Integration with Planning, Timesheets, Projects and Holidays
+- Efficiency reports in Planning application
+    """,
+    'depends': [
+        'hr',
+        'hr_timesheet',
+        'hr_holidays',
+        'project',
+        'planning',
+        'project_timesheet_holidays',
+    ],
+                                        'data': [
+        'security/ir.model.access.csv',
+        'data/hr_efficiency_cron.xml',
+        'data/hr_efficiency_filters.xml',
+        'data/hr_efficiency_indicators.xml',
+        'data/hr_efficiency_init.xml',
+        'data/hr_efficiency_post_init.xml',
+        'wizard/hr_efficiency_calculation_wizard_views.xml',
+        'wizard/hr_efficiency_field_visibility_wizard_views.xml',
+        'views/hr_efficiency_views.xml',
+        'views/hr_efficiency_indicator_views.xml',
+        'views/hr_efficiency_dynamic_field_views.xml',
+        'views/hr_efficiency_dynamic_views.xml',
+        'views/planning_views.xml',
+        'report/hr_efficiency_report_views.xml',
+    ],
+    'demo': [],
+    'installable': True,
+    'auto_install': False,
+    'application': False,
+    'license': 'LGPL-3',
+}

+ 16 - 0
hr_efficiency/data/hr_efficiency_cron.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data noupdate="1">
+        <record id="ir_cron_hr_efficiency_calculation" model="ir.cron">
+            <field name="name">HR Efficiency: Calculate Efficiency (Weekly)</field>
+            <field name="model_id" ref="model_hr_efficiency"/>
+            <field name="state">code</field>
+            <field name="code">model._cron_calculate_efficiency()</field>
+            <field name="interval_number">1</field>
+            <field name="interval_type">weeks</field>
+            <field name="active" eval="True"/>
+            <field name="priority">10</field>
+            <field name="nextcall" eval="(DateTime.now() + timedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S')"/>
+        </record>
+    </data>
+</odoo>

+ 97 - 0
hr_efficiency/data/hr_efficiency_filters.xml

@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data noupdate="1">
+        <!-- Filtros dinámicos para la vista de búsqueda -->
+        
+        <!-- Filtro para proyectos facturables -->
+        <record id="filter_billable_projects" model="ir.filters">
+            <field name="name">Proyectos Facturables</field>
+            <field name="model_id" ref="model_hr_efficiency"/>
+            <field name="domain">[('planned_billable_hours', '>', 0)]</field>
+            <field name="context">{}</field>
+            <field name="is_default" eval="False"/>
+            <field name="active" eval="True"/>
+        </record>
+        
+        <!-- Filtro para proyectos no facturables -->
+        <record id="filter_non_billable_projects" model="ir.filters">
+            <field name="name">Proyectos No Facturables</field>
+            <field name="model_id" ref="model_hr_efficiency"/>
+            <field name="domain">[('planned_non_billable_hours', '>', 0)]</field>
+            <field name="context">{}</field>
+            <field name="is_default" eval="False"/>
+            <field name="active" eval="True"/>
+        </record>
+        
+        <!-- Filtro para mes actual (por defecto) -->
+        <record id="filter_current_month" model="ir.filters">
+            <field name="name">Mes Actual</field>
+            <field name="model_id" ref="model_hr_efficiency"/>
+            <field name="domain">[('month_year', '=', '2024-08')]</field>
+            <field name="context">{}</field>
+            <field name="is_default" eval="True"/>
+            <field name="active" eval="True"/>
+        </record>
+        
+        <!-- Filtro para últimos 2 meses -->
+        <record id="filter_last_2_months" model="ir.filters">
+            <field name="name">Últimos 2 Meses</field>
+            <field name="model_id" ref="model_hr_efficiency"/>
+            <field name="domain">[('month_year', 'in', ['2024-06', '2024-07'])]</field>
+            <field name="context">{}</field>
+            <field name="is_default" eval="False"/>
+            <field name="active" eval="True"/>
+        </record>
+        
+        <!-- Filtro para próximos 2 meses -->
+        <record id="filter_next_2_months" model="ir.filters">
+            <field name="name">Próximos 2 Meses</field>
+            <field name="model_id" ref="model_hr_efficiency"/>
+            <field name="domain">[('month_year', 'in', ['2024-09', '2024-10'])]</field>
+            <field name="context">{}</field>
+            <field name="is_default" eval="False"/>
+            <field name="active" eval="True"/>
+        </record>
+        
+        <!-- Filtro para junio 2024 -->
+        <record id="filter_june_2024" model="ir.filters">
+            <field name="name">Junio 2024</field>
+            <field name="model_id" ref="model_hr_efficiency"/>
+            <field name="domain">[('month_year', '=', '2024-06')]</field>
+            <field name="context">{}</field>
+            <field name="is_default" eval="False"/>
+            <field name="active" eval="True"/>
+        </record>
+        
+        <!-- Filtro para julio 2024 -->
+        <record id="filter_july_2024" model="ir.filters">
+            <field name="name">Julio 2024</field>
+            <field name="model_id" ref="model_hr_efficiency"/>
+            <field name="domain">[('month_year', '=', '2024-07')]</field>
+            <field name="context">{}</field>
+            <field name="is_default" eval="False"/>
+            <field name="active" eval="True"/>
+        </record>
+        
+        <!-- Filtro para septiembre 2024 -->
+        <record id="filter_september_2024" model="ir.filters">
+            <field name="name">Septiembre 2024</field>
+            <field name="model_id" ref="model_hr_efficiency"/>
+            <field name="domain">[('month_year', '=', '2024-09')]</field>
+            <field name="context">{}</field>
+            <field name="is_default" eval="False"/>
+            <field name="active" eval="True"/>
+        </record>
+        
+        <!-- Filtro para octubre 2024 -->
+        <record id="filter_october_2024" model="ir.filters">
+            <field name="name">Octubre 2024</field>
+            <field name="model_id" ref="model_hr_efficiency"/>
+            <field name="domain">[('month_year', '=', '2024-10')]</field>
+            <field name="context">{}</field>
+            <field name="is_default" eval="False"/>
+            <field name="active" eval="True"/>
+        </record>
+        
+    </data>
+</odoo>

+ 30 - 0
hr_efficiency/data/hr_efficiency_indicators.xml

@@ -0,0 +1,30 @@
+<?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>
+            <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>
+        </record>
+
+        <!-- Default Time Tracking Indicator -->
+        <record id="indicator_time_tracking" model="hr.efficiency.indicator">
+            <field name="name">Time Tracking Efficiency</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>
+        </record>
+    </data>
+</odoo>

+ 7 - 0
hr_efficiency/data/hr_efficiency_init.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data noupdate="1">
+        <!-- Initialize dynamic fields and views -->
+        <function model="hr.efficiency" name="_init_dynamic_system"/>
+    </data>
+</odoo>

+ 9 - 0
hr_efficiency/data/hr_efficiency_post_init.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data noupdate="1">
+        <!-- Post-installation: Ensure dynamic fields are created and views are updated -->
+        <function model="hr.efficiency" name="_init_dynamic_system"/>
+        <!-- Update views with dynamic fields -->
+        <function model="hr.efficiency" name="_update_views_with_dynamic_fields"/>
+    </data>
+</odoo>

+ 6 - 0
hr_efficiency/models/__init__.py

@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import hr_efficiency
+from . import hr_efficiency_indicator
+from . import hr_efficiency_dynamic_field

+ 559 - 0
hr_efficiency/models/hr_efficiency.py

@@ -0,0 +1,559 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime, date
+from dateutil.relativedelta import relativedelta
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+from odoo.tools import float_round
+
+
+class HrEfficiency(models.Model):
+    _name = 'hr.efficiency'
+    _description = 'Employee Efficiency'
+    _order = 'month_year desc, employee_id'
+    _rec_name = 'display_name'
+    _active_name = 'active'
+
+    active = fields.Boolean('Active', default=True, help='Technical field to archive old records')
+    month_year = fields.Char('Month Year', required=True, index=True, help="Format: YYYY-MM")
+    employee_id = fields.Many2one('hr.employee', 'Employee', required=True, index=True)
+    company_id = fields.Many2one('res.company', 'Company', required=True, default=lambda self: self.env.company)
+    calculation_date = fields.Datetime('Calculation Date', default=fields.Datetime.now, help='When this calculation was performed')
+    
+    # Available hours (considering holidays and time off)
+    available_hours = fields.Float('Available Hours', digits=(10, 2), help="Total available hours considering holidays and time off")
+    
+    # Planned hours
+    planned_hours = fields.Float('Planned Hours', digits=(10, 2), help="Total hours planned in planning module")
+    planned_billable_hours = fields.Float('Planned Billable Hours', digits=(10, 2), help="Hours planned on billable projects")
+    planned_non_billable_hours = fields.Float('Planned Non-Billable Hours', digits=(10, 2), help="Hours planned on non-billable projects")
+    
+    # Actual hours
+    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)
+    
+    # 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')
+    
+    display_name = fields.Char('Display Name', compute='_compute_display_name', store=True)
+    
+    # Note: Removed unique constraint to allow historical tracking
+    # Multiple records can exist for the same employee and month
+
+    @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', 'planned_hours', 'total_actual_hours')
+    def _compute_indicators(self):
+        for record in self:
+            # Get all manual fields for this model
+            manual_fields = self.env['ir.model.fields'].search([
+                ('model', '=', 'hr.efficiency'),
+                ('state', '=', 'manual'),
+                ('ttype', '=', 'float')
+            ])
+            
+            # Prepare efficiency data
+            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,
+            }
+            
+            # 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)
+                
+                if indicator:
+                    # Calculate indicator value using the indicator formula
+                    indicator_value = indicator.evaluate_formula(efficiency_data)
+                    
+                    # Set the value on the record
+                    if hasattr(record, field.name):
+                        setattr(record, field.name, float_round(indicator_value, 2))
+            
+            # Overall efficiency based on configured indicators
+            record.overall_efficiency = self._calculate_overall_efficiency(record)
+    
+    def _get_indicator_field_name(self, indicator_name):
+        """
+        Convert indicator name to valid field name
+        """
+        # 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')
+        
+        # Ensure it starts with x_ for manual fields
+        if not field_name.startswith('x_'):
+            field_name = 'x_' + field_name
+            
+        return field_name
+    
+
+    
+    @api.model
+    def _create_default_dynamic_fields(self):
+        """
+        Create default dynamic field records for existing indicators
+        """
+        # Get all active indicators
+        indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
+        
+        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
+            
+            existing_field = self.env['ir.model.fields'].search([
+                ('model', '=', 'hr.efficiency'),
+                ('name', '=', field_name)
+            ], limit=1)
+            
+            if not existing_field:
+                # Create field in ir.model.fields (like Studio does)
+                self.env['ir.model.fields'].create({
+                    'name': field_name,
+                    'model': 'hr.efficiency',
+                    'model_id': self.env['ir.model'].search([('model', '=', 'hr.efficiency')], limit=1).id,
+                    'ttype': 'float',
+                    'field_description': indicator.name,
+                    'state': 'manual',  # This is the key - it makes it a custom field
+                    'store': True,
+                    'compute': '_compute_indicators',
+                })
+
+
+    
+    def _calculate_overall_efficiency(self, record):
+        """
+        Calculate overall efficiency based on configured indicators
+        """
+        indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
+        
+        if not indicators:
+            # Default calculation if no indicators configured
+            return 0.0
+        
+        total_weight = 0
+        weighted_sum = 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,
+        }
+        
+        for indicator in indicators:
+            if indicator.weight > 0:
+                indicator_value = indicator.evaluate_formula(efficiency_data)
+                weighted_sum += indicator_value * indicator.weight
+                total_weight += indicator.weight
+        
+        if total_weight > 0:
+            return float_round(weighted_sum / total_weight, 2)
+        else:
+            return 0.0
+
+    @api.depends('employee_id', 'month_year')
+    def _compute_display_name(self):
+        for record in self:
+            if record.employee_id and record.month_year:
+                record.display_name = f"{record.employee_id.name} - {record.month_year}"
+            else:
+                record.display_name = "New Efficiency Record"
+
+    @api.model
+    def _calculate_employee_efficiency(self, employee, month_year):
+        """
+        Calculate efficiency for a specific employee and month
+        """
+        # Parse month_year (format: YYYY-MM)
+        try:
+            year, month = month_year.split('-')
+            start_date = date(int(year), int(month), 1)
+            end_date = (start_date + relativedelta(months=1)) - relativedelta(days=1)
+        except ValueError:
+            raise UserError(_("Invalid month_year format. Expected: YYYY-MM"))
+
+        # Calculate available hours (considering holidays and time off)
+        available_hours = self._calculate_available_hours(employee, start_date, end_date)
+        
+        # Calculate planned hours
+        planned_hours, planned_billable_hours, planned_non_billable_hours = self._calculate_planned_hours(employee, start_date, end_date)
+        
+        # Calculate actual hours
+        actual_billable_hours, actual_non_billable_hours = self._calculate_actual_hours(employee, start_date, end_date)
+        
+        return {
+            'month_year': month_year,
+            'employee_id': employee.id,
+            'company_id': employee.company_id.id,
+            'available_hours': available_hours,
+            '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_non_billable_hours': actual_non_billable_hours,
+        }
+
+    def _calculate_available_hours(self, employee, start_date, end_date):
+        """
+        Calculate available hours considering holidays and time off
+        """
+        if not employee.resource_calendar_id:
+            return 0.0
+        
+        # Convert dates to datetime for the method
+        start_datetime = datetime.combine(start_date, datetime.min.time())
+        end_datetime = datetime.combine(end_date, datetime.max.time())
+        
+        # Get working hours from calendar
+        work_hours_data = employee._list_work_time_per_day(start_datetime, end_datetime)
+        total_work_hours = sum(hours for _, hours in work_hours_data[employee.id])
+        
+        # Subtract hours from approved time off
+        time_off_hours = self._get_time_off_hours(employee, start_date, end_date)
+        
+        return max(0.0, total_work_hours - time_off_hours)
+
+    def _get_time_off_hours(self, employee, start_date, end_date):
+        """
+        Get hours from approved time off requests
+        """
+        # Get approved time off requests
+        leaves = self.env['hr.leave'].search([
+            ('employee_id', '=', employee.id),
+            ('state', '=', 'validate'),
+            ('date_from', '<=', end_date),
+            ('date_to', '>=', start_date),
+        ])
+        
+        total_hours = 0.0
+        for leave in leaves:
+            # Calculate overlap with the month
+            overlap_start = max(leave.date_from.date(), start_date)
+            overlap_end = min(leave.date_to.date(), end_date)
+            
+            if overlap_start <= overlap_end:
+                # Get hours for the overlap period
+                overlap_start_dt = datetime.combine(overlap_start, datetime.min.time())
+                overlap_end_dt = datetime.combine(overlap_end, datetime.max.time())
+                work_hours_data = employee._list_work_time_per_day(overlap_start_dt, overlap_end_dt)
+                total_hours += sum(hours for _, hours in work_hours_data[employee.id])
+        
+        return total_hours
+
+    def _calculate_planned_hours(self, employee, start_date, end_date):
+        """
+        Calculate planned hours from planning module
+        """
+        # Get planning slots for the employee in the date range
+        planning_slots = self.env['planning.slot'].search([
+            ('employee_id', '=', employee.id),
+            ('start_datetime', '>=', datetime.combine(start_date, datetime.min.time())),
+            ('end_datetime', '<=', datetime.combine(end_date, datetime.max.time())),
+            ('state', 'in', ['draft', 'published']),
+        ])
+        
+        total_planned = 0.0
+        total_billable = 0.0
+        total_non_billable = 0.0
+        
+        for slot in planning_slots:
+            hours = slot.allocated_hours or 0.0
+            
+            # Check if the slot is linked to a billable project
+            if slot.project_id and slot.project_id.allow_billable:
+                total_billable += hours
+            else:
+                total_non_billable += hours
+            
+            total_planned += hours
+        
+        return total_planned, total_billable, total_non_billable
+
+    def _calculate_actual_hours(self, employee, start_date, end_date):
+        """
+        Calculate actual hours from timesheets
+        """
+        # Get timesheets for the employee in the date range (excluding time off)
+        timesheets = self.env['account.analytic.line'].search([
+            ('employee_id', '=', employee.id),
+            ('date', '>=', start_date),
+            ('date', '<=', end_date),
+            ('project_id', '!=', False),  # Only project timesheets
+            ('holiday_id', '=', False),   # Exclude time off timesheets
+        ])
+        
+        total_billable = 0.0
+        total_non_billable = 0.0
+        
+        for timesheet in timesheets:
+            hours = timesheet.unit_amount or 0.0
+            
+            # Additional filter: exclude time off tasks
+            if timesheet.task_id and timesheet.task_id.name and 'time off' in timesheet.task_id.name.lower():
+                continue  # Skip time off tasks
+            
+            # Additional filter: exclude time off from name
+            if timesheet.name and 'tiempo personal' in timesheet.name.lower():
+                continue  # Skip personal time entries
+            
+            # Check if the project is billable
+            if timesheet.project_id and timesheet.project_id.allow_billable:
+                total_billable += hours
+            else:
+                total_non_billable += hours
+        
+        return total_billable, total_non_billable
+
+    @api.model
+    def calculate_efficiency_for_period(self, start_month=None, end_month=None):
+        """
+        Calculate efficiency for all employees for a given period
+        """
+        if not start_month:
+            # Default: last 3 months and next 6 months
+            current_date = date.today()
+            start_month = (current_date - relativedelta(months=3)).strftime('%Y-%m')
+            end_month = (current_date + relativedelta(months=6)).strftime('%Y-%m')
+        
+        # Generate list of months
+        months = self._generate_month_list(start_month, end_month)
+        
+        # Get all active employees
+        employees = self.env['hr.employee'].search([('active', '=', True)])
+        
+        created_records = []
+        
+        for employee in employees:
+            for month in months:
+                # Calculate efficiency data
+                efficiency_data = self._calculate_employee_efficiency(employee, month)
+                
+                # Check if there are changes compared to the latest record
+                latest_record = self.search([
+                    ('employee_id', '=', employee.id),
+                    ('month_year', '=', month),
+                    ('company_id', '=', employee.company_id.id),
+                ], order='calculation_date desc', limit=1)
+                
+                has_changes = False
+                if latest_record:
+                    # Compare current data with latest record
+                    fields_to_compare = [
+                        'available_hours', 'planned_hours', 'planned_billable_hours',
+                        'planned_non_billable_hours', 'actual_billable_hours',
+                        'actual_non_billable_hours'
+                    ]
+                    
+                    for field in fields_to_compare:
+                        if abs(efficiency_data[field] - latest_record[field]) > 0.01:  # Tolerance for floating point
+                            has_changes = True
+                            break
+                else:
+                    # No previous record exists, so this is a change
+                    has_changes = True
+                
+                # Only create new record if there are changes
+                if has_changes:
+                    # Archive existing records for this employee and month
+                    existing_records = self.search([
+                        ('employee_id', '=', employee.id),
+                        ('month_year', '=', month),
+                        ('company_id', '=', employee.company_id.id),
+                    ])
+                    if existing_records:
+                        existing_records.write({'active': False})
+                    
+                    # Create new record
+                    new_record = self.create(efficiency_data)
+                    created_records.append(new_record)
+        
+        return {
+            'created': len(created_records),
+            'updated': 0,  # No longer updating existing records
+            'total_processed': len(created_records),
+        }
+    
+    @api.model
+    def _init_dynamic_system(self):
+        """
+        Initialize dynamic fields and views when module is installed
+        """
+        import logging
+        _logger = logging.getLogger(__name__)
+        
+        try:
+            _logger.info("Starting dynamic system initialization...")
+            
+            # Step 1: Create dynamic field records for existing indicators
+            self._create_default_dynamic_fields()
+            _logger.info("Default dynamic fields created successfully")
+            
+            # Step 2: Dynamic fields are created via hr.efficiency.dynamic.field model
+            _logger.info("Dynamic fields creation handled by hr.efficiency.dynamic.field model")
+            
+            # Step 3: Update views
+            self._update_views_with_dynamic_fields()
+            _logger.info("Views updated successfully")
+            
+            # Step 4: Force recompute of existing records
+            records = self.search([])
+            if records:
+                records._invalidate_cache()
+                _logger.info(f"Invalidated cache for {len(records)} records")
+            
+            _logger.info("Dynamic system initialization completed successfully")
+            
+        except Exception as e:
+            _logger.error(f"Error during dynamic system initialization: {str(e)}")
+            raise
+    
+    @api.model
+    def _update_views_with_dynamic_fields(self):
+        """
+        Update inherited views to include dynamic fields after module is loaded
+        """
+        import logging
+        _logger = logging.getLogger(__name__)
+        
+        try:
+            # Get all manual fields for this model (like Studio does)
+            manual_fields = self.env['ir.model.fields'].search([
+                ('model', '=', 'hr.efficiency'),
+                ('state', '=', 'manual'),
+                ('ttype', '=', 'float')
+            ], order='id')
+            
+            _logger.info(f"Found {len(manual_fields)} manual fields to add to views")
+            
+            # Build dynamic fields XML for list view
+            dynamic_fields_xml = ''
+            for field in manual_fields:
+                field_xml = f'<field name="{field.name}" widget="percentage" optional="hide"'
+                
+                # Add decorations based on indicator thresholds
+                indicator = self.env['hr.efficiency.indicator'].search([
+                    ('name', 'ilike', field.field_description)
+                ], limit=1)
+                
+                if indicator:
+                    if indicator.color_threshold_green:
+                        field_xml += f' decoration-success="{field.name} &gt;= {indicator.color_threshold_green}"'
+                    if indicator.color_threshold_yellow:
+                        field_xml += f' decoration-warning="{field.name} &gt;= {indicator.color_threshold_yellow} and {field.name} &lt; {indicator.color_threshold_green}"'
+                    field_xml += f' decoration-danger="{field.name} &lt; {indicator.color_threshold_yellow}"'
+                
+                field_xml += '/>'
+                dynamic_fields_xml += 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:
+                arch = inherited_list_view.arch
+                comment = '<!-- Dynamic indicator fields will be added here -->'
+                if comment in arch:
+                    arch = arch.replace(comment, dynamic_fields_xml)
+                    inherited_list_view.write({'arch': arch})
+                    _logger.info(f"Updated inherited list view with {len(manual_fields)} dynamic fields")
+                else:
+                    _logger.warning("Comment not found in inherited list view")
+            
+            # Build dynamic fields XML for form view
+            form_dynamic_fields_xml = ''
+            for field in manual_fields:
+                field_xml = f'<field name="{field.name}" widget="percentage"/>'
+                form_dynamic_fields_xml += field_xml
+            
+            # 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:
+                arch = inherited_form_view.arch
+                comment = '<!-- Dynamic indicator fields will be added here -->'
+                if comment in arch:
+                    arch = arch.replace(comment, form_dynamic_fields_xml)
+                    inherited_form_view.write({'arch': arch})
+                    _logger.info(f"Updated inherited form view with dynamic fields")
+                else:
+                    _logger.warning("Comment not found in inherited form view")
+                    
+        except Exception as e:
+            _logger.error(f"Error updating views with dynamic fields: {str(e)}")
+            raise
+
+    def _generate_month_list(self, start_month, end_month):
+        """
+        Generate list of months between start_month and end_month (inclusive)
+        """
+        months = []
+        current = datetime.strptime(start_month, '%Y-%m')
+        end = datetime.strptime(end_month, '%Y-%m')
+        
+        while current <= end:
+            months.append(current.strftime('%Y-%m'))
+            current = current + relativedelta(months=1)
+        
+        return months
+
+    @api.model
+    def _cron_calculate_efficiency(self):
+        """
+        Cron job to automatically calculate efficiency
+        """
+        self.calculate_efficiency_for_period()
+
+    @api.model
+    def _get_month_filter_options(self):
+        """
+        Get dynamic month filter options for search view
+        Returns a list of tuples (month_year, display_name) for the last 2, current, and next 2 months
+        """
+        current_date = date.today()
+        months = []
+        
+        # Last 2 months
+        for i in range(2, 0, -1):
+            month_date = current_date - relativedelta(months=i)
+            month_year = month_date.strftime('%Y-%m')
+            month_name = month_date.strftime('%B %Y')  # e.g., "August 2024"
+            months.append((month_year, month_name))
+        
+        # Current month
+        current_month_year = current_date.strftime('%Y-%m')
+        current_month_name = current_date.strftime('%B %Y')
+        months.append((current_month_year, current_month_name))
+        
+        # Next 2 months
+        for i in range(1, 3):
+            month_date = current_date + relativedelta(months=i)
+            month_year = month_date.strftime('%Y-%m')
+            month_name = month_date.strftime('%B %Y')
+            months.append((month_year, month_name))
+        
+        return months

+ 145 - 0
hr_efficiency/models/hr_efficiency_dynamic_field.py

@@ -0,0 +1,145 @@
+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'),
+    ], 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

+ 165 - 0
hr_efficiency/models/hr_efficiency_indicator.py

@@ -0,0 +1,165 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import ValidationError
+
+
+class HrEfficiencyIndicator(models.Model):
+    _name = 'hr.efficiency.indicator'
+    _description = 'Efficiency Indicator Configuration'
+    _order = 'sequence, name'
+
+    name = fields.Char('Indicator Name', required=True)
+    sequence = fields.Integer('Sequence', default=10)
+    active = fields.Boolean('Active', default=True)
+    
+    # Formula configuration
+    formula = fields.Text('Formula', required=True, help="""
+    Use the following variables in your formula:
+    - available_hours: Available hours for the employee
+    - planned_hours: Total planned hours
+    - planned_billable_hours: Planned hours on billable projects
+    - planned_non_billable_hours: Planned hours on non-billable projects
+    - actual_billable_hours: Actual hours worked on billable projects
+    - actual_non_billable_hours: Actual hours worked on non-billable projects
+    
+    Examples:
+    - (planned_hours / available_hours) * 100  # Planning efficiency
+    - ((actual_billable_hours + actual_non_billable_hours) / planned_hours) * 100  # Time tracking efficiency
+    """)
+    
+    # Target and thresholds
+    target_percentage = fields.Float('Target %', default=90.0, help='Target percentage for this indicator')
+    weight = fields.Float('Weight %', default=50.0, help='Weight of this indicator in the overall efficiency calculation')
+    
+    # Display settings
+    description = fields.Text('Description', help='Description of what this indicator measures')
+    color_threshold_green = fields.Float('Green Threshold', default=90.0, help='Percentage above which to show green')
+    color_threshold_yellow = fields.Float('Yellow Threshold', default=70.0, help='Percentage above which to show yellow')
+    
+    @api.constrains('weight')
+    def _check_weight(self):
+        for record in self:
+            if record.weight < 0 or record.weight > 100:
+                raise ValidationError(_('Weight must be between 0 and 100'))
+    
+    @api.constrains('target_percentage')
+    def _check_target_percentage(self):
+        for record in self:
+            if record.target_percentage < 0 or record.target_percentage > 100:
+                raise ValidationError(_('Target percentage must be between 0 and 100'))
+    
+    @api.model_create_multi
+    def create(self, vals_list):
+        """Create indicators and create dynamic fields"""
+        records = super().create(vals_list)
+        # Create dynamic fields for all indicators
+        for record in records:
+            self._create_dynamic_field(record)
+        return records
+    
+    def write(self, vals):
+        """Update indicator and update dynamic field"""
+        result = super().write(vals)
+        # Update dynamic field for this indicator
+        for record in self:
+            self._update_dynamic_field(record)
+        return result
+    
+    def unlink(self):
+        """Delete indicator and delete dynamic field"""
+        # Delete associated dynamic fields
+        for record in self:
+            dynamic_field = self.env['hr.efficiency.dynamic.field'].search([
+                ('indicator_id', '=', record.id)
+            ])
+            if dynamic_field:
+                dynamic_field.unlink()
+        result = super().unlink()
+        return result
+    
+    def _create_dynamic_field(self, indicator):
+        """Create a dynamic field for the indicator"""
+        # Check if dynamic field already exists
+        existing_field = self.env['hr.efficiency.dynamic.field'].search([
+            ('indicator_id', '=', indicator.id)
+        ])
+        
+        if not existing_field:
+            # Create dynamic field
+            field_name = indicator.name.lower().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
+            
+            self.env['hr.efficiency.dynamic.field'].create({
+                'name': field_name,
+                'label': indicator.name,
+                'indicator_id': indicator.id,
+                'sequence': indicator.sequence,
+                'active': indicator.active,
+                'show_in_list': True,
+                'show_in_form': True,
+                'show_in_search': False,
+                'widget': 'percentage',
+                'decoration_success': indicator.color_threshold_green,
+                'decoration_warning': indicator.color_threshold_yellow,
+                'decoration_danger': 0.0,
+            })
+    
+    def _update_dynamic_field(self, indicator):
+        """Update the dynamic field for the indicator"""
+        dynamic_field = self.env['hr.efficiency.dynamic.field'].search([
+            ('indicator_id', '=', indicator.id)
+        ])
+        
+        if dynamic_field:
+            dynamic_field.write({
+                'label': indicator.name,
+                'active': indicator.active,
+                'decoration_success': indicator.color_threshold_green,
+                'decoration_warning': indicator.color_threshold_yellow,
+            })
+    
+    def evaluate_formula(self, efficiency_data):
+        """
+        Evaluate the formula using the provided efficiency data
+        """
+        try:
+            # Create a safe environment for formula evaluation
+            safe_dict = {
+                'available_hours': efficiency_data.get('available_hours', 0),
+                'planned_hours': efficiency_data.get('planned_hours', 0),
+                'planned_billable_hours': efficiency_data.get('planned_billable_hours', 0),
+                '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),
+            }
+            
+            # Add math functions for safety
+            import math
+            safe_dict.update({
+                'abs': abs,
+                'min': min,
+                'max': max,
+                'round': round,
+            })
+            
+            # Evaluate the formula
+            result = eval(self.formula, {"__builtins__": {}}, safe_dict)
+            return result
+        except Exception as e:
+            _logger.error(f"Error evaluating formula for indicator {self.name}: {e}")
+            return 0.0
+    
+    def get_color_class(self, percentage):
+        """
+        Return the CSS color class based on the percentage
+        """
+        if percentage >= self.color_threshold_green:
+            return 'text-success'
+        elif percentage >= self.color_threshold_yellow:
+            return 'text-warning'
+        else:
+            return 'text-danger'

+ 4 - 0
hr_efficiency/report/__init__.py

@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import hr_efficiency_report

+ 89 - 0
hr_efficiency/report/hr_efficiency_report.py

@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+from odoo.tools.sql import drop_view_if_exists, SQL
+
+
+class HrEfficiencyReport(models.Model):
+    _name = "hr.efficiency.report"
+    _description = "HR Efficiency Analysis Report"
+    _auto = False
+
+    month_year = fields.Char('Month Year', readonly=True)
+    employee_id = fields.Many2one('hr.employee', 'Employee', readonly=True)
+    company_id = fields.Many2one('res.company', 'Company', readonly=True)
+    department_id = fields.Many2one('hr.department', 'Department', readonly=True)
+    job_title = fields.Char('Job Title', readonly=True)
+    
+    # Hours
+    available_hours = fields.Float('Available Hours', readonly=True)
+    planned_hours = fields.Float('Planned Hours', readonly=True)
+    planned_billable_hours = fields.Float('Planned Billable Hours', readonly=True)
+    planned_non_billable_hours = fields.Float('Planned Non-Billable Hours', readonly=True)
+    actual_billable_hours = fields.Float('Actual Billable Hours', readonly=True)
+    actual_non_billable_hours = fields.Float('Actual Non-Billable Hours', readonly=True)
+    total_actual_hours = fields.Float('Total Actual Hours', readonly=True)
+    
+    # Efficiency rates
+    efficiency_rate = fields.Float('Efficiency Rate (%)', readonly=True)
+    billable_efficiency_rate = fields.Float('Billable Efficiency Rate (%)', readonly=True)
+    
+    # Computed fields for analysis
+    utilization_rate = fields.Float('Utilization Rate (%)', readonly=True, help="Percentage of available hours that were planned")
+    billable_utilization_rate = fields.Float('Billable Utilization Rate (%)', readonly=True, help="Percentage of available hours that were planned on billable projects")
+
+    @property
+    def _table_query(self):
+        return """
+            SELECT 
+                e.id,
+                e.month_year,
+                e.employee_id,
+                e.company_id,
+                emp.department_id,
+                emp.job_title,
+                e.available_hours,
+                e.planned_hours,
+                e.planned_billable_hours,
+                e.planned_non_billable_hours,
+                e.actual_billable_hours,
+                e.actual_non_billable_hours,
+                e.total_actual_hours,
+                e.efficiency_rate,
+                e.billable_efficiency_rate,
+                CASE 
+                    WHEN e.available_hours > 0 THEN (e.planned_hours / e.available_hours) * 100
+                    ELSE 0 
+                END AS utilization_rate,
+                CASE 
+                    WHEN e.available_hours > 0 THEN (e.planned_billable_hours / e.available_hours) * 100
+                    ELSE 0 
+                END AS billable_utilization_rate
+            FROM hr_efficiency e
+            LEFT JOIN hr_employee emp ON e.employee_id = emp.id
+        """
+
+    @api.model
+    def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
+        """
+        Override read_group to handle computed fields
+        """
+        res = super().read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
+        
+        # Recalculate computed fields for grouped data
+        for group in res:
+            if '__domain' in group:
+                records = self.search(group['__domain'])
+                if records:
+                    # Calculate averages for efficiency rates
+                    if 'efficiency_rate' in fields:
+                        group['efficiency_rate'] = sum(r.efficiency_rate for r in records) / len(records)
+                    if 'billable_efficiency_rate' in fields:
+                        group['billable_efficiency_rate'] = sum(r.billable_efficiency_rate for r in records) / len(records)
+                    if 'utilization_rate' in fields:
+                        group['utilization_rate'] = sum(r.utilization_rate for r in records) / len(records)
+                    if 'billable_utilization_rate' in fields:
+                        group['billable_utilization_rate'] = sum(r.billable_utilization_rate for r in records) / len(records)
+        
+        return res

+ 90 - 0
hr_efficiency/report/hr_efficiency_report_views.xml

@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Pivot View -->
+        <record id="view_hr_efficiency_report_pivot" model="ir.ui.view">
+            <field name="name">hr.efficiency.report.pivot</field>
+            <field name="model">hr.efficiency.report</field>
+            <field name="arch" type="xml">
+                <pivot string="Efficiency Analysis" sample="1">
+                    <field name="month_year" interval="month" type="row"/>
+                    <field name="employee_id" type="row"/>
+                    <field name="efficiency_rate" type="measure" widget="percentage"/>
+                    <field name="billable_efficiency_rate" type="measure" widget="percentage"/>
+                    <field name="utilization_rate" type="measure" widget="percentage"/>
+                    <field name="billable_utilization_rate" type="measure" widget="percentage"/>
+                    <field name="available_hours" type="measure" widget="float_time"/>
+                    <field name="planned_hours" type="measure" widget="float_time"/>
+                    <field name="total_actual_hours" type="measure" widget="float_time"/>
+                </pivot>
+            </field>
+        </record>
+
+        <!-- Graph View -->
+        <record id="view_hr_efficiency_report_graph" model="ir.ui.view">
+            <field name="name">hr.efficiency.report.graph</field>
+            <field name="model">hr.efficiency.report</field>
+            <field name="arch" type="xml">
+                <graph string="Efficiency Analysis" type="bar" sample="1">
+                    <field name="month_year" interval="month"/>
+                    <field name="employee_id"/>
+                    <field name="efficiency_rate" type="measure"/>
+                    <field name="billable_efficiency_rate" type="measure"/>
+                </graph>
+            </field>
+        </record>
+
+        <!-- Search View -->
+        <record id="view_hr_efficiency_report_search" model="ir.ui.view">
+            <field name="name">hr.efficiency.report.search</field>
+            <field name="model">hr.efficiency.report</field>
+            <field name="arch" type="xml">
+                <search string="Search Efficiency Report">
+                    <field name="employee_id"/>
+                    <field name="month_year"/>
+                    <field name="department_id"/>
+                    <field name="company_id" groups="base.group_multi_company"/>
+                    <filter string="High Efficiency" name="high_efficiency" domain="[('efficiency_rate', '>=', 90)]"/>
+                    <filter string="Medium Efficiency" name="medium_efficiency" domain="[('efficiency_rate', '>=', 70), ('efficiency_rate', '&lt;', 90)]"/>
+                    <filter string="Low Efficiency" name="low_efficiency" domain="[('efficiency_rate', '&lt;', 70)]"/>
+                    <filter string="High Billable Efficiency" name="high_billable_efficiency" domain="[('billable_efficiency_rate', '>=', 90)]"/>
+                    <group expand="0" string="Group By">
+                        <filter string="Employee" name="group_employee" context="{'group_by': 'employee_id'}"/>
+                        <filter string="Month" name="group_month" context="{'group_by': 'month_year'}"/>
+                        <filter string="Department" name="group_department" context="{'group_by': 'department_id'}"/>
+                        <filter string="Company" name="group_company" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
+                    </group>
+                </search>
+            </field>
+        </record>
+
+        <!-- Action -->
+        <record id="action_hr_efficiency_report" model="ir.actions.act_window">
+            <field name="name">Efficiency Analysis</field>
+            <field name="res_model">hr.efficiency.report</field>
+            <field name="view_mode">pivot,graph</field>
+            <field name="search_view_id" ref="view_hr_efficiency_report_search"/>
+            <field name="context">{
+                'pivot_row_groupby': ['month_year:month'],
+                'pivot_measures': ['efficiency_rate', 'billable_efficiency_rate', 'utilization_rate'],
+                'graph_groupbys': ['month_year:month', 'employee_id'],
+                'group_by': [],
+            }</field>
+            <field name="help" type="html">
+                <p class="o_view_nocontent_empty_folder">
+                    No efficiency data found!
+                </p><p>
+                    Generate efficiency data by running the calculation wizard or wait for the automatic weekly calculation.
+                </p>
+            </field>
+        </record>
+
+        <!-- Menu -->
+        <menuitem id="menu_hr_efficiency_report"
+                  name="Efficiency Analysis"
+                  parent="hr.menu_hr_root"
+                  action="action_hr_efficiency_report"
+                  sequence="52"
+                  groups="hr.group_hr_user"/>
+    </data>
+</odoo>

+ 13 - 0
hr_efficiency/security/ir.model.access.csv

@@ -0,0 +1,13 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_hr_efficiency_user,hr.efficiency.user,model_hr_efficiency,hr.group_hr_user,1,1,1,0
+access_hr_efficiency_manager,hr.efficiency.manager,model_hr_efficiency,hr.group_hr_manager,1,1,1,1
+access_hr_efficiency_calculation_wizard_user,hr.efficiency.calculation.wizard.user,model_hr_efficiency_calculation_wizard,hr.group_hr_user,1,1,1,0
+access_hr_efficiency_calculation_wizard_manager,hr.efficiency.calculation.wizard.manager,model_hr_efficiency_calculation_wizard,hr.group_hr_manager,1,1,1,1
+access_hr_efficiency_report_user,hr.efficiency.report.user,model_hr_efficiency_report,hr.group_hr_user,1,0,0,0
+access_hr_efficiency_report_manager,hr.efficiency.report.manager,model_hr_efficiency_report,hr.group_hr_manager,1,0,0,0
+access_hr_efficiency_indicator_user,hr.efficiency.indicator.user,model_hr_efficiency_indicator,hr.group_hr_user,1,1,1,0
+access_hr_efficiency_indicator_manager,hr.efficiency.indicator.manager,model_hr_efficiency_indicator,hr.group_hr_manager,1,1,1,1
+access_hr_efficiency_field_visibility_wizard_user,hr.efficiency.field.visibility.wizard.user,model_hr_efficiency_field_visibility_wizard,hr.group_hr_user,1,0,0,0
+access_hr_efficiency_field_visibility_wizard_manager,hr.efficiency.field.visibility.wizard.manager,model_hr_efficiency_field_visibility_wizard,hr.group_hr_manager,1,1,1,1
+access_hr_efficiency_dynamic_field_user,hr.efficiency.dynamic.field.user,model_hr_efficiency_dynamic_field,hr.group_hr_user,1,1,1,0
+access_hr_efficiency_dynamic_field_manager,hr.efficiency.dynamic.field.manager,model_hr_efficiency_dynamic_field,hr.group_hr_manager,1,1,1,1

+ 116 - 0
hr_efficiency/views/hr_efficiency_dynamic_field_views.xml

@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- List View -->
+        <record id="view_hr_efficiency_dynamic_field_list" model="ir.ui.view">
+            <field name="name">hr.efficiency.dynamic.field.list</field>
+            <field name="model">hr.efficiency.dynamic.field</field>
+            <field name="arch" type="xml">
+                <list string="Dynamic Fields">
+                    <field name="sequence" widget="handle"/>
+                    <field name="name"/>
+                    <field name="label"/>
+                    <field name="indicator_id"/>
+                    <field name="active"/>
+                    <field name="show_in_list"/>
+                    <field name="show_in_form"/>
+                    <field name="widget"/>
+                    <field name="decoration_success"/>
+                    <field name="decoration_warning"/>
+                    <field name="decoration_danger"/>
+                </list>
+            </field>
+        </record>
+
+        <!-- Form View -->
+        <record id="view_hr_efficiency_dynamic_field_form" model="ir.ui.view">
+            <field name="name">hr.efficiency.dynamic.field.form</field>
+            <field name="model">hr.efficiency.dynamic.field</field>
+            <field name="arch" type="xml">
+                <form string="Dynamic Field Configuration">
+                    <header>
+                        <button name="action_toggle_visibility" string="Toggle Visibility" type="object" class="btn-secondary"/>
+                        <button name="action_move_up" string="Move Up" type="object" class="btn-secondary"/>
+                        <button name="action_move_down" string="Move Down" type="object" class="btn-secondary"/>
+                    </header>
+                    <sheet>
+                        <div class="oe_title">
+                            <h1>
+                                <field name="label" placeholder="Field Label"/>
+                            </h1>
+                        </div>
+                        <group>
+                            <group>
+                                <field name="name"/>
+                                <field name="indicator_id"/>
+                                <field name="sequence"/>
+                                <field name="active"/>
+                            </group>
+                            <group>
+                                <field name="widget"/>
+                                <field name="show_in_list"/>
+                                <field name="show_in_form"/>
+                                <field name="show_in_search"/>
+                            </group>
+                        </group>
+                        <notebook>
+                            <page string="Styling" name="styling">
+                                <group>
+                                    <field name="decoration_success"/>
+                                    <field name="decoration_warning"/>
+                                    <field name="decoration_danger"/>
+                                </group>
+                            </page>
+                            <page string="Technical Info" name="technical">
+                                <group>
+                                    <field name="field_technical_name" readonly="1"/>
+                                </group>
+                            </page>
+                        </notebook>
+                    </sheet>
+                </form>
+            </field>
+        </record>
+
+        <!-- Search View -->
+        <record id="view_hr_efficiency_dynamic_field_search" model="ir.ui.view">
+            <field name="name">hr.efficiency.dynamic.field.search</field>
+            <field name="model">hr.efficiency.dynamic.field</field>
+            <field name="arch" type="xml">
+                <search string="Search Dynamic Fields">
+                    <field name="name"/>
+                    <field name="label"/>
+                    <field name="indicator_id"/>
+                    <filter string="Active" name="active" domain="[('active', '=', True)]"/>
+                    <filter string="Inactive" name="inactive" domain="[('active', '=', False)]"/>
+                    <separator/>
+                    <filter string="Show in List" name="show_in_list" domain="[('show_in_list', '=', True)]"/>
+                    <filter string="Show in Form" name="show_in_form" domain="[('show_in_form', '=', True)]"/>
+                </search>
+            </field>
+        </record>
+
+        <!-- Action -->
+        <record id="action_hr_efficiency_dynamic_field" model="ir.actions.act_window">
+            <field name="name">Dynamic Fields</field>
+            <field name="res_model">hr.efficiency.dynamic.field</field>
+            <field name="view_mode">list,form</field>
+            <field name="context">{}</field>
+            <field name="help" type="html">
+                <p class="o_view_nocontent_smiling_face">
+                    Create your first dynamic field!
+                </p>
+                <p>
+                    Dynamic fields allow you to customize how indicators are displayed in the efficiency views.
+                </p>
+            </field>
+        </record>
+
+        <!-- Menu Item -->
+        <menuitem id="menu_hr_efficiency_dynamic_field"
+                  name="Dynamic Fields"
+                  parent="planning.planning_menu_settings"
+                  action="action_hr_efficiency_dynamic_field"
+                  sequence="15"/>
+    </data>
+</odoo>

+ 45 - 0
hr_efficiency/views/hr_efficiency_dynamic_views.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Vista heredada para la lista con campos dinámicos -->
+        <record id="view_hr_efficiency_list_inherited" model="ir.ui.view">
+            <field name="name">hr.efficiency.list.inherited</field>
+            <field name="model">hr.efficiency</field>
+            <field name="inherit_id" ref="hr_efficiency.view_hr_efficiency_tree"/>
+            <field name="arch" type="xml">
+                <xpath expr="//field[@name='total_actual_hours']" position="after">
+                    <!-- Dynamic indicator fields will be added here -->
+                </xpath>
+            </field>
+        </record>
+
+        <!-- Vista heredada para el formulario con campos dinámicos -->
+        <record id="view_hr_efficiency_form_inherited" model="ir.ui.view">
+            <field name="name">hr.efficiency.form.inherited</field>
+            <field name="model">hr.efficiency</field>
+            <field name="inherit_id" ref="hr_efficiency.view_hr_efficiency_form"/>
+            <field name="arch" type="xml">
+                <xpath expr="//field[@name='overall_efficiency']" position="before">
+                    <!-- Dynamic indicator fields will be added here -->
+                </xpath>
+            </field>
+        </record>
+
+        <!-- Actualizar la acción para usar la vista heredada -->
+        <record id="action_hr_efficiency" model="ir.actions.act_window">
+            <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="context">{'search_default_current_month': 1}</field>
+            <field name="help" type="html">
+                <p class="o_view_nocontent_smiling_face">
+                    No efficiency records found!
+                </p>
+                <p>
+                    Click on "Calculate Efficiency" to generate efficiency records for employees.
+                </p>
+            </field>
+        </record>
+    </data>
+</odoo>

+ 85 - 0
hr_efficiency/views/hr_efficiency_indicator_views.xml

@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <!-- Tree View -->
+    <record id="view_hr_efficiency_indicator_tree" model="ir.ui.view">
+        <field name="name">hr.efficiency.indicator.tree</field>
+        <field name="model">hr.efficiency.indicator</field>
+        <field name="arch" type="xml">
+            <list string="Efficiency Indicators">
+                <field name="sequence" widget="handle"/>
+                <field name="name"/>
+                <field name="formula"/>
+                <field name="target_percentage"/>
+                <field name="weight"/>
+                <field name="color_threshold_green"/>
+                <field name="color_threshold_yellow"/>
+                <field name="active"/>
+            </list>
+        </field>
+    </record>
+
+    <!-- Form View -->
+    <record id="view_hr_efficiency_indicator_form" model="ir.ui.view">
+        <field name="name">hr.efficiency.indicator.form</field>
+        <field name="model">hr.efficiency.indicator</field>
+        <field name="arch" type="xml">
+            <form string="Efficiency Indicator">
+                <sheet>
+                    <group>
+                        <group>
+                            <field name="name"/>
+                            <field name="sequence"/>
+                            <field name="active"/>
+                        </group>
+                        <group>
+                            <field name="target_percentage"/>
+                            <field name="weight"/>
+                            <field name="color_threshold_green"/>
+                            <field name="color_threshold_yellow"/>
+                        </group>
+                    </group>
+                    <group>
+                        <field name="description"/>
+                    </group>
+                    <group>
+                        <field name="formula" widget="ace" options="{'mode': 'python'}"/>
+                    </group>
+                </sheet>
+            </form>
+        </field>
+    </record>
+
+    <!-- Search View -->
+    <record id="view_hr_efficiency_indicator_search" model="ir.ui.view">
+        <field name="name">hr.efficiency.indicator.search</field>
+        <field name="model">hr.efficiency.indicator</field>
+        <field name="arch" type="xml">
+            <search string="Search Efficiency Indicators">
+                <field name="name"/>
+                <filter string="Active" name="active" domain="[('active', '=', True)]"/>
+                <filter string="Inactive" name="inactive" domain="[('active', '=', False)]"/>
+            </search>
+        </field>
+    </record>
+
+    <!-- Action -->
+    <record id="action_hr_efficiency_indicator" model="ir.actions.act_window">
+        <field name="name">Efficiency Indicators</field>
+        <field name="res_model">hr.efficiency.indicator</field>
+                        <field name="view_mode">list,form</field>
+        <field name="help" type="html">
+            <p class="o_view_nocontent_empty_folder">
+                No efficiency indicators configured!
+            </p><p>
+                Create efficiency indicators to define how efficiency is calculated.
+            </p>
+        </field>
+    </record>
+
+    <!-- Menu Item -->
+    <menuitem id="menu_hr_efficiency_indicator"
+              name="Efficiency Indicators"
+              parent="planning.planning_menu_settings_config"
+              action="action_hr_efficiency_indicator"
+              sequence="20"/>
+</odoo>

+ 155 - 0
hr_efficiency/views/hr_efficiency_views.xml

@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Tree View -->
+        <record id="view_hr_efficiency_tree" model="ir.ui.view">
+            <field name="name">hr.efficiency.tree</field>
+            <field name="model">hr.efficiency</field>
+            <field name="arch" type="xml">
+                <list string="Employee Efficiency" decoration-success="overall_efficiency >= 90" decoration-warning="overall_efficiency >= 70 and overall_efficiency &lt; 90" decoration-danger="overall_efficiency &lt; 70">
+                    <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="overall_efficiency"/>
+                </list>
+            </field>
+        </record>
+
+        <!-- Form View -->
+        <record id="view_hr_efficiency_form" model="ir.ui.view">
+            <field name="name">hr.efficiency.form</field>
+            <field name="model">hr.efficiency</field>
+            <field name="arch" type="xml">
+                <form string="Employee Efficiency">
+                    <sheet>
+                        <div class="oe_title">
+                            <h1>
+                                <field name="display_name" readonly="1"/>
+                            </h1>
+                        </div>
+                        <group>
+                            <group>
+                                <field name="month_year"/>
+                                <field name="employee_id"/>
+                                <field name="company_id" groups="base.group_multi_company"/>
+                                <field name="calculation_date"/>
+                            </group>
+                            <group>
+                                <!-- Dynamic indicator fields will be added here -->
+                                <field name="overall_efficiency"/>
+                            </group>
+                        </group>
+                        <notebook>
+                            <page string="Available Hours" name="available">
+                                <group>
+                                    <field name="available_hours" widget="float_time"/>
+                                </group>
+                            </page>
+                            <page string="Planned Hours" name="planned">
+                                <group>
+                                    <field name="planned_hours" widget="float_time"/>
+                                    <field name="planned_billable_hours" widget="float_time"/>
+                                    <field name="planned_non_billable_hours" widget="float_time"/>
+                                </group>
+                            </page>
+                            <page string="Actual Hours" name="actual">
+                                <group>
+                                    <field name="actual_billable_hours" widget="float_time"/>
+                                    <field name="actual_non_billable_hours" widget="float_time"/>
+                                    <field name="total_actual_hours" widget="float_time"/>
+                                </group>
+                            </page>
+                        </notebook>
+                    </sheet>
+                </form>
+            </field>
+        </record>
+
+        <!-- Search View -->
+        <record id="view_hr_efficiency_search" model="ir.ui.view">
+            <field name="name">hr.efficiency.search</field>
+            <field name="model">hr.efficiency</field>
+            <field name="arch" type="xml">
+                <search string="Search Efficiency">
+                    <field name="employee_id"/>
+                    <field name="month_year"/>
+                    <field name="company_id" groups="base.group_multi_company"/>
+                    
+                                            <!-- Filtros de indicadores -->
+                        <filter string="High Overall Efficiency" name="high_overall_efficiency" domain="[('overall_efficiency', '>=', 90)]"/>
+                    
+                    <!-- Filtros de tipo de proyecto -->
+                    <separator/>
+                    <filter string="Proyectos Facturables" name="billable_projects" domain="[('planned_billable_hours', '>', 0)]"/>
+                    <filter string="Proyectos No Facturables" name="non_billable_projects" domain="[('planned_non_billable_hours', '>', 0)]"/>
+                    
+                                            <!-- Filtros de fecha -->
+                        <separator/>
+                        <filter string="junio" name="june_2025" domain="[('month_year', '=', '2025-06')]"/>
+                        <filter string="julio" name="july_2025" domain="[('month_year', '=', '2025-07')]"/>
+                        <filter string="agosto" name="august_2025" domain="[('month_year', '=', '2025-08')]"/>
+                        <filter string="septiembre" name="september_2025" domain="[('month_year', '=', '2025-09')]"/>
+                        <filter string="octubre" name="october_2025" domain="[('month_year', '=', '2025-10')]"/>
+                        
+                        <!-- Filtros de trimestres -->
+                        <separator/>
+                        <filter string="Trimestre 1" name="q1_2025" domain="[('month_year', 'in', ['2025-01', '2025-02', '2025-03'])]"/>
+                        <filter string="Trimestre 2" name="q2_2025" domain="[('month_year', 'in', ['2025-04', '2025-05', '2025-06'])]"/>
+                        <filter string="Trimestre 3" name="q3_2025" domain="[('month_year', 'in', ['2025-07', '2025-08', '2025-09'])]"/>
+                        <filter string="Trimestre 4" name="q4_2025" domain="[('month_year', 'in', ['2025-10', '2025-11', '2025-12'])]"/>
+                        
+                        <!-- Filtros de años -->
+                        <separator/>
+                        <filter string="2023" name="year_2023" domain="[('month_year', 'like', '2023-')]"/>
+                        <filter string="2024" name="year_2024" domain="[('month_year', 'like', '2024-')]"/>
+                        <filter string="2025" name="year_2025" domain="[('month_year', 'like', '2025-')]"/>
+                    
+                    <!-- Filtro de archivado -->
+                    <separator/>
+                    <filter string="Archivado" name="archived" domain="[('active', '=', False)]"/>
+                    <filter string="No Archivado" name="not_archived" domain="[('active', '=', True)]"/>
+                    
+                    <group expand="0" string="Group By">
+                        <filter string="Employee" name="group_employee" context="{'group_by': 'employee_id'}"/>
+                        <filter string="Month" name="group_month" context="{'group_by': 'month_year'}"/>
+                        <filter string="Company" name="group_company" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
+                    </group>
+                </search>
+            </field>
+        </record>
+
+        <!-- Action -->
+        <record id="action_hr_efficiency" model="ir.actions.act_window">
+            <field name="name">Employee Efficiency</field>
+            <field name="res_model">hr.efficiency</field>
+            <field name="view_mode">list,form</field>
+            <field name="search_view_id" ref="view_hr_efficiency_search"/>
+            <field name="context">{'search_default_august_2025': 1}</field>
+            <field name="help" type="html">
+                <p class="o_view_nocontent_empty_folder">
+                    No efficiency records found!
+                </p><p>
+                    Create efficiency records by running the calculation wizard or wait for the automatic weekly calculation.
+                </p>
+            </field>
+        </record>
+        
+
+
+        <!-- Menu -->
+        <menuitem id="menu_hr_efficiency"
+                  name="Efficiency"
+                  parent="hr.menu_hr_root"
+                  action="action_hr_efficiency"
+                  sequence="50"
+                  groups="hr.group_hr_user"/>
+    </data>
+</odoo>

+ 12 - 0
hr_efficiency/views/planning_views.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Add Efficiency menu to Planning Reporting -->
+                            <menuitem id="planning_menu_efficiency"
+                              name="Efficiency"
+                              parent="planning.planning_menu_reporting"
+                              action="hr_efficiency.action_hr_efficiency"
+                              sequence="20"
+                              groups="planning.group_planning_user"/>
+    </data>
+</odoo>

+ 5 - 0
hr_efficiency/wizard/__init__.py

@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import hr_efficiency_calculation_wizard
+from . import hr_efficiency_field_visibility_wizard

+ 190 - 0
hr_efficiency/wizard/hr_efficiency_calculation_wizard.py

@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import date, datetime
+from dateutil.relativedelta import relativedelta
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
+
+class HrEfficiencyCalculationWizard(models.TransientModel):
+    _name = 'hr.efficiency.calculation.wizard'
+    _description = 'HR Efficiency Calculation Wizard'
+
+    start_month = fields.Char('Start Month', required=True, help="Format: YYYY-MM")
+    end_month = fields.Char('End Month', required=True, help="Format: YYYY-MM")
+    employee_ids = fields.Many2many('hr.employee', string='Employees', help="Leave empty to calculate for all active employees")
+    company_id = fields.Many2one('res.company', 'Company', required=True, default=lambda self: self.env.company)
+    
+    # Results
+    result_message = fields.Text('Result', readonly=True)
+    created_count = fields.Integer('Created Records', readonly=True)
+    updated_count = fields.Integer('Updated Records', readonly=True)
+    total_count = fields.Integer('Total Processed', readonly=True)
+
+    @api.model
+    def default_get(self, fields_list):
+        res = super().default_get(fields_list)
+        # Set default period: last 3 months and next 6 months
+        current_date = date.today()
+        res['start_month'] = (current_date - relativedelta(months=3)).strftime('%Y-%m')
+        res['end_month'] = (current_date + relativedelta(months=6)).strftime('%Y-%m')
+        return res
+
+    def action_calculate_efficiency(self):
+        """
+        Calculate efficiency for the specified period and employees
+        """
+        # Validate input
+        self._validate_input()
+        
+        # Get employees to process
+        if self.employee_ids:
+            employees = self.employee_ids
+        else:
+            employees = self.env['hr.employee'].search([
+                ('active', '=', True),
+                ('company_id', '=', self.company_id.id)
+            ])
+        
+        if not employees:
+            raise UserError(_("No employees found to process."))
+        
+        # Calculate efficiency
+        efficiency_model = self.env['hr.efficiency']
+        
+        # Generate list of months
+        months = self._generate_month_list(self.start_month, self.end_month)
+        
+        created_records = []
+        
+        for employee in employees:
+            for month in months:
+                # Calculate efficiency data
+                efficiency_data = efficiency_model._calculate_employee_efficiency(employee, month)
+                efficiency_data['company_id'] = self.company_id.id
+                
+                # Check if there are changes compared to the latest record
+                latest_record = efficiency_model.search([
+                    ('employee_id', '=', employee.id),
+                    ('month_year', '=', month),
+                    ('company_id', '=', self.company_id.id),
+                ], order='calculation_date desc', limit=1)
+                
+                has_changes = False
+                if latest_record:
+                    # Compare current data with latest record
+                    fields_to_compare = [
+                        'available_hours', 'planned_hours', 'planned_billable_hours',
+                        'planned_non_billable_hours', 'actual_billable_hours',
+                        'actual_non_billable_hours'
+                    ]
+                    
+                    for field in fields_to_compare:
+                        if abs(efficiency_data[field] - latest_record[field]) > 0.01:  # Tolerance for floating point
+                            has_changes = True
+                            break
+                else:
+                    # No previous record exists, so this is a change
+                    has_changes = True
+                
+                # Only create new record if there are changes
+                if has_changes:
+                    # Archive existing records for this employee and month
+                    existing_records = efficiency_model.search([
+                        ('employee_id', '=', employee.id),
+                        ('month_year', '=', month),
+                        ('company_id', '=', self.company_id.id),
+                    ])
+                    if existing_records:
+                        existing_records.write({'active': False})
+                    
+                    # Create new record
+                    new_record = efficiency_model.create(efficiency_data)
+                    created_records.append(new_record)
+        
+        # Update wizard with results
+        self.write({
+            'created_count': len(created_records),
+            'updated_count': 0,  # No longer updating existing records
+            'total_count': len(created_records),
+            'result_message': self._generate_result_message(created_records, employees, months),
+        })
+        
+        return {
+            'type': 'ir.actions.act_window',
+            'res_model': 'hr.efficiency.calculation.wizard',
+            'res_id': self.id,
+            'view_mode': 'form',
+            'target': 'new',
+        }
+
+    def _validate_input(self):
+        """
+        Validate wizard input
+        """
+        # Validate month format
+        try:
+            datetime.strptime(self.start_month, '%Y-%m')
+            datetime.strptime(self.end_month, '%Y-%m')
+        except ValueError:
+            raise UserError(_("Invalid month format. Please use YYYY-MM format (e.g., 2024-01)"))
+        
+        # Validate date range
+        start_date = datetime.strptime(self.start_month, '%Y-%m')
+        end_date = datetime.strptime(self.end_month, '%Y-%m')
+        
+        if start_date > end_date:
+            raise UserError(_("Start month cannot be after end month."))
+        
+        # Limit range to reasonable period (e.g., 2 years)
+        if (end_date - start_date).days > 730:
+            raise UserError(_("Date range cannot exceed 2 years."))
+
+    def _generate_month_list(self, start_month, end_month):
+        """
+        Generate list of months between start_month and end_month (inclusive)
+        """
+        months = []
+        current = datetime.strptime(start_month, '%Y-%m')
+        end = datetime.strptime(end_month, '%Y-%m')
+        
+        while current <= end:
+            months.append(current.strftime('%Y-%m'))
+            current = current + relativedelta(months=1)
+        
+        return months
+
+    def _generate_result_message(self, created_records, employees, months):
+        """
+        Generate result message for the wizard
+        """
+        message = _("Efficiency calculation completed successfully!\n\n")
+        message += _("Period: %s to %s\n") % (self.start_month, self.end_month)
+        message += _("Employees processed: %d\n") % len(employees)
+        message += _("Months processed: %d\n\n") % len(months)
+        message += _("Results:\n")
+        message += _("- New records created: %d\n") % len(created_records)
+        message += _("- Total records processed: %d\n") % len(created_records)
+        message += _("\nNote: New records are only created when there are changes. Old records are archived.")
+        
+        return message
+
+    def action_view_results(self):
+        """
+        Open the efficiency records view
+        """
+        return {
+            'type': 'ir.actions.act_window',
+            'name': _('Efficiency Records'),
+            'res_model': 'hr.efficiency',
+            'view_mode': 'list,form',
+            'domain': [
+                ('company_id', '=', self.company_id.id),
+                ('month_year', '>=', self.start_month),
+                ('month_year', '<=', self.end_month),
+            ],
+            'context': {
+                'default_company_id': self.company_id.id,
+            },
+        }

+ 82 - 0
hr_efficiency/wizard/hr_efficiency_calculation_wizard_views.xml

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Wizard Form View -->
+        <record id="view_hr_efficiency_calculation_wizard_form" model="ir.ui.view">
+            <field name="name">hr.efficiency.calculation.wizard.form</field>
+            <field name="model">hr.efficiency.calculation.wizard</field>
+            <field name="arch" type="xml">
+                <form string="Calculate Employee Efficiency">
+                    <sheet>
+                        <div class="oe_title">
+                            <h1>Calculate Employee Efficiency</h1>
+                        </div>
+                        <group>
+                            <group>
+                                <field name="start_month" placeholder="YYYY-MM"/>
+                                <field name="end_month" placeholder="YYYY-MM"/>
+                            </group>
+                            <group>
+                                <field name="company_id" groups="base.group_multi_company"/>
+                                <field name="employee_ids" widget="many2many_tags"/>
+                            </group>
+                        </group>
+                        <group invisible="not result_message">
+                            <field name="result_message" readonly="1" nolabel="1"/>
+                        </group>
+                        <group invisible="total_count == 0">
+                            <group>
+                                <field name="created_count" readonly="1"/>
+                                <field name="updated_count" readonly="1"/>
+                            </group>
+                            <group>
+                                <field name="total_count" readonly="1"/>
+                            </group>
+                        </group>
+                    </sheet>
+                    <footer>
+                        <button name="action_calculate_efficiency" 
+                                string="Calculate Efficiency" 
+                                type="object" 
+                                class="btn-primary"
+                                invisible="result_message"/>
+                        <button name="action_view_results" 
+                                string="View Results" 
+                                type="object" 
+                                class="btn-secondary"
+                                invisible="total_count == 0"/>
+                        <button string="Close" class="btn-secondary" special="cancel"/>
+                    </footer>
+                </form>
+            </field>
+        </record>
+
+        <!-- Action -->
+        <record id="action_hr_efficiency_calculation_wizard" model="ir.actions.act_window">
+            <field name="name">Calculate Efficiency</field>
+            <field name="res_model">hr.efficiency.calculation.wizard</field>
+            <field name="view_mode">form</field>
+            <field name="target">new</field>
+            <field name="help" type="html">
+                <p>
+                    This wizard will calculate efficiency for all employees for the specified period.
+                    The calculation includes:
+                </p>
+                <ul>
+                    <li>Available hours (considering holidays and time off)</li>
+                    <li>Planned hours from the planning module</li>
+                    <li>Actual hours from timesheets</li>
+                    <li>Separation between billable and non-billable projects</li>
+                </ul>
+            </field>
+        </record>
+
+        <!-- Menu -->
+        <menuitem id="menu_hr_efficiency_calculation"
+                  name="Calculate Efficiency"
+                  parent="hr.menu_hr_root"
+                  action="action_hr_efficiency_calculation_wizard"
+                  sequence="51"
+                  groups="hr.group_hr_manager"/>
+    </data>
+</odoo>

+ 66 - 0
hr_efficiency/wizard/hr_efficiency_field_visibility_wizard.py

@@ -0,0 +1,66 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
+
+
+class HrEfficiencyFieldVisibilityWizard(models.TransientModel):
+    _name = 'hr.efficiency.field.visibility.wizard'
+    _description = 'Efficiency Field Visibility Wizard'
+    _table = 'hr_efficiency_field_vis_wizard'
+
+    # Dynamic fields for each indicator
+    indicator_ids = fields.Many2many('hr.efficiency.indicator', 'hr_efficiency_field_vis_wizard_rel', 'wizard_id', 'indicator_id', string='Indicators to Show')
+    
+    @api.model
+    def default_get(self, fields_list):
+        """Get default values - show all active indicators"""
+        res = super().default_get(fields_list)
+        active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)])
+        res['indicator_ids'] = [(6, 0, active_indicators.ids)]
+        return res
+    
+    def action_apply_visibility(self):
+        """Apply the selected field visibility"""
+        # Get all active indicators
+        all_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)])
+        
+        # Get selected indicators
+        selected_indicators = self.indicator_ids
+        
+        # Update the hr.efficiency model to only show selected indicators
+        efficiency_model = self.env['hr.efficiency']
+        
+        # Clear all dynamic fields first
+        for indicator in all_indicators:
+            field_name = efficiency_model._get_indicator_field_name(indicator.name)
+            if hasattr(efficiency_model, field_name):
+                delattr(efficiency_model.__class__, field_name)
+        
+        # Create fields only for selected indicators
+        for indicator in selected_indicators:
+            field_name = efficiency_model._get_indicator_field_name(indicator.name)
+            field = fields.Float(
+                indicator.name + ' (%)',
+                compute='_compute_indicators',
+                store=True,
+                help=f'Indicator: {indicator.description or indicator.name}'
+            )
+            setattr(efficiency_model.__class__, field_name, field)
+        
+        # Update views with dynamic fields
+        efficiency_model._update_views_with_dynamic_fields()
+        
+        # Force recomputation
+        records = efficiency_model.search([])
+        if records:
+            records._invalidate_cache()
+        
+        return {
+            'type': 'ir.actions.client',
+            'tag': 'display_notification',
+            'params': {
+                'title': _('Success'),
+                'message': _('Field visibility updated successfully.'),
+                'type': 'success',
+                'sticky': False,
+            }
+        }

+ 39 - 0
hr_efficiency/wizard/hr_efficiency_field_visibility_wizard_views.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Field Visibility Wizard Form View -->
+        <record id="view_hr_efficiency_field_visibility_wizard_form" model="ir.ui.view">
+            <field name="name">hr.efficiency.field.visibility.wizard.form</field>
+            <field name="model">hr.efficiency.field.visibility.wizard</field>
+            <field name="arch" type="xml">
+                <form string="Field Visibility Settings">
+                    <sheet>
+                        <group>
+                            <field name="indicator_ids" widget="many2many_tags" options="{'no_create': True}"/>
+                        </group>
+                    </sheet>
+                    <footer>
+                        <button name="action_apply_visibility" string="Apply" type="object" class="btn-primary"/>
+                        <button string="Cancel" class="btn-secondary" special="cancel"/>
+                    </footer>
+                </form>
+            </field>
+        </record>
+
+        <!-- Field Visibility Wizard Action -->
+        <record id="action_hr_efficiency_field_visibility_wizard" model="ir.actions.act_window">
+            <field name="name">Field Visibility</field>
+            <field name="res_model">hr.efficiency.field.visibility.wizard</field>
+            <field name="view_mode">form</field>
+            <field name="target">new</field>
+            <field name="context">{}</field>
+        </record>
+
+        <!-- Menu Item for Field Visibility -->
+        <menuitem id="menu_hr_efficiency_field_visibility"
+                  name="Field Visibility"
+                  parent="planning.planning_menu_settings"
+                  action="action_hr_efficiency_field_visibility_wizard"
+                  sequence="20"/>
+    </data>
+</odoo>