from datetime import timedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError from odoo.addons.resource.models.resource import HOURS_PER_DAY class HrTimesheetSheet(models.Model): _inherit = "hr_timesheet.sheet" employee_stats_ids = fields.One2many( "hr.employee.stats", "sheet_id", "Employee Stats" ) timesheet_sheet_gap_hours = fields.Float( "Timesheet sheet gap hours", compute="_compute_hours" ) timesheet_sheet_recovery_hours = fields.Float( "Timesheet sheet recovery hours", compute="_compute_hours" ) recovery_allocation_ids = fields.One2many( 'hr.leave.allocation', 'timesheet_sheet_id', string='Recovery Allocations', ) @api.depends("employee_stats_ids.gap_hours", "employee_id") def _compute_hours(self): for sheet in self: sheet.timesheet_sheet_gap_hours = sheet._get_timesheet_sheet_gap_hours() sheet.timesheet_sheet_recovery_hours = sheet._get_timesheet_sheet_recovery_hours() def _get_timesheet_sheet_gap_hours(self): for sheet in self: timesheet_sheet_gap_hours = sum( sheet.employee_stats_ids.filtered( lambda stat: stat.date <= fields.Date.today() ).mapped("gap_hours") ) return timesheet_sheet_gap_hours def _get_timesheet_sheet_recovery_hours(self): self.ensure_one() coef = self.env.company.coef # apply coef only if hours to recovery are positive and coef is existing if self.timesheet_sheet_gap_hours > 0 and coef > 0: timesheet_sheet_recovery_hours = self.timesheet_sheet_gap_hours + ( self.timesheet_sheet_gap_hours * coef / 100 ) else: timesheet_sheet_recovery_hours = self.timesheet_sheet_gap_hours return timesheet_sheet_recovery_hours def search_and_create_employee_stats(self): for sheet in self: if sheet.employee_id: for day in range((sheet.date_end - sheet.date_start).days + 1): date = sheet.date_start + timedelta(days=day) stats = self.env["hr.employee.stats"].search( [ ("employee_id", "=", sheet.employee_id.id), ("date", "=", date), ] ) if stats and not stats.sheet_id: stats.write({"sheet_id": sheet.id}) if not stats: self.env["hr.employee.stats"].create( { "employee_id": sheet.employee_id.id, "date": date, "sheet_id": sheet.id, "company_id": sheet.company_id.id, } ) return True @api.model def create(self, vals): res = super().create(vals) res.search_and_create_employee_stats() return res def write(self, vals): res = super().write(vals) if "date_end" in vals or "date_start" in vals or "employee_id" in vals: self.search_and_create_employee_stats() return res def unlink(self): for sheet in self: sheet.employee_stats_ids.unlink() return super().unlink() def action_timesheet_draft(self): res = super().action_timesheet_draft() for sheet in self: # set the state of the recovery allocation to refuse if the timesheet sheet is set to draft if sheet.state == "draft": sheet.recovery_allocation_ids.write({"state": "refuse"}) return res def _get_contracts_in_progress_during_timesheet_sheet_time_period(self): """ get the contracts which was in progress during the timesheet sheet range time """ contracts = self.env["hr.contract"].search( [ ("employee_id", "=", self.employee_id.id), ("state", "in", ("open", "close")), ("date_start", "<=", self.date_end), "|", ("date_end", "=", False), # pas de date de fin OU ("date_end", ">=", self.date_start), # date de fin après le début ], ) return contracts def _get_calendar_in_progress_during_timesheet_time_period(self): """ get the ressource calendar which was used during the timesheet sheet time period """ #checks if only one contract runs over the duration of the timesheet contracts = self._get_contracts_in_progress_during_timesheet_sheet_time_period() if len(contracts) > 1: # check if a new contract start during timesheet sheet time period. If yes, raise an error raise UserError( _("There is a contract starting during the timesheet sheet time period for the employee %s" "Please create a new timesheet sheet starting from the new contract start date") % self.employee_id.display_name ) # get the ressource calendar id according to the work contract elif self.employee_id.resource_calendar_id: return self.employee_id.resource_calendar_id elif self.env.company.resource_calendar_id: return self.env.company.resource_calendar_id return None def _get_working_hours_per_week(self): """ Get the weekly working hours for the employee, which is defined by: - the employee's work contract, - or their resource calendar, - or the company's resource calendar, - or the default value of 40 hours per week. :return: limit recovery hours """ # get ressource calendar id used during the timesheet sheet time period ressource_calendar_id = self._get_calendar_in_progress_during_timesheet_time_period() if ressource_calendar_id: resource_calendar_attendance_ids = self.env[ "resource.calendar.attendance" ].search([("calendar_id", "=", ressource_calendar_id.id)]) # calculate working hours per week according to the employee's resource calendar weekly_working_hours = 0 for day in resource_calendar_attendance_ids: weekly_working_hours += day.hour_to - day.hour_from return weekly_working_hours return HOURS_PER_DAY * 5 def _get_working_hours_per_day(self): """ Get the hours per day for the employee according to: - the employee's work contract, - or their resource calendar, - or the company's resource calendar, - or the default value of 8 hours per day. :return: hours per day """ # get ressource calendar id used during the timesheet sheet time period ressource_calendar_id = self._get_calendar_in_progress_during_timesheet_time_period() if ressource_calendar_id: return ressource_calendar_id.hours_per_day return HOURS_PER_DAY def _get_max_allowed_recovery_hours(self): """ Get the maximum number of hours beyond which new recovery allowances cannot be created """ return self._get_working_hours_per_week() def action_generate_recovery_allocation(self): # check if the user has the right to review the timesheet sheet self.ensure_one() self._check_can_review() recovery_type_id = self.env.company.recovery_type_id employee_id = self.employee_id if not employee_id or not recovery_type_id: raise UserError( _("Employe not defined for the timesheet sheet or recovery type not defined in settings") ) # check if allocation already exists for this timesheet sheet, if yes, refuse it allocations = ( self.env["hr.leave.allocation"] .sudo() .search( [ ("timesheet_sheet_id.id", "=", self.id), ("state", "!=", "refuse"), ] ) ) if allocations: allocations.write({"state": "refuse"}) # get recovery hours from total gap hours of the timesheet sheet recovery_hours = self._get_timesheet_sheet_recovery_hours() # get recovery hours cap max_allowed_recovery_hours = self._get_max_allowed_recovery_hours() if max_allowed_recovery_hours: # find recovery remaining leaves for the employee recovery_type_id = self.env.company.recovery_type_id # get virtual remaining leaves for the employee and the recovery leaves type data_days = recovery_type_id.get_employees_days([employee_id.id])[employee_id.id] total_recovery_type_leaves = data_days.get(recovery_type_id.id,{}) total_virtual_remaining_recovery_type_leaves = total_recovery_type_leaves.get('virtual_remaining_leaves', 0) # TODO vérifier si ça marche avec un recovery type dont l'unité de mesure est le jour # add the recovery hours to the total remaining leaves recovery type, and check if the limit of recovery hours is exceeded exceeded_hours = total_virtual_remaining_recovery_type_leaves + recovery_hours - max_allowed_recovery_hours # if limit recovery hours is exceeded, don't create a new allocation if exceeded_hours > 0: raise UserError( _( "The number of recovery hours exceeds the authorized limit (%s h) by %s hours" ) % (max_allowed_recovery_hours, exceeded_hours) ) # convert recovery hours into days recovery_days = recovery_hours / self._get_working_hours_per_day() # create an allocation (positive or negative) according to the total gap hours for the timesheet sheet range time new_alloc = self.env["hr.leave.allocation"].create( { "private_name": "Allocation : %s - %s" % (recovery_type_id.name, employee_id.display_name), "holiday_status_id": recovery_type_id.id, "employee_id": employee_id.id, "number_of_days": recovery_days, "timesheet_sheet_id": self.id, "allocation_type": 'accrual', } ) # set the allocation to validate or not if self.env.company.auto_validate_recovery_allocation: new_alloc.write({"state": "validate"}) return { "type": "ir.actions.act_window", "res_model": "hr.leave.allocation", "res_id": new_alloc.id, "view_mode": "form", "target": "current", "views": [ ( self.env.ref("hr_holidays.hr_leave_allocation_view_form").id, "form", ) ], }