| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836 |
- # -*- coding: utf-8 -*-
- # Part of Odoo. See LICENSE file for full copyright and licensing details.
- import logging
- 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
- _logger = logging.getLogger(__name__)
- 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, domain=[('employee_type', '=', 'employee')])
- 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)
- expected_hours_to_date = fields.Float('Expected Hours to Date', compute='_compute_expected_hours_to_date', store=True, help='Hours that should be registered based on planning until current date')
-
- # 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')
- overall_efficiency_display = fields.Char('Overall Efficiency Display', compute='_compute_overall_efficiency_display', store=True, help='Overall efficiency formatted for display with badge widget')
-
- 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', 'month_year', 'employee_id', 'employee_id.contract_id')
- def _compute_expected_hours_to_date(self):
- """
- Calculate expected hours to date based on available hours and working days
- """
- for record in self:
- if not record.month_year or not record.available_hours or not record.employee_id.contract_id:
- record.expected_hours_to_date = 0.0
- continue
-
- try:
- # Parse month_year (format: YYYY-MM)
- year, month = record.month_year.split('-')
- start_date = date(int(year), int(month), 1)
- end_date = (start_date + relativedelta(months=1)) - relativedelta(days=1)
-
- # Get current date
- current_date = date.today()
-
- # If current date is outside the month, use end of month
- if current_date > end_date:
- calculation_date = end_date
- elif current_date < start_date:
- calculation_date = start_date
- else:
- calculation_date = current_date
-
- # Calculate working days in the month
- total_working_days = self._count_working_days(start_date, end_date, record.employee_id)
-
- # Calculate working days until current date
- 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
-
- # Ensure we don't exceed available hours for the month
- expected_hours = min(expected_hours, record.available_hours)
-
- record.expected_hours_to_date = float_round(expected_hours, 2)
- else:
- record.expected_hours_to_date = 0.0
-
- except (ValueError, AttributeError) as e:
- record.expected_hours_to_date = 0.0
- @api.depends('overall_efficiency')
- def _compute_overall_efficiency_display(self):
- """
- Compute display value for overall efficiency that works with badge widget
- """
- for record in self:
- if record.overall_efficiency == 0:
- record.overall_efficiency_display = '0.00'
- else:
- record.overall_efficiency_display = f"{record.overall_efficiency:.2f}"
- @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,
- 'expected_hours_to_date': record.expected_hours_to_date,
- }
-
- # 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 - handle both computed and stored fields
- if field.name in record._fields:
- record[field.name] = float_round(indicator_value, 2)
- elif 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)
-
- # Update stored manual fields after computing
- self._update_stored_manual_fields()
-
- def _update_stored_manual_fields(self):
- """Update stored manual fields with computed values"""
- 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'),
- ('store', '=', True),
- ], order='id')
-
- # 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,
- 'expected_hours_to_date': record.expected_hours_to_date,
- }
-
- # Calculate and update stored manual fields
- 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 and field.name in record._fields:
- # Calculate indicator value using the indicator formula
- indicator_value = indicator.evaluate_formula(efficiency_data)
-
- # Update the stored field value
- record[field.name] = float_round(indicator_value, 2)
-
- @api.model
- def _recompute_all_indicators(self):
- """Recompute all indicator fields for all records"""
- records = self.search([])
- if records:
- records._compute_indicators()
-
- 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
-
- # Check if there's any data to calculate
- if (record.available_hours == 0 and
- record.planned_hours == 0 and
- record.actual_billable_hours == 0 and
- record.actual_non_billable_hours == 0):
- return 0.0
-
- total_weight = 0
- weighted_sum = 0
- valid_indicators = 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,
- 'expected_hours_to_date': record.expected_hours_to_date,
- }
-
- for indicator in indicators:
- if indicator.weight > 0:
- indicator_value = indicator.evaluate_formula(efficiency_data)
- # Count indicators with valid values (including 0 when it's a valid result)
- if indicator_value is not None:
- weighted_sum += indicator_value * indicator.weight
- total_weight += indicator.weight
- valid_indicators += 1
-
- # If no valid indicators or no total weight, return 0
- if total_weight <= 0 or valid_indicators == 0:
- return 0.0
-
- # Multiply by 100 to show as percentage
- return float_round((weighted_sum / total_weight) * 100, 2)
- @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())),
- # Removed state restriction to include all planning slots regardless of state
- ])
-
- 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 of type 'employee'
- employees = self.env['hr.employee'].search([
- ('active', '=', True),
- ('employee_type', '=', 'employee')
- ])
-
- 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'
- ]
-
- # Check basic fields
- for field in fields_to_compare:
- if abs(efficiency_data[field] - latest_record[field]) > 0.01: # Tolerance for floating point
- has_changes = True
- break
-
- # If no changes in basic fields, check dynamic indicators
- if not has_changes:
- # Get all active indicators
- active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)])
-
- for indicator in active_indicators:
- field_name = self._get_indicator_field_name(indicator.name)
-
- # Calculate current indicator value
- current_value = indicator.evaluate_formula(efficiency_data)
-
- # Get previous indicator value from record
- previous_value = getattr(latest_record, field_name, None)
-
- # Compare values with tolerance
- if (current_value is not None and previous_value is not None and
- abs(current_value - previous_value) > 0.01):
- has_changes = True
- break
- elif current_value != previous_value: # Handle None vs value cases
- 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 active indicators ordered by weight (descending)
- active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='weight desc')
-
- fields_to_display = []
-
- for indicator in active_indicators:
- field_name = self._get_indicator_field_name(indicator.name)
-
- # Check if this indicator has a manual field
- manual_field = self.env['ir.model.fields'].search([
- ('model', '=', 'hr.efficiency'),
- ('state', '=', 'manual'),
- ('ttype', '=', 'float'),
- ('name', '=', field_name),
- ], limit=1)
-
- if manual_field:
- # Indicator with manual field
- fields_to_display.append({
- 'name': field_name,
- 'field_description': indicator.name,
- 'indicator': indicator
- })
- else:
- # Create manual field for this indicator
- _logger.info(f"Creating manual field for indicator '{indicator.name}'")
- self.env['hr.efficiency.indicator']._create_dynamic_field(indicator)
- # Add to display list after creation
- fields_to_display.append({
- 'name': field_name,
- 'field_description': indicator.name,
- 'indicator': indicator
- })
-
- _logger.info(f"Found {len(fields_to_display)} fields to add to views")
-
- # Build dynamic fields XML for list view
- dynamic_fields_xml = ''
- for field_info in fields_to_display:
- field_xml = f'<field name="{field_info["name"]}" widget="percentage" optional="hide"'
-
- # Add decorations based on indicator thresholds
- indicator = field_info['indicator']
- 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"'
-
- 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:
- new_arch = f"""
- <xpath expr=\"//field[@name='expected_hours_to_date']\" position=\"after\">{dynamic_fields_xml}</xpath>
- """
- inherited_list_view.write({'arch': new_arch})
- _logger.info(f"Updated inherited list view with {len(fields_to_display)} dynamic fields")
-
- # Build dynamic fields XML for form view
- form_dynamic_fields_xml = ''
- for field_info in fields_to_display:
- field_xml = f'<field name="{field_info["name"]}" widget="badge"'
-
- # Add decorations based on indicator thresholds
- indicator = field_info['indicator']
- 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"'
-
- field_xml += '/>'
- 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:
- new_form_arch = f"""
- <xpath expr=\"//field[@name='overall_efficiency']\" position=\"before\">{form_dynamic_fields_xml}</xpath>
- """
- inherited_form_view.write({'arch': new_form_arch})
- _logger.info("Updated inherited form view with dynamic fields")
-
- 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()
- self._update_dynamic_filter_labels()
- @api.model
- def _update_dynamic_filter_labels(self):
- """
- Update dynamic filter labels based on current date
- """
- labels = self._get_dynamic_month_labels()
-
- # Update filter labels
- filter_mapping = {
- 'filter_two_months_ago': labels['two_months_ago'],
- 'filter_last_month': labels['last_month'],
- 'filter_current_month': labels['current_month'],
- 'filter_next_month': labels['next_month'],
- 'filter_two_months_ahead': labels['two_months_ahead'],
- }
-
- for filter_name, label in filter_mapping.items():
- try:
- filter_record = self.env.ref(f'hr_efficiency.{filter_name}', raise_if_not_found=False)
- if filter_record:
- filter_record.write({'name': label})
- except Exception as e:
- _logger.warning(f"Could not update filter {filter_name}: {str(e)}")
- @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
- @api.model
- def _get_dynamic_month_labels(self):
- """
- Get dynamic month labels for filters
- Returns a dictionary with month labels like "Mes 1", "Mes 2", "Mes 3 << actual", etc.
- """
- current_date = date.today()
- labels = {}
-
- # 2 months ago
- month_2_ago = current_date - relativedelta(months=2)
- labels['two_months_ago'] = f"Mes {month_2_ago.strftime('%m')}"
-
- # 1 month ago
- month_1_ago = current_date - relativedelta(months=1)
- labels['last_month'] = f"Mes {month_1_ago.strftime('%m')}"
-
- # Current month
- labels['current_month'] = f"Mes {current_date.strftime('%m')} << actual"
-
- # Next month
- month_1_ahead = current_date + relativedelta(months=1)
- labels['next_month'] = f"Mes {month_1_ahead.strftime('%m')}"
-
- # 2 months ahead
- month_2_ahead = current_date + relativedelta(months=2)
- labels['two_months_ahead'] = f"Mes {month_2_ahead.strftime('%m')}"
-
- return labels
- def _count_working_days(self, start_date, end_date, employee=None):
- """
- Count working days between two dates considering employee calendar
- """
- working_days = 0
- current_date = start_date
-
- while current_date <= end_date:
- # Check if it's a working day according to employee calendar
- if self._is_working_day(current_date, employee):
- working_days += 1
- current_date += relativedelta(days=1)
-
- return working_days
- def _is_working_day(self, check_date, employee=None):
- """
- Check if a date is a working day considering employee calendar
- """
- if not employee or not employee.resource_calendar_id:
- # Fallback to basic weekday check
- return check_date.weekday() < 5
-
- try:
- # Convert date to datetime for calendar check
- check_datetime = datetime.combine(check_date, datetime.min.time())
-
- # Check if the day is a working day according to employee calendar
- working_hours = employee.resource_calendar_id._list_work_time_per_day(
- check_datetime, check_datetime, compute_leaves=True
- )
-
- # If there are working hours, it's a working day
- return bool(working_hours)
-
- except Exception:
- # Fallback to basic weekday check if there's any error
- return check_date.weekday() < 5
|