|
|
@@ -278,8 +278,8 @@ class HrEfficiencyIndicator(models.Model):
|
|
|
'total_actual_hours': efficiency_data.get('total_actual_hours', 0),
|
|
|
'expected_hours_to_date': efficiency_data.get('expected_hours_to_date', 0),
|
|
|
'wage': efficiency_data.get('wage', 0),
|
|
|
+ 'wage_overhead': efficiency_data.get('wage_overhead', 0),
|
|
|
'utilization_rate': efficiency_data.get('utilization_rate', 100.0),
|
|
|
- 'overhead': efficiency_data.get('overhead', 40.0),
|
|
|
'precio_por_hora': efficiency_data.get('precio_por_hora', 0),
|
|
|
}
|
|
|
|
|
|
@@ -311,3 +311,262 @@ class HrEfficiencyIndicator(models.Model):
|
|
|
return 'text-danger'
|
|
|
else:
|
|
|
return 'text-danger' # Below red threshold - critical
|
|
|
+
|
|
|
+ def action_sync_from_xml(self):
|
|
|
+ """
|
|
|
+ Acción manual para forzar sincronización desde XML
|
|
|
+ Útil para debugging y actualizaciones manuales
|
|
|
+ """
|
|
|
+ result = self.env['hr.efficiency.indicator'].sync_indicators_from_xml()
|
|
|
+
|
|
|
+ # Mostrar mensaje al usuario
|
|
|
+ message = f"""
|
|
|
+ 🔄 Sincronización completada:
|
|
|
+ 📊 Indicadores procesados: {result['indicators_processed']}
|
|
|
+ 🆕 Campos creados: {result['fields_created']}
|
|
|
+ ✅ Campos actualizados: {result['fields_updated']}
|
|
|
+ 🔄 Registros recalculados: {result['records_recalculated']}
|
|
|
+ """
|
|
|
+
|
|
|
+ return {
|
|
|
+ 'type': 'ir.actions.client',
|
|
|
+ 'tag': 'display_notification',
|
|
|
+ 'params': {
|
|
|
+ 'title': 'Sincronización Completada',
|
|
|
+ 'message': message,
|
|
|
+ 'type': 'success',
|
|
|
+ 'sticky': True,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @api.model
|
|
|
+ def _load_indicator_from_xml(self, indicator_name):
|
|
|
+ """
|
|
|
+ Cargar datos del indicador desde el XML
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ # Mapeo de indicadores con sus datos del XML
|
|
|
+ xml_indicators = {
|
|
|
+ 'Planning Coverage': {
|
|
|
+ 'formula': '(planned_hours / available_hours) if available_hours > 0 else 0',
|
|
|
+ 'sequence': 10,
|
|
|
+ 'description': 'Cobertura de Planificación: Mide qué porcentaje del tiempo disponible ha sido planificado, sin importar si es facturable o no. (Total Horas Planeadas / Horas Disponibles)',
|
|
|
+ 'target_percentage': 95.0,
|
|
|
+ 'weight': 0.0,
|
|
|
+ 'color_threshold_green': 85.0,
|
|
|
+ 'color_threshold_yellow': 75.0,
|
|
|
+ 'color_threshold_red': 65.0,
|
|
|
+ },
|
|
|
+ 'Planned Utilization': {
|
|
|
+ 'formula': '(planned_billable_hours / available_hours) if available_hours > 0 else 0',
|
|
|
+ 'sequence': 20,
|
|
|
+ 'description': 'Utilización Planeada: ¿Cuál era el objetivo de utilización para el equipo? Permite comparar meta vs. realidad. (Horas Facturables Planeadas / Horas Disponibles)',
|
|
|
+ 'target_percentage': 80.0,
|
|
|
+ 'weight': 0.0,
|
|
|
+ 'color_threshold_green': 85.0,
|
|
|
+ 'color_threshold_yellow': 75.0,
|
|
|
+ 'color_threshold_red': 60.0,
|
|
|
+ },
|
|
|
+ 'Break-Even Hours Needed': {
|
|
|
+ 'formula': '(wage_overhead * (utilization_rate / 100)) / precio_por_hora if precio_por_hora > 0 else 0',
|
|
|
+ 'sequence': 30,
|
|
|
+ 'description': 'Horas de Punto de Equilibrio: ¿Cuántas horas facturables se necesitan para cubrir el costo productivo (costo ponderado por utilización)? El resultado es un número de horas.',
|
|
|
+ 'target_percentage': 0.0,
|
|
|
+ 'weight': 0.0,
|
|
|
+ 'color_threshold_green': 85.0,
|
|
|
+ 'color_threshold_yellow': 75.0,
|
|
|
+ 'color_threshold_red': 65.0,
|
|
|
+ },
|
|
|
+ 'Planned Profitability Coverage': {
|
|
|
+ 'formula': '(planned_billable_hours / ((wage_overhead * (utilization_rate / 100)) / precio_por_hora)) if wage_overhead > 0 and precio_por_hora > 0 else 0',
|
|
|
+ 'sequence': 40,
|
|
|
+ 'description': 'Cobertura de Rentabilidad Planeada: Mide si las horas facturables planeadas son suficientes para alcanzar el punto de equilibrio. Más de 100% indica un plan rentable. (Horas Facturables Planeadas / Horas de Punto de Equilibrio)',
|
|
|
+ 'target_percentage': 100.0,
|
|
|
+ 'weight': 0.0,
|
|
|
+ 'color_threshold_green': 100.0,
|
|
|
+ 'color_threshold_yellow': 85.0,
|
|
|
+ 'color_threshold_red': 70.0,
|
|
|
+ },
|
|
|
+ 'Estimation Accuracy Plan Adherence': {
|
|
|
+ 'formula': '(total_actual_hours / planned_hours) if planned_hours > 0 else 0',
|
|
|
+ 'sequence': 50,
|
|
|
+ 'description': 'Precisión de la Estimación: ¿Qué tan acertada fue la planificación general vs. la realidad? Un valor cercano a 100% es ideal. (Total Horas Registradas / Total Horas Planeadas)',
|
|
|
+ 'target_percentage': 100.0,
|
|
|
+ 'weight': 0.0,
|
|
|
+ 'color_threshold_green': 85.0,
|
|
|
+ 'color_threshold_yellow': 75.0,
|
|
|
+ 'color_threshold_red': 60.0,
|
|
|
+ },
|
|
|
+ 'Billable Plan Compliance': {
|
|
|
+ 'formula': '(actual_billable_hours / planned_billable_hours) if planned_billable_hours > 0 else 0',
|
|
|
+ 'sequence': 60,
|
|
|
+ 'description': 'Cumplimiento del Plan Facturable: ¿Se cumplió con el objetivo específico de horas facturables? (Horas Facturables Registradas / Horas Facturables Planeadas)',
|
|
|
+ 'target_percentage': 100.0,
|
|
|
+ 'weight': 0.0,
|
|
|
+ 'color_threshold_green': 85.0,
|
|
|
+ 'color_threshold_yellow': 75.0,
|
|
|
+ 'color_threshold_red': 65.0,
|
|
|
+ },
|
|
|
+ 'Occupancy Rate': {
|
|
|
+ 'formula': '(total_actual_hours / available_hours) if available_hours > 0 else 0',
|
|
|
+ 'sequence': 70,
|
|
|
+ 'description': 'Tasa de Ocupación: Mide qué tan "ocupado" está el equipo en general, considerando horas facturables y no facturables. (Total Horas Registradas / Horas Disponibles)',
|
|
|
+ 'target_percentage': 95.0,
|
|
|
+ 'weight': 0.0,
|
|
|
+ 'color_threshold_green': 85.0,
|
|
|
+ 'color_threshold_yellow': 75.0,
|
|
|
+ 'color_threshold_red': 60.0,
|
|
|
+ },
|
|
|
+ 'Utilization Rate': {
|
|
|
+ 'formula': '(actual_billable_hours / available_hours) if available_hours > 0 else 0',
|
|
|
+ 'sequence': 80,
|
|
|
+ 'description': 'Tasa de Utilización: Mide qué tan "productivo" (generando ingresos) está el equipo. (Horas Facturables Registradas / Horas Disponibles)',
|
|
|
+ 'target_percentage': 80.0,
|
|
|
+ 'weight': 0.0,
|
|
|
+ 'color_threshold_green': 85.0,
|
|
|
+ 'color_threshold_yellow': 75.0,
|
|
|
+ 'color_threshold_red': 60.0,
|
|
|
+ },
|
|
|
+ 'Billability Rate': {
|
|
|
+ 'formula': '(actual_billable_hours / total_actual_hours) if total_actual_hours > 0 else 0',
|
|
|
+ 'sequence': 90,
|
|
|
+ 'description': 'Tasa de Facturabilidad: De todo el tiempo trabajado, ¿qué porcentaje fue facturable? (Horas Facturables Registradas / Total Horas Registradas)',
|
|
|
+ 'target_percentage': 85.0,
|
|
|
+ 'weight': 0.0,
|
|
|
+ 'color_threshold_green': 85.0,
|
|
|
+ 'color_threshold_yellow': 75.0,
|
|
|
+ 'color_threshold_red': 65.0,
|
|
|
+ },
|
|
|
+ 'Actual Profitability Achievement': {
|
|
|
+ 'formula': '(actual_billable_hours / ((wage_overhead * (utilization_rate / 100)) / precio_por_hora)) if wage_overhead > 0 and precio_por_hora > 0 else 0',
|
|
|
+ 'sequence': 100,
|
|
|
+ 'description': 'Logro de Rentabilidad Real: Mide el progreso real hacia el punto de equilibrio basado en las horas facturables registradas. Más de 100% indica que ya se ha alcanzado la rentabilidad. (Horas Facturables Registradas / Horas de Punto de Equilibrio)',
|
|
|
+ 'target_percentage': 100.0,
|
|
|
+ 'weight': 0.0,
|
|
|
+ 'color_threshold_green': 100.0,
|
|
|
+ 'color_threshold_yellow': 85.0,
|
|
|
+ 'color_threshold_red': 70.0,
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ return xml_indicators.get(indicator_name, {})
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ _logger.error(f"Error cargando indicador {indicator_name} desde XML: {e}")
|
|
|
+ return {}
|
|
|
+
|
|
|
+ @api.model
|
|
|
+ def sync_indicators_from_xml(self):
|
|
|
+ """
|
|
|
+ Sincroniza los indicadores desde el XML con la base de datos
|
|
|
+ Compara definiciones y actualiza campos dinámicos automáticamente
|
|
|
+ """
|
|
|
+ import logging
|
|
|
+ _logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+ try:
|
|
|
+ _logger.info("🔄 Iniciando sincronización de indicadores desde XML...")
|
|
|
+
|
|
|
+ # Obtener todos los indicadores activos ordenados por secuencia
|
|
|
+ active_indicators = self.search([('active', '=', True)], order='sequence')
|
|
|
+ _logger.info(f"📊 Encontrados {len(active_indicators)} indicadores activos")
|
|
|
+
|
|
|
+ # Contadores para el reporte
|
|
|
+ fields_created = 0
|
|
|
+ fields_updated = 0
|
|
|
+ indicators_processed = 0
|
|
|
+
|
|
|
+ for indicator in active_indicators:
|
|
|
+ try:
|
|
|
+ # PASO 1: Actualizar el indicador desde XML (fórmulas, secuencias, etc.)
|
|
|
+ # Cargar el indicador desde XML para obtener la versión correcta
|
|
|
+ xml_indicator = self._load_indicator_from_xml(indicator.name)
|
|
|
+ if xml_indicator:
|
|
|
+ # Actualizar indicador con datos del XML
|
|
|
+ update_vals = {}
|
|
|
+ if indicator.formula != xml_indicator.get('formula'):
|
|
|
+ update_vals['formula'] = xml_indicator.get('formula')
|
|
|
+ if indicator.sequence != xml_indicator.get('sequence'):
|
|
|
+ update_vals['sequence'] = xml_indicator.get('sequence')
|
|
|
+ if indicator.description != xml_indicator.get('description'):
|
|
|
+ update_vals['description'] = xml_indicator.get('description')
|
|
|
+ if indicator.target_percentage != xml_indicator.get('target_percentage'):
|
|
|
+ update_vals['target_percentage'] = xml_indicator.get('target_percentage')
|
|
|
+ if indicator.weight != xml_indicator.get('weight'):
|
|
|
+ update_vals['weight'] = xml_indicator.get('weight')
|
|
|
+ if indicator.color_threshold_green != xml_indicator.get('color_threshold_green'):
|
|
|
+ update_vals['color_threshold_green'] = xml_indicator.get('color_threshold_green')
|
|
|
+ if indicator.color_threshold_yellow != xml_indicator.get('color_threshold_yellow'):
|
|
|
+ update_vals['color_threshold_yellow'] = xml_indicator.get('color_threshold_yellow')
|
|
|
+ if indicator.color_threshold_red != xml_indicator.get('color_threshold_red'):
|
|
|
+ update_vals['color_threshold_red'] = xml_indicator.get('color_threshold_red')
|
|
|
+
|
|
|
+ if update_vals:
|
|
|
+ indicator.write(update_vals)
|
|
|
+ _logger.info(f"✅ Actualizado indicador: {indicator.name}")
|
|
|
+ if 'formula' in update_vals:
|
|
|
+ _logger.info(f" Nueva fórmula: {update_vals['formula'][:50]}...")
|
|
|
+
|
|
|
+ # PASO 2: Verificar si el campo dinámico existe
|
|
|
+ field_name = self.env['hr.efficiency']._get_indicator_field_name(indicator.name)
|
|
|
+
|
|
|
+ # Buscar el campo manual existente
|
|
|
+ existing_field = self.env['ir.model.fields'].search([
|
|
|
+ ('model', '=', 'hr.efficiency'),
|
|
|
+ ('name', '=', field_name),
|
|
|
+ ('state', '=', 'manual'),
|
|
|
+ ], limit=1)
|
|
|
+
|
|
|
+ if existing_field:
|
|
|
+ # Actualizar campo existente si hay cambios
|
|
|
+ update_vals = {}
|
|
|
+ if existing_field.field_description != indicator.name:
|
|
|
+ update_vals['field_description'] = indicator.name
|
|
|
+ if existing_field.help != (indicator.description or indicator.name):
|
|
|
+ update_vals['help'] = indicator.description or indicator.name
|
|
|
+
|
|
|
+ if update_vals:
|
|
|
+ existing_field.write(update_vals)
|
|
|
+ fields_updated += 1
|
|
|
+ _logger.info(f"✅ Actualizado campo: {field_name}")
|
|
|
+ else:
|
|
|
+ # Crear nuevo campo dinámico
|
|
|
+ self._create_dynamic_field(indicator)
|
|
|
+ fields_created += 1
|
|
|
+ _logger.info(f"🆕 Creado campo: {field_name}")
|
|
|
+
|
|
|
+ indicators_processed += 1
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ _logger.error(f"❌ Error procesando indicador {indicator.name}: {e}")
|
|
|
+ continue
|
|
|
+
|
|
|
+ # Actualizar vistas con los campos dinámicos
|
|
|
+ self.env['hr.efficiency']._update_views_with_dynamic_fields()
|
|
|
+
|
|
|
+ # Recalcular todos los registros existentes
|
|
|
+ efficiency_records = self.env['hr.efficiency'].search([('active', '=', True)])
|
|
|
+ if efficiency_records:
|
|
|
+ efficiency_records._calculate_all_indicators()
|
|
|
+ _logger.info(f"🔄 Recalculados {len(efficiency_records)} registros de eficiencia")
|
|
|
+
|
|
|
+ # Reporte final
|
|
|
+ _logger.info("=" * 60)
|
|
|
+ _logger.info("📋 REPORTE DE SINCRONIZACIÓN COMPLETADO")
|
|
|
+ _logger.info("=" * 60)
|
|
|
+ _logger.info(f"📊 Indicadores procesados: {indicators_processed}")
|
|
|
+ _logger.info(f"🆕 Campos creados: {fields_created}")
|
|
|
+ _logger.info(f"✅ Campos actualizados: {fields_updated}")
|
|
|
+ _logger.info(f"🔄 Registros recalculados: {len(efficiency_records)}")
|
|
|
+ _logger.info("=" * 60)
|
|
|
+
|
|
|
+ return {
|
|
|
+ 'indicators_processed': indicators_processed,
|
|
|
+ 'fields_created': fields_created,
|
|
|
+ 'fields_updated': fields_updated,
|
|
|
+ 'records_recalculated': len(efficiency_records),
|
|
|
+ }
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ _logger.error(f"❌ Error crítico en sincronización: {e}")
|
|
|
+ raise
|