hr_efficiency_indicator.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. import logging
  2. from odoo import models, fields, api, _
  3. from odoo.exceptions import ValidationError
  4. _logger = logging.getLogger(__name__)
  5. class HrEfficiencyIndicator(models.Model):
  6. _name = 'hr.efficiency.indicator'
  7. _description = 'Efficiency Indicator Configuration'
  8. _order = 'sequence, name'
  9. name = fields.Char('Indicator Name', required=True)
  10. sequence = fields.Integer('Sequence', default=10)
  11. active = fields.Boolean('Active', default=True)
  12. # Indicator type for widget selection
  13. indicator_type = fields.Selection([
  14. ('percentage', 'Percentage'),
  15. ('hours', 'Hours'),
  16. ('currency', 'Currency'),
  17. ('number', 'Number')
  18. ], string='Indicator Type', default='percentage', required=True,
  19. help='Type of indicator that determines how it will be displayed in views')
  20. # Formula configuration
  21. formula = fields.Text('Formula', required=True, help="""
  22. Use the following variables in your formula:
  23. - available_hours: Available hours for the employee
  24. - planned_hours: Total planned hours
  25. - planned_billable_hours: Planned hours on billable projects
  26. - planned_non_billable_hours: Planned hours on non-billable projects
  27. - actual_billable_hours: Actual hours worked on billable projects
  28. - actual_non_billable_hours: Actual hours worked on non-billable projects
  29. Examples:
  30. - (planned_hours / available_hours) * 100 # Planning efficiency
  31. - ((actual_billable_hours + actual_non_billable_hours) / planned_hours) * 100 # Time tracking efficiency
  32. """)
  33. # Target and thresholds
  34. target_percentage = fields.Float('Target %', default=90.0, help='Target percentage for this indicator')
  35. weight = fields.Float('Weight %', default=50.0, help='Weight of this indicator in the overall efficiency calculation')
  36. # Priority for column background color
  37. priority = fields.Selection([
  38. ('none', 'Sin Prioridad'),
  39. ('low', 'Baja (Verde)'),
  40. ('medium', 'Media (Amarillo)'),
  41. ('high', 'Alta (Rojo)')
  42. ], string='Prioridad', default='none', help='Prioridad del indicador que determina el color de fondo de la columna')
  43. # Display settings
  44. description = fields.Text('Description', help='Description of what this indicator measures')
  45. color_threshold_green = fields.Float('Green Threshold', default=90.0, help='Percentage above which to show green')
  46. color_threshold_yellow = fields.Float('Yellow Threshold', default=70.0, help='Percentage above which to show yellow')
  47. color_threshold_red = fields.Float('Red Threshold', default=50.0, help='Percentage above which to show red (below this shows danger)')
  48. # Computed field for priority background color
  49. priority_color = fields.Char('Priority Color', compute='_compute_priority_color', store=True, help='Color de fondo basado en la prioridad')
  50. @api.depends('priority')
  51. def _compute_priority_color(self):
  52. """Compute background color based on priority"""
  53. for record in self:
  54. if record.priority == 'low':
  55. record.priority_color = '#d4edda' # Verde claro
  56. elif record.priority == 'medium':
  57. record.priority_color = '#fff3cd' # Amarillo claro
  58. elif record.priority == 'high':
  59. record.priority_color = '#f8d7da' # Rojo claro
  60. else:
  61. record.priority_color = '' # Sin color (transparente)
  62. @api.constrains('weight')
  63. def _check_weight(self):
  64. for record in self:
  65. if record.weight < 0 or record.weight > 100:
  66. raise ValidationError(_('Weight must be between 0 and 100'))
  67. @api.constrains('target_percentage')
  68. def _check_target_percentage(self):
  69. for record in self:
  70. if record.target_percentage < 0 or record.target_percentage > 100:
  71. raise ValidationError(_('Target percentage must be between 0 and 100'))
  72. @api.model_create_multi
  73. def create(self, vals_list):
  74. """Create indicators and create dynamic fields"""
  75. records = super().create(vals_list)
  76. # Create dynamic fields for all indicators
  77. for record in records:
  78. self._create_dynamic_field(record)
  79. return records
  80. def write(self, vals):
  81. """Update indicator and update dynamic field"""
  82. result = super().write(vals)
  83. # Update dynamic field for this indicator
  84. for record in self:
  85. self._update_dynamic_field(record, vals)
  86. return result
  87. def unlink(self):
  88. """Delete indicator and delete dynamic field"""
  89. # FIRST: Mark indicators as inactive to exclude them from views
  90. self.write({'active': False})
  91. # SECOND: Update views to remove field references BEFORE deleting fields
  92. self.env['hr.efficiency']._update_views_with_dynamic_fields()
  93. # THEN: Delete associated dynamic fields and manual fields
  94. for record in self:
  95. dynamic_field = self.env['hr.efficiency.dynamic.field'].search([
  96. ('indicator_id', '=', record.id)
  97. ])
  98. if dynamic_field:
  99. dynamic_field.unlink()
  100. # Also remove the corresponding manual field in ir.model.fields
  101. efficiency_model = 'hr.efficiency'
  102. technical_name = self.env[efficiency_model]._get_indicator_field_name(record.name)
  103. imf = self.env['ir.model.fields'].search([
  104. ('model', '=', efficiency_model),
  105. ('name', '=', technical_name),
  106. ('state', '=', 'manual'),
  107. ], limit=1)
  108. if imf:
  109. imf.unlink()
  110. result = super().unlink()
  111. return result
  112. def _create_dynamic_field(self, indicator):
  113. """Create a dynamic field for the indicator"""
  114. # Create dynamic field record for all indicators
  115. existing_field = self.env['hr.efficiency.dynamic.field'].search([
  116. ('indicator_id', '=', indicator.id)
  117. ])
  118. if not existing_field:
  119. # Create dynamic field
  120. field_name = indicator.name.lower().replace(' ', '_').replace('-', '_')
  121. field_name = field_name.replace('í', 'i').replace('á', 'a').replace('é', 'e').replace('ó', 'o').replace('ú', 'u')
  122. field_name = field_name.replace('ñ', 'n')
  123. # Ensure it starts with a letter
  124. if not field_name[0].isalpha():
  125. field_name = 'indicator_' + field_name
  126. # Determine widget based on indicator type
  127. widget_map = {
  128. 'percentage': 'percentage',
  129. 'hours': 'float_time',
  130. 'currency': 'monetary',
  131. 'number': 'float'
  132. }
  133. widget = widget_map.get(indicator.indicator_type, 'percentage')
  134. self.env['hr.efficiency.dynamic.field'].create({
  135. 'name': field_name,
  136. 'label': indicator.name,
  137. 'indicator_id': indicator.id,
  138. 'sequence': indicator.sequence,
  139. 'active': indicator.active,
  140. 'show_in_list': True,
  141. 'show_in_form': True,
  142. 'show_in_search': False,
  143. 'widget': widget,
  144. 'decoration_success': indicator.color_threshold_green,
  145. 'decoration_warning': indicator.color_threshold_yellow,
  146. 'decoration_danger': indicator.color_threshold_red,
  147. 'priority_background_color': indicator.priority_color,
  148. })
  149. # Ensure an ir.model.fields manual field exists (Studio-like) for ALL indicators
  150. efficiency_model = 'hr.efficiency'
  151. model_rec = self.env['ir.model']._get(efficiency_model)
  152. # Compute the technical field name like Studio (x_ prefix)
  153. technical_name = self.env[efficiency_model]._get_indicator_field_name(indicator.name)
  154. imf = self.env['ir.model.fields'].search([
  155. ('model', '=', efficiency_model),
  156. ('name', '=', technical_name),
  157. ], limit=1)
  158. if not imf:
  159. self.env['ir.model.fields'].with_context(studio=True).create({
  160. 'name': technical_name,
  161. 'model': efficiency_model,
  162. 'model_id': model_rec.id,
  163. 'ttype': 'float',
  164. 'field_description': indicator.name,
  165. 'help': indicator.description or indicator.name,
  166. 'state': 'manual',
  167. 'store': True,
  168. 'compute': False,
  169. })
  170. else:
  171. # Keep label and help in sync
  172. update_vals = {}
  173. if imf.field_description != indicator.name:
  174. update_vals['field_description'] = indicator.name
  175. if imf.help != (indicator.description or indicator.name):
  176. update_vals['help'] = indicator.description or indicator.name
  177. if update_vals:
  178. imf.write(update_vals)
  179. # Update views to include the new field
  180. self.env['hr.efficiency']._update_views_with_dynamic_fields()
  181. # Recompute all indicators to populate the new stored field
  182. records = self.env['hr.efficiency'].search([])
  183. if records:
  184. records._calculate_all_indicators()
  185. def _update_dynamic_field(self, indicator, vals=None):
  186. """Update the dynamic field for the indicator"""
  187. dynamic_field = self.env['hr.efficiency.dynamic.field'].search([
  188. ('indicator_id', '=', indicator.id)
  189. ])
  190. if dynamic_field:
  191. # Determine widget based on indicator type
  192. widget_map = {
  193. 'percentage': 'percentage',
  194. 'hours': 'float_time',
  195. 'currency': 'monetary',
  196. 'number': 'float'
  197. }
  198. widget = widget_map.get(indicator.indicator_type, 'percentage')
  199. dynamic_field.write({
  200. 'label': indicator.name,
  201. 'active': indicator.active,
  202. 'widget': widget,
  203. 'decoration_success': indicator.color_threshold_green,
  204. 'decoration_warning': indicator.color_threshold_yellow,
  205. 'decoration_danger': indicator.color_threshold_red,
  206. 'priority_background_color': indicator.priority_color,
  207. })
  208. # Sync corresponding ir.model.fields label and refresh views
  209. efficiency_model = 'hr.efficiency'
  210. technical_name = self.env[efficiency_model]._get_indicator_field_name(indicator.name)
  211. imf = self.env['ir.model.fields'].search([
  212. ('model', '=', efficiency_model),
  213. ('name', '=', technical_name),
  214. ], limit=1)
  215. if imf:
  216. update_vals = {}
  217. if imf.field_description != indicator.name:
  218. update_vals['field_description'] = indicator.name
  219. if imf.help != (indicator.description or indicator.name):
  220. update_vals['help'] = indicator.description or indicator.name
  221. if update_vals:
  222. imf.write(update_vals)
  223. # Update views when active status, sequence, or bulk operations change
  224. if 'active' in vals or 'sequence' in vals or len(self) > 1:
  225. try:
  226. self.env['hr.efficiency']._update_views_with_dynamic_fields()
  227. except Exception as e:
  228. # Log the error but don't let it rollback the transaction
  229. import logging
  230. _logger = logging.getLogger(__name__)
  231. _logger.error(f"Error updating views after indicator change: {e}")
  232. # Continue with the transaction even if view update fails
  233. def evaluate_formula(self, efficiency_data):
  234. """
  235. Evaluate the formula using the provided efficiency data
  236. """
  237. try:
  238. # Create a safe environment for formula evaluation
  239. safe_dict = {
  240. 'available_hours': efficiency_data.get('available_hours', 0),
  241. 'planned_hours': efficiency_data.get('planned_hours', 0),
  242. 'planned_billable_hours': efficiency_data.get('planned_billable_hours', 0),
  243. 'planned_non_billable_hours': efficiency_data.get('planned_non_billable_hours', 0),
  244. 'actual_billable_hours': efficiency_data.get('actual_billable_hours', 0),
  245. 'actual_non_billable_hours': efficiency_data.get('actual_non_billable_hours', 0),
  246. 'total_actual_hours': efficiency_data.get('total_actual_hours', 0),
  247. 'expected_hours_to_date': efficiency_data.get('expected_hours_to_date', 0),
  248. 'wage': efficiency_data.get('wage', 0),
  249. 'wage_overhead': efficiency_data.get('wage_overhead', 0),
  250. 'utilization_rate': efficiency_data.get('utilization_rate', 100.0),
  251. 'precio_por_hora': efficiency_data.get('precio_por_hora', 0),
  252. }
  253. # Add math functions for safety
  254. import math
  255. safe_dict.update({
  256. 'abs': abs,
  257. 'min': min,
  258. 'max': max,
  259. 'round': round,
  260. })
  261. # Evaluate the formula
  262. result = eval(self.formula, {"__builtins__": {}}, safe_dict)
  263. return result
  264. except Exception as e:
  265. _logger.error(f"Error evaluating formula for indicator {self.name}: {e}")
  266. return 0.0
  267. def get_color_class(self, percentage):
  268. """
  269. Return the CSS color class based on the percentage
  270. """
  271. if percentage >= self.color_threshold_green:
  272. return 'text-success'
  273. elif percentage >= self.color_threshold_yellow:
  274. return 'text-warning'
  275. elif percentage >= self.color_threshold_red:
  276. return 'text-danger'
  277. else:
  278. return 'text-danger' # Below red threshold - critical
  279. def action_sync_from_xml(self):
  280. """
  281. Acción manual para forzar sincronización desde XML
  282. Útil para debugging y actualizaciones manuales
  283. """
  284. result = self.env['hr.efficiency.indicator'].sync_indicators_from_xml()
  285. # Mostrar mensaje al usuario
  286. message = f"""
  287. 🔄 Sincronización completada:
  288. 📊 Indicadores procesados: {result['indicators_processed']}
  289. 🆕 Campos creados: {result['fields_created']}
  290. ✅ Campos actualizados: {result['fields_updated']}
  291. 🔄 Registros recalculados: {result['records_recalculated']}
  292. """
  293. return {
  294. 'type': 'ir.actions.client',
  295. 'tag': 'display_notification',
  296. 'params': {
  297. 'title': 'Sincronización Completada',
  298. 'message': message,
  299. 'type': 'success',
  300. 'sticky': True,
  301. }
  302. }
  303. @api.model
  304. def _load_indicator_from_xml(self, indicator_name):
  305. """
  306. Cargar datos del indicador desde el XML
  307. """
  308. try:
  309. # Mapeo de indicadores con sus datos del XML
  310. xml_indicators = {
  311. 'Planning Coverage': {
  312. 'formula': '(planned_hours / available_hours) if available_hours > 0 else 0',
  313. 'sequence': 10,
  314. '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)',
  315. 'target_percentage': 95.0,
  316. 'weight': 0.0,
  317. 'color_threshold_green': 85.0,
  318. 'color_threshold_yellow': 75.0,
  319. 'color_threshold_red': 65.0,
  320. },
  321. 'Planned Utilization': {
  322. 'formula': '(planned_billable_hours / available_hours) if available_hours > 0 else 0',
  323. 'sequence': 20,
  324. '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)',
  325. 'target_percentage': 80.0,
  326. 'weight': 0.0,
  327. 'color_threshold_green': 85.0,
  328. 'color_threshold_yellow': 75.0,
  329. 'color_threshold_red': 60.0,
  330. },
  331. 'Break-Even Hours Needed': {
  332. 'formula': '(wage_overhead * (utilization_rate / 100)) / precio_por_hora if precio_por_hora > 0 else 0',
  333. 'sequence': 30,
  334. '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.',
  335. 'target_percentage': 0.0,
  336. 'weight': 0.0,
  337. 'color_threshold_green': 85.0,
  338. 'color_threshold_yellow': 75.0,
  339. 'color_threshold_red': 65.0,
  340. },
  341. 'Planned Profitability Coverage': {
  342. 'formula': '(planned_billable_hours / ((wage_overhead * (utilization_rate / 100)) / precio_por_hora)) if wage_overhead > 0 and precio_por_hora > 0 else 0',
  343. 'sequence': 40,
  344. '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)',
  345. 'target_percentage': 100.0,
  346. 'weight': 0.0,
  347. 'color_threshold_green': 100.0,
  348. 'color_threshold_yellow': 85.0,
  349. 'color_threshold_red': 70.0,
  350. },
  351. 'Estimation Accuracy Plan Adherence': {
  352. 'formula': '(total_actual_hours / planned_hours) if planned_hours > 0 else 0',
  353. 'sequence': 50,
  354. '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)',
  355. 'target_percentage': 100.0,
  356. 'weight': 0.0,
  357. 'color_threshold_green': 85.0,
  358. 'color_threshold_yellow': 75.0,
  359. 'color_threshold_red': 60.0,
  360. },
  361. 'Billable Plan Compliance': {
  362. 'formula': '(actual_billable_hours / planned_billable_hours) if planned_billable_hours > 0 else 0',
  363. 'sequence': 60,
  364. 'description': 'Cumplimiento del Plan Facturable: ¿Se cumplió con el objetivo específico de horas facturables? (Horas Facturables Registradas / Horas Facturables Planeadas)',
  365. 'target_percentage': 100.0,
  366. 'weight': 0.0,
  367. 'color_threshold_green': 85.0,
  368. 'color_threshold_yellow': 75.0,
  369. 'color_threshold_red': 65.0,
  370. },
  371. 'Occupancy Rate': {
  372. 'formula': '(total_actual_hours / available_hours) if available_hours > 0 else 0',
  373. 'sequence': 70,
  374. '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)',
  375. 'target_percentage': 95.0,
  376. 'weight': 0.0,
  377. 'color_threshold_green': 85.0,
  378. 'color_threshold_yellow': 75.0,
  379. 'color_threshold_red': 60.0,
  380. },
  381. 'Utilization Rate': {
  382. 'formula': '(actual_billable_hours / available_hours) if available_hours > 0 else 0',
  383. 'sequence': 80,
  384. 'description': 'Tasa de Utilización: Mide qué tan "productivo" (generando ingresos) está el equipo. (Horas Facturables Registradas / Horas Disponibles)',
  385. 'target_percentage': 80.0,
  386. 'weight': 0.0,
  387. 'color_threshold_green': 85.0,
  388. 'color_threshold_yellow': 75.0,
  389. 'color_threshold_red': 60.0,
  390. },
  391. 'Billability Rate': {
  392. 'formula': '(actual_billable_hours / total_actual_hours) if total_actual_hours > 0 else 0',
  393. 'sequence': 90,
  394. 'description': 'Tasa de Facturabilidad: De todo el tiempo trabajado, ¿qué porcentaje fue facturable? (Horas Facturables Registradas / Total Horas Registradas)',
  395. 'target_percentage': 85.0,
  396. 'weight': 0.0,
  397. 'color_threshold_green': 85.0,
  398. 'color_threshold_yellow': 75.0,
  399. 'color_threshold_red': 65.0,
  400. },
  401. 'Actual Profitability Achievement': {
  402. 'formula': '(actual_billable_hours / ((wage_overhead * (utilization_rate / 100)) / precio_por_hora)) if wage_overhead > 0 and precio_por_hora > 0 else 0',
  403. 'sequence': 100,
  404. '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)',
  405. 'target_percentage': 100.0,
  406. 'weight': 0.0,
  407. 'color_threshold_green': 100.0,
  408. 'color_threshold_yellow': 85.0,
  409. 'color_threshold_red': 70.0,
  410. },
  411. }
  412. return xml_indicators.get(indicator_name, {})
  413. except Exception as e:
  414. _logger.error(f"Error cargando indicador {indicator_name} desde XML: {e}")
  415. return {}
  416. @api.model
  417. def sync_indicators_from_xml(self):
  418. """
  419. Sincroniza los indicadores desde el XML con la base de datos
  420. Compara definiciones y actualiza campos dinámicos automáticamente
  421. """
  422. import logging
  423. _logger = logging.getLogger(__name__)
  424. try:
  425. _logger.info("🔄 Iniciando sincronización de indicadores desde XML...")
  426. # Obtener todos los indicadores activos ordenados por secuencia
  427. active_indicators = self.search([('active', '=', True)], order='sequence')
  428. _logger.info(f"📊 Encontrados {len(active_indicators)} indicadores activos")
  429. # Contadores para el reporte
  430. fields_created = 0
  431. fields_updated = 0
  432. indicators_processed = 0
  433. for indicator in active_indicators:
  434. try:
  435. # PASO 1: Actualizar el indicador desde XML (fórmulas, secuencias, etc.)
  436. # Cargar el indicador desde XML para obtener la versión correcta
  437. xml_indicator = self._load_indicator_from_xml(indicator.name)
  438. if xml_indicator:
  439. # Actualizar indicador con datos del XML
  440. update_vals = {}
  441. if indicator.formula != xml_indicator.get('formula'):
  442. update_vals['formula'] = xml_indicator.get('formula')
  443. if indicator.sequence != xml_indicator.get('sequence'):
  444. update_vals['sequence'] = xml_indicator.get('sequence')
  445. if indicator.description != xml_indicator.get('description'):
  446. update_vals['description'] = xml_indicator.get('description')
  447. if indicator.target_percentage != xml_indicator.get('target_percentage'):
  448. update_vals['target_percentage'] = xml_indicator.get('target_percentage')
  449. if indicator.weight != xml_indicator.get('weight'):
  450. update_vals['weight'] = xml_indicator.get('weight')
  451. if indicator.color_threshold_green != xml_indicator.get('color_threshold_green'):
  452. update_vals['color_threshold_green'] = xml_indicator.get('color_threshold_green')
  453. if indicator.color_threshold_yellow != xml_indicator.get('color_threshold_yellow'):
  454. update_vals['color_threshold_yellow'] = xml_indicator.get('color_threshold_yellow')
  455. if indicator.color_threshold_red != xml_indicator.get('color_threshold_red'):
  456. update_vals['color_threshold_red'] = xml_indicator.get('color_threshold_red')
  457. if update_vals:
  458. indicator.write(update_vals)
  459. _logger.info(f"✅ Actualizado indicador: {indicator.name}")
  460. if 'formula' in update_vals:
  461. _logger.info(f" Nueva fórmula: {update_vals['formula'][:50]}...")
  462. # PASO 2: Verificar si el campo dinámico existe
  463. field_name = self.env['hr.efficiency']._get_indicator_field_name(indicator.name)
  464. # Buscar el campo manual existente
  465. existing_field = self.env['ir.model.fields'].search([
  466. ('model', '=', 'hr.efficiency'),
  467. ('name', '=', field_name),
  468. ('state', '=', 'manual'),
  469. ], limit=1)
  470. if existing_field:
  471. # Actualizar campo existente si hay cambios
  472. update_vals = {}
  473. if existing_field.field_description != indicator.name:
  474. update_vals['field_description'] = indicator.name
  475. if existing_field.help != (indicator.description or indicator.name):
  476. update_vals['help'] = indicator.description or indicator.name
  477. if update_vals:
  478. existing_field.write(update_vals)
  479. fields_updated += 1
  480. _logger.info(f"✅ Actualizado campo: {field_name}")
  481. else:
  482. # Crear nuevo campo dinámico
  483. self._create_dynamic_field(indicator)
  484. fields_created += 1
  485. _logger.info(f"🆕 Creado campo: {field_name}")
  486. indicators_processed += 1
  487. except Exception as e:
  488. _logger.error(f"❌ Error procesando indicador {indicator.name}: {e}")
  489. continue
  490. # Actualizar vistas con los campos dinámicos
  491. self.env['hr.efficiency']._update_views_with_dynamic_fields()
  492. # Recalcular todos los registros existentes
  493. efficiency_records = self.env['hr.efficiency'].search([('active', '=', True)])
  494. if efficiency_records:
  495. efficiency_records._calculate_all_indicators()
  496. _logger.info(f"🔄 Recalculados {len(efficiency_records)} registros de eficiencia")
  497. # Reporte final
  498. _logger.info("=" * 60)
  499. _logger.info("📋 REPORTE DE SINCRONIZACIÓN COMPLETADO")
  500. _logger.info("=" * 60)
  501. _logger.info(f"📊 Indicadores procesados: {indicators_processed}")
  502. _logger.info(f"🆕 Campos creados: {fields_created}")
  503. _logger.info(f"✅ Campos actualizados: {fields_updated}")
  504. _logger.info(f"🔄 Registros recalculados: {len(efficiency_records)}")
  505. _logger.info("=" * 60)
  506. return {
  507. 'indicators_processed': indicators_processed,
  508. 'fields_created': fields_created,
  509. 'fields_updated': fields_updated,
  510. 'records_recalculated': len(efficiency_records),
  511. }
  512. except Exception as e:
  513. _logger.error(f"❌ Error crítico en sincronización: {e}")
  514. raise