|
|
@@ -38,6 +38,8 @@ class HrEfficiency(models.Model):
|
|
|
# Employee utilization and company overhead (stored at calculation time)
|
|
|
utilization_rate = fields.Float('Utilization Rate (%)', digits=(5, 2), default=100.0, aggregator='avg', help="Employee's utilization rate at the time of calculation")
|
|
|
overhead = fields.Float('Company Overhead (%)', digits=(5, 2), default=40.0, aggregator='avg', help="Company's overhead percentage at the time of calculation")
|
|
|
+ expected_profitability = fields.Float('Expected Profitability (%)', digits=(5, 2), default=30.0, aggregator='avg', help="Company's expected profitability percentage at the time of calculation")
|
|
|
+ efficiency_factor = fields.Float('Efficiency Factor (%)', digits=(5, 2), default=85.0, aggregator='avg', help="Company's efficiency factor percentage at the time of calculation")
|
|
|
|
|
|
# Planned hours (what was planned)
|
|
|
planned_hours = fields.Float('Planned Hours', digits=(10, 2), help="Total hours planned for the month")
|
|
|
@@ -53,7 +55,7 @@ class HrEfficiency(models.Model):
|
|
|
expected_hours_to_date = fields.Float('Expected Hours to Date', digits=(10, 2), help='Hours that should be registered based on planning until current date')
|
|
|
|
|
|
# Precio por Hora (stored field)
|
|
|
- precio_por_hora = fields.Float('Precio por Hora', digits=(10, 2), help='Precio que cobramos al cliente por hora (costo + overhead + 30% rentabilidad)', aggregator='avg')
|
|
|
+ precio_por_hora = fields.Float('Precio por Hora', digits=(10, 2), help='Precio que cobramos al cliente por hora (hourly_cost + overhead + rentabilidad_esperada)', aggregator='avg')
|
|
|
|
|
|
# Dynamic indicator fields (will be created automatically)
|
|
|
# These fields are managed dynamically based on hr.efficiency.indicator records
|
|
|
@@ -88,11 +90,11 @@ class HrEfficiency(models.Model):
|
|
|
|
|
|
def _calculate_expected_hours_to_date(self, record):
|
|
|
"""
|
|
|
- Calculate expected hours to date based on available hours and working days
|
|
|
+ Calculate expected hours to date based on planned hours and working days
|
|
|
"""
|
|
|
- if not record.month_year or not record.available_hours or not record.employee_id.contract_id:
|
|
|
+ if not record.month_year or not record.planned_hours or not record.employee_id.contract_id:
|
|
|
return 0.0
|
|
|
-
|
|
|
+
|
|
|
try:
|
|
|
# Parse month_year (format: YYYY-MM)
|
|
|
year, month = record.month_year.split('-')
|
|
|
@@ -117,11 +119,11 @@ class HrEfficiency(models.Model):
|
|
|
working_days_until_date = self._count_working_days(start_date, calculation_date, record.employee_id)
|
|
|
|
|
|
if total_working_days > 0:
|
|
|
- # Calculate expected hours based on available hours (what employee should work)
|
|
|
- expected_hours = (record.available_hours / total_working_days) * working_days_until_date
|
|
|
+ # Calculate expected hours based on planned hours (what was planned to work)
|
|
|
+ expected_hours = (record.planned_hours / total_working_days) * working_days_until_date
|
|
|
|
|
|
- # Ensure we don't exceed available hours for the month
|
|
|
- expected_hours = min(expected_hours, record.available_hours)
|
|
|
+ # Ensure we don't exceed planned hours for the month
|
|
|
+ expected_hours = min(expected_hours, record.planned_hours)
|
|
|
|
|
|
return float_round(expected_hours, 2)
|
|
|
else:
|
|
|
@@ -132,18 +134,21 @@ class HrEfficiency(models.Model):
|
|
|
|
|
|
def _calculate_precio_por_hora(self, record):
|
|
|
"""
|
|
|
- Calculate precio por hora: (wage / available_hours) * (1 + overhead/100) * 1.30
|
|
|
+ Calculate precio por hora: hourly_cost * (1 + overhead/100) * (1 + expected_profitability/100)
|
|
|
"""
|
|
|
try:
|
|
|
- wage = getattr(record, 'wage', 0.0) or 0.0
|
|
|
- available_hours = getattr(record, 'available_hours', 0.0) or 0.0
|
|
|
+ # Get hourly_cost from employee
|
|
|
+ employee = getattr(record, 'employee_id', None)
|
|
|
+ if not employee:
|
|
|
+ return 0.0
|
|
|
+
|
|
|
+ hourly_cost = getattr(employee, 'hourly_cost', 0.0) or 0.0
|
|
|
overhead = getattr(record, 'overhead', 40.0) or 40.0
|
|
|
+ expected_profitability = getattr(record, 'expected_profitability', 30.0) or 30.0
|
|
|
|
|
|
- if available_hours > 0:
|
|
|
- # Calculate cost per hour
|
|
|
- cost_per_hour = wage / available_hours
|
|
|
- # Apply overhead and 30% profit margin
|
|
|
- precio_por_hora = cost_per_hour * (1 + (overhead / 100)) * 1.30
|
|
|
+ if hourly_cost > 0:
|
|
|
+ # Apply overhead and expected profitability margin
|
|
|
+ precio_por_hora = hourly_cost * (1 + (overhead / 100)) * (1 + (expected_profitability / 100))
|
|
|
return float_round(precio_por_hora, 2)
|
|
|
else:
|
|
|
return 0.0
|
|
|
@@ -266,37 +271,9 @@ class HrEfficiency(models.Model):
|
|
|
list(values.values()) + [record_id]
|
|
|
)
|
|
|
|
|
|
- # Update aggregation fields after calculating indicators
|
|
|
- self._update_aggregation_fields()
|
|
|
+
|
|
|
|
|
|
- def _update_aggregation_fields(self):
|
|
|
- """
|
|
|
- Actualiza los campos de agregación con los valores de los indicadores dinámicos
|
|
|
- """
|
|
|
- try:
|
|
|
- for record in self:
|
|
|
- # Obtener todos los indicadores activos (en orden de secuencia)
|
|
|
- active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
|
|
|
-
|
|
|
- for indicator in active_indicators:
|
|
|
- field_name = self._get_indicator_field_name(indicator.name)
|
|
|
- agg_field_name = f"{field_name}_agg"
|
|
|
-
|
|
|
- # Verificar si el campo de agregación existe
|
|
|
- if hasattr(record, agg_field_name):
|
|
|
- # Obtener el valor del indicador dinámico
|
|
|
- indicator_value = getattr(record, field_name, 0.0) or 0.0
|
|
|
-
|
|
|
- # Actualizar el campo de agregación usando SQL directo
|
|
|
- record.env.cr.execute(
|
|
|
- "UPDATE hr_efficiency SET %s = %%s WHERE id = %%s" % agg_field_name,
|
|
|
- [indicator_value, record.id]
|
|
|
- )
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- import logging
|
|
|
- _logger = logging.getLogger(__name__)
|
|
|
- _logger.warning(f"Error updating aggregation fields: {str(e)}")
|
|
|
+
|
|
|
|
|
|
def _update_stored_manual_fields(self):
|
|
|
"""Update stored manual fields with computed values"""
|
|
|
@@ -496,6 +473,8 @@ class HrEfficiency(models.Model):
|
|
|
# Get employee's utilization rate and company's overhead at calculation time
|
|
|
utilization_rate = employee.utilization_rate or 100.0
|
|
|
overhead = employee.company_id.overhead or 40.0
|
|
|
+ expected_profitability = employee.company_id.expected_profitability or 30.0
|
|
|
+ efficiency_factor = employee.company_id.efficiency_factor or 85.0
|
|
|
|
|
|
# Apply utilization_rate to actual_billable_hours to reflect real billable capacity
|
|
|
adjusted_actual_billable_hours = actual_billable_hours * (utilization_rate / 100)
|
|
|
@@ -514,6 +493,8 @@ class HrEfficiency(models.Model):
|
|
|
'currency_id': currency_id,
|
|
|
'utilization_rate': utilization_rate,
|
|
|
'overhead': overhead,
|
|
|
+ 'expected_profitability': expected_profitability,
|
|
|
+ 'efficiency_factor': efficiency_factor,
|
|
|
}
|
|
|
|
|
|
@api.model
|
|
|
@@ -887,8 +868,7 @@ class HrEfficiency(models.Model):
|
|
|
"""
|
|
|
super()._register_hook()
|
|
|
try:
|
|
|
- # Ensure aggregation fields exist for dynamic indicators
|
|
|
- self._ensure_aggregation_fields_exist()
|
|
|
+
|
|
|
# Update views with current dynamic fields on every module load
|
|
|
self._update_views_with_dynamic_fields()
|
|
|
except Exception as e:
|
|
|
@@ -897,59 +877,7 @@ class HrEfficiency(models.Model):
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
_logger.warning(f"Could not update dynamic views on module load: {str(e)}")
|
|
|
|
|
|
- @api.model
|
|
|
- def _ensure_aggregation_fields_exist(self):
|
|
|
- """
|
|
|
- Asegura que existan campos de agregación para todos los indicadores activos
|
|
|
- """
|
|
|
- try:
|
|
|
- active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
|
|
|
-
|
|
|
- for indicator in active_indicators:
|
|
|
- field_name = self._get_indicator_field_name(indicator.name)
|
|
|
- agg_field_name = f"{field_name}_agg"
|
|
|
-
|
|
|
- # Verificar si el campo de agregación ya existe en ir.model.fields
|
|
|
- agg_field_record = self.env['ir.model.fields'].search([
|
|
|
- ('model', '=', 'hr.efficiency'),
|
|
|
- ('name', '=', agg_field_name)
|
|
|
- ], limit=1)
|
|
|
-
|
|
|
- if not agg_field_record:
|
|
|
- # Determinar tipo de agregación basado en el tipo de indicador
|
|
|
- agg_type = 'avg' if indicator.indicator_type == 'percentage' else 'sum'
|
|
|
-
|
|
|
- # Obtener el model_id para hr.efficiency
|
|
|
- model_record = self.env['ir.model'].search([('model', '=', 'hr.efficiency')], limit=1)
|
|
|
-
|
|
|
- if model_record:
|
|
|
- # Crear el campo de agregación
|
|
|
- self.env['ir.model.fields'].create({
|
|
|
- 'name': agg_field_name,
|
|
|
- 'model_id': model_record.id,
|
|
|
- 'field_description': f'{indicator.name} (Agregación)',
|
|
|
- 'ttype': 'float',
|
|
|
- 'state': 'manual',
|
|
|
- 'store': True,
|
|
|
- 'help': f'Campo de agregación para {indicator.name}'
|
|
|
- })
|
|
|
-
|
|
|
- import logging
|
|
|
- _logger = logging.getLogger(__name__)
|
|
|
- _logger.info(f"Created aggregation field: {agg_field_name} for indicator: {indicator.name}")
|
|
|
- else:
|
|
|
- import logging
|
|
|
- _logger = logging.getLogger(__name__)
|
|
|
- _logger.warning(f"Model hr.efficiency not found for aggregation field: {agg_field_name}")
|
|
|
-
|
|
|
- import logging
|
|
|
- _logger = logging.getLogger(__name__)
|
|
|
- _logger.info(f"Created aggregation field: {agg_field_name} for indicator: {indicator.name}")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- import logging
|
|
|
- _logger = logging.getLogger(__name__)
|
|
|
- _logger.warning(f"Error creating aggregation fields: {str(e)}")
|
|
|
+
|
|
|
|
|
|
@api.model
|
|
|
def _update_views_with_dynamic_fields(self):
|
|
|
@@ -1017,22 +945,27 @@ class HrEfficiency(models.Model):
|
|
|
|
|
|
# Add decorations based on indicator thresholds
|
|
|
if indicator:
|
|
|
- # Use standard efficiency ranges: >=0.9 green, >=0.8 yellow, >=0.7 orange, <0.7 red or zero
|
|
|
- field_xml += f' decoration-success="{field_info["name"]} >= 0.9"'
|
|
|
- field_xml += f' decoration-warning="{field_info["name"]} >= 0.8 and {field_info["name"]} < 0.9"'
|
|
|
- field_xml += f' decoration-info="{field_info["name"]} >= 0.7 and {field_info["name"]} < 0.8"'
|
|
|
- field_xml += f' decoration-danger="{field_info["name"]} < 0.7 or {field_info["name"]} == 0"'
|
|
|
+ # Use dynamic thresholds from indicator configuration
|
|
|
+ green_threshold = indicator.color_threshold_green / 100.0
|
|
|
+ yellow_threshold = indicator.color_threshold_yellow / 100.0
|
|
|
+
|
|
|
+ field_xml += f' decoration-success="{field_info["name"]} >= {green_threshold}"'
|
|
|
+ field_xml += f' decoration-warning="{field_info["name"]} >= {yellow_threshold} and {field_info["name"]} < {green_threshold}"'
|
|
|
+ field_xml += f' decoration-danger="{field_info["name"]} < {yellow_threshold}"'
|
|
|
+
|
|
|
+ # Add priority background color using CSS classes
|
|
|
+ if hasattr(indicator, 'priority') and indicator.priority != 'none':
|
|
|
+ if indicator.priority == 'low':
|
|
|
+ field_xml += f' class="priority-low-bg"'
|
|
|
+ elif indicator.priority == 'medium':
|
|
|
+ field_xml += f' class="priority-medium-bg"'
|
|
|
+ elif indicator.priority == 'high':
|
|
|
+ field_xml += f' class="priority-high-bg"'
|
|
|
|
|
|
field_xml += '/>'
|
|
|
dynamic_fields_xml += field_xml
|
|
|
|
|
|
- # Add aggregation field for this indicator
|
|
|
- agg_field_name = f"{field_info['name']}_agg"
|
|
|
- agg_type = 'avg' if indicator.indicator_type == 'percentage' else 'sum'
|
|
|
- agg_label = f"Total {indicator.name}" if agg_type == 'sum' else f"Average {indicator.name}"
|
|
|
-
|
|
|
- agg_field_xml = f'<field name="{agg_field_name}" {agg_type}="{agg_label}" optional="hide" invisible="1"/>'
|
|
|
- dynamic_fields_xml += agg_field_xml
|
|
|
+
|
|
|
|
|
|
# Update inherited list view
|
|
|
inherited_list_view = self.env.ref('hr_efficiency.view_hr_efficiency_list_inherited', raise_if_not_found=False)
|
|
|
@@ -1074,11 +1007,22 @@ class HrEfficiency(models.Model):
|
|
|
|
|
|
# Add decorations based on indicator thresholds
|
|
|
if indicator:
|
|
|
- # Use standard efficiency ranges: >=0.9 green, >=0.8 yellow, >=0.7 orange, <0.7 red or zero
|
|
|
- field_xml += f' decoration-success="{field_info["name"]} >= 0.9"'
|
|
|
- field_xml += f' decoration-warning="{field_info["name"]} >= 0.8 and {field_info["name"]} < 0.9"'
|
|
|
- field_xml += f' decoration-info="{field_info["name"]} >= 0.7 and {field_info["name"]} < 0.8"'
|
|
|
- field_xml += f' decoration-danger="{field_info["name"]} < 0.7 or {field_info["name"]} == 0"'
|
|
|
+ # Use dynamic thresholds from indicator configuration
|
|
|
+ green_threshold = indicator.color_threshold_green / 100.0
|
|
|
+ yellow_threshold = indicator.color_threshold_yellow / 100.0
|
|
|
+
|
|
|
+ field_xml += f' decoration-success="{field_info["name"]} >= {green_threshold}"'
|
|
|
+ field_xml += f' decoration-warning="{field_info["name"]} >= {yellow_threshold} and {field_info["name"]} < {green_threshold}"'
|
|
|
+ field_xml += f' decoration-danger="{field_info["name"]} < {yellow_threshold}"'
|
|
|
+
|
|
|
+ # Add priority background color using CSS classes
|
|
|
+ if hasattr(indicator, 'priority') and indicator.priority != 'none':
|
|
|
+ if indicator.priority == 'low':
|
|
|
+ field_xml += f' class="priority-low-bg"'
|
|
|
+ elif indicator.priority == 'medium':
|
|
|
+ field_xml += f' class="priority-medium-bg"'
|
|
|
+ elif indicator.priority == 'high':
|
|
|
+ field_xml += f' class="priority-high-bg"'
|
|
|
|
|
|
field_xml += '/>'
|
|
|
form_dynamic_fields_xml += field_xml
|