hr_efficiency_indicator.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. from odoo import models, fields, api, _
  2. from odoo.exceptions import ValidationError
  3. class HrEfficiencyIndicator(models.Model):
  4. _name = 'hr.efficiency.indicator'
  5. _description = 'Efficiency Indicator Configuration'
  6. _order = 'sequence, name'
  7. name = fields.Char('Indicator Name', required=True)
  8. sequence = fields.Integer('Sequence', default=10)
  9. active = fields.Boolean('Active', default=True)
  10. # Formula configuration
  11. formula = fields.Text('Formula', required=True, help="""
  12. Use the following variables in your formula:
  13. - available_hours: Available hours for the employee
  14. - planned_hours: Total planned hours
  15. - planned_billable_hours: Planned hours on billable projects
  16. - planned_non_billable_hours: Planned hours on non-billable projects
  17. - actual_billable_hours: Actual hours worked on billable projects
  18. - actual_non_billable_hours: Actual hours worked on non-billable projects
  19. Examples:
  20. - (planned_hours / available_hours) * 100 # Planning efficiency
  21. - ((actual_billable_hours + actual_non_billable_hours) / planned_hours) * 100 # Time tracking efficiency
  22. """)
  23. # Target and thresholds
  24. target_percentage = fields.Float('Target %', default=90.0, help='Target percentage for this indicator')
  25. weight = fields.Float('Weight %', default=50.0, help='Weight of this indicator in the overall efficiency calculation')
  26. # Display settings
  27. description = fields.Text('Description', help='Description of what this indicator measures')
  28. color_threshold_green = fields.Float('Green Threshold', default=90.0, help='Percentage above which to show green')
  29. color_threshold_yellow = fields.Float('Yellow Threshold', default=70.0, help='Percentage above which to show yellow')
  30. @api.constrains('weight')
  31. def _check_weight(self):
  32. for record in self:
  33. if record.weight < 0 or record.weight > 100:
  34. raise ValidationError(_('Weight must be between 0 and 100'))
  35. @api.constrains('target_percentage')
  36. def _check_target_percentage(self):
  37. for record in self:
  38. if record.target_percentage < 0 or record.target_percentage > 100:
  39. raise ValidationError(_('Target percentage must be between 0 and 100'))
  40. @api.model_create_multi
  41. def create(self, vals_list):
  42. """Create indicators and create dynamic fields"""
  43. records = super().create(vals_list)
  44. # Create dynamic fields for all indicators
  45. for record in records:
  46. self._create_dynamic_field(record)
  47. return records
  48. def write(self, vals):
  49. """Update indicator and update dynamic field"""
  50. result = super().write(vals)
  51. # Update dynamic field for this indicator
  52. for record in self:
  53. self._update_dynamic_field(record)
  54. return result
  55. def unlink(self):
  56. """Delete indicator and delete dynamic field"""
  57. # Delete associated dynamic fields
  58. for record in self:
  59. dynamic_field = self.env['hr.efficiency.dynamic.field'].search([
  60. ('indicator_id', '=', record.id)
  61. ])
  62. if dynamic_field:
  63. dynamic_field.unlink()
  64. result = super().unlink()
  65. return result
  66. def _create_dynamic_field(self, indicator):
  67. """Create a dynamic field for the indicator"""
  68. # Check if dynamic field already exists
  69. existing_field = self.env['hr.efficiency.dynamic.field'].search([
  70. ('indicator_id', '=', indicator.id)
  71. ])
  72. if not existing_field:
  73. # Create dynamic field
  74. field_name = indicator.name.lower().replace(' ', '_').replace('-', '_')
  75. field_name = field_name.replace('í', 'i').replace('á', 'a').replace('é', 'e').replace('ó', 'o').replace('ú', 'u')
  76. field_name = field_name.replace('ñ', 'n')
  77. # Ensure it starts with a letter
  78. if not field_name[0].isalpha():
  79. field_name = 'indicator_' + field_name
  80. self.env['hr.efficiency.dynamic.field'].create({
  81. 'name': field_name,
  82. 'label': indicator.name,
  83. 'indicator_id': indicator.id,
  84. 'sequence': indicator.sequence,
  85. 'active': indicator.active,
  86. 'show_in_list': True,
  87. 'show_in_form': True,
  88. 'show_in_search': False,
  89. 'widget': 'percentage',
  90. 'decoration_success': indicator.color_threshold_green,
  91. 'decoration_warning': indicator.color_threshold_yellow,
  92. 'decoration_danger': 0.0,
  93. })
  94. def _update_dynamic_field(self, indicator):
  95. """Update the dynamic field for the indicator"""
  96. dynamic_field = self.env['hr.efficiency.dynamic.field'].search([
  97. ('indicator_id', '=', indicator.id)
  98. ])
  99. if dynamic_field:
  100. dynamic_field.write({
  101. 'label': indicator.name,
  102. 'active': indicator.active,
  103. 'decoration_success': indicator.color_threshold_green,
  104. 'decoration_warning': indicator.color_threshold_yellow,
  105. })
  106. def evaluate_formula(self, efficiency_data):
  107. """
  108. Evaluate the formula using the provided efficiency data
  109. """
  110. try:
  111. # Create a safe environment for formula evaluation
  112. safe_dict = {
  113. 'available_hours': efficiency_data.get('available_hours', 0),
  114. 'planned_hours': efficiency_data.get('planned_hours', 0),
  115. 'planned_billable_hours': efficiency_data.get('planned_billable_hours', 0),
  116. 'planned_non_billable_hours': efficiency_data.get('planned_non_billable_hours', 0),
  117. 'actual_billable_hours': efficiency_data.get('actual_billable_hours', 0),
  118. 'actual_non_billable_hours': efficiency_data.get('actual_non_billable_hours', 0),
  119. }
  120. # Add math functions for safety
  121. import math
  122. safe_dict.update({
  123. 'abs': abs,
  124. 'min': min,
  125. 'max': max,
  126. 'round': round,
  127. })
  128. # Evaluate the formula
  129. result = eval(self.formula, {"__builtins__": {}}, safe_dict)
  130. return result
  131. except Exception as e:
  132. _logger.error(f"Error evaluating formula for indicator {self.name}: {e}")
  133. return 0.0
  134. def get_color_class(self, percentage):
  135. """
  136. Return the CSS color class based on the percentage
  137. """
  138. if percentage >= self.color_threshold_green:
  139. return 'text-success'
  140. elif percentage >= self.color_threshold_yellow:
  141. return 'text-warning'
  142. else:
  143. return 'text-danger'