# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging import pytz 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' # Basic fields name = fields.Char('Name', compute='_compute_display_name', store=True) employee_id = fields.Many2one('hr.employee', 'Employee', required=True, domain=[('employee_type', '=', 'employee')]) month_year = fields.Char('Month Year', required=True, help="Format: YYYY-MM (e.g., 2024-08)") date = fields.Date('Date', compute='_compute_date', store=True, help="Date field for standard Odoo date filters") company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company) active = fields.Boolean('Active', default=True) calculation_date = fields.Datetime('Calculation Date', default=fields.Datetime.now, help='When this calculation was performed') # Available hours (what employee should work) available_hours = fields.Float('Available Hours', digits=(10, 2), help="Total hours employee should work in the month") # Planned hours (what was planned) planned_hours = fields.Float('Planned Hours', digits=(10, 2), help="Total hours planned for the month") 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('month_year') def _compute_date(self): """ Compute date field from month_year for standard Odoo date filters """ for record in self: if record.month_year: try: year, month = record.month_year.split('-') record.date = date(int(year), int(month), 1) except (ValueError, AttributeError): record.date = False else: record.date = False @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 that overlap with the date range # This is the same logic as Odoo Planning's Gantt chart start_datetime = datetime.combine(start_date, datetime.min.time()) end_datetime = datetime.combine(end_date, datetime.max.time()) planning_slots = self.env['planning.slot'].search([ ('employee_id', '=', employee.id), ('start_datetime', '<=', end_datetime), ('end_datetime', '>=', start_datetime), # Removed state restriction to include all planning slots regardless of state ]) total_planned = 0.0 total_billable = 0.0 total_non_billable = 0.0 # Get working intervals for the resource and company calendar # This is the same approach as Odoo Planning's Gantt chart start_utc = pytz.utc.localize(start_datetime) end_utc = pytz.utc.localize(end_datetime) if employee.resource_id: resource_work_intervals, calendar_work_intervals = employee.resource_id._get_valid_work_intervals( start_utc, end_utc, calendars=employee.company_id.resource_calendar_id ) else: # Fallback to company calendar if no resource calendar_work_intervals = {employee.company_id.resource_calendar_id.id: []} resource_work_intervals = {} for slot in planning_slots: # Use the same logic as Odoo Planning's Gantt chart # Calculate duration only within the specified period hours = slot._get_duration_over_period( start_utc, end_utc, resource_work_intervals, calendar_work_intervals, has_allocated_hours=False ) # 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 _post_init_hook(self): """ Post-install hook to ensure dynamic fields are created for existing indicators """ import logging _logger = logging.getLogger(__name__) try: _logger.info("Running post-install hook for hr_efficiency module") # Ensure all active indicators have manual fields active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)]) for indicator in active_indicators: self.env['hr.efficiency.indicator']._create_dynamic_field(indicator) # Update views with dynamic fields self._update_views_with_dynamic_fields() _logger.info(f"Post-install hook completed. Processed {len(active_indicators)} indicators") except Exception as e: _logger.error(f"Error in post-install hook: {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'=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""" {dynamic_fields_xml} """ 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'=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""" {form_dynamic_fields_xml} """ 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