# Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import datetime, time, timedelta from pytz import timezone, UTC from odoo import api, fields, models, _ from odoo.tools import float_round from odoo.addons.resource.models.utils import sum_intervals, HOURS_PER_DAY from odoo.exceptions import UserError class Employee(models.Model): _inherit = 'hr.employee' def _get_employees_working_hours(self, employees, start_datetime, end_datetime): # find working hours for the given period of employees with working calendar # Note: convert date str into datetime object. Time will be 00:00:00 and 23:59:59 # respectively for date_start and date_stop, because we want the date_stop to be included. start_datetime = datetime.combine(fields.Date.from_string(start_datetime), time.min) end_datetime = datetime.combine(fields.Date.from_string(end_datetime), time.max) start_datetime = start_datetime.replace(tzinfo=UTC) end_datetime = end_datetime.replace(tzinfo=UTC) employees_work_days_data, _dummy = employees.sudo().resource_id._get_valid_work_intervals(start_datetime, end_datetime) return employees_work_days_data def _get_timesheet_manager_id_domain(self): group = self.env.ref('hr_timesheet.group_hr_timesheet_approver', raise_if_not_found=False) return [('groups_id', 'in', [group.id])] if group else [] timesheet_manager_id = fields.Many2one( 'res.users', string='Timesheet', compute='_compute_timesheet_manager', store=True, readonly=False, domain=_get_timesheet_manager_id_domain, help='Select the user responsible for approving "Timesheet" of this employee.\n' 'If empty, the approval is done by a Timesheets > Administrator or a Timesheets > User: all timesheets (as determined in the users settings).') last_validated_timesheet_date = fields.Date(groups="hr_timesheet.group_timesheet_manager") @api.depends('parent_id') def _compute_timesheet_manager(self): for employee in self: previous_manager = employee._origin.parent_id.user_id manager = employee.parent_id.user_id if manager and manager.has_group('hr_timesheet.group_hr_timesheet_approver') and (employee.timesheet_manager_id == previous_manager or not employee.timesheet_manager_id): employee.timesheet_manager_id = manager elif not employee.timesheet_manager_id: employee.timesheet_manager_id = False def get_timesheet_and_working_hours(self, date_start, date_stop): """ Get the difference between the supposed working hour (based on resource calendar) and the timesheeted hours, for the given period `date_start` - `date_stop` (inclusives). :param date_start: start date of the period to check (date string) :param date_stop: end date of the period to check (date string) :returns dict: a dict mapping the employee_id with his timesheeted and working hours for the given period. """ employees = self.filtered('resource_calendar_id') result = {i: dict(timesheet_hours=0.0, working_hours=0.0, date_start=date_start, date_stop=date_stop) for i in self.ids} if not employees: return result # find timesheeted hours of employees with working hours self.env.cr.execute(""" SELECT A.employee_id as employee_id, sum(A.unit_amount) as amount_sum FROM account_analytic_line A WHERE A.employee_id IN %s AND date >= %s AND date <= %s GROUP BY A.employee_id """, (tuple(employees.ids), date_start, date_stop)) for data_row in self.env.cr.dictfetchall(): result[data_row['employee_id']]['timesheet_hours'] = float_round(data_row['amount_sum'], 2) employees_work_days_data = self._get_employees_working_hours(employees, date_start, date_stop) for employee in employees: working_hours = sum_intervals(employees_work_days_data[employee.resource_id.id]) result[employee.id]['working_hours'] = float_round(working_hours, 2) return result @api.model def get_daily_working_hours(self, date_start, date_stop): result = {} # Change the type of the date from string to Date date_start_date = fields.Date.from_string(date_start) date_stop_date = min(fields.Date.from_string(date_stop), fields.Date.today()) # Compute the difference between the starting and ending date delta = date_stop_date - date_start_date # Get the current user current_employee = self.env.user.employee_id if not current_employee: return result # Collect the number of hours that an employee should work according to their schedule calendar = current_employee.resource_calendar_id or current_employee.company_id.resource_calendar_id datetime_min = timezone(self.env.user.tz or calendar.tz or 'UTC').localize(datetime.combine(date_start_date, time.min)).astimezone(UTC) datetime_max = timezone(self.env.user.tz or calendar.tz or 'UTC').localize(datetime.combine(date_stop_date, time.max)).astimezone(UTC) employee_work_days_data = calendar._work_intervals_batch( datetime_min, datetime_max, resources=current_employee.resource_id, compute_leaves=False ) working_hours = employee_work_days_data[current_employee.resource_id.id] for day_count in range(delta.days + 1): date = date_start_date + timedelta(days=day_count) if current_employee.resource_calendar_id: value = sum( (stop - start).total_seconds() / 3600 for start, stop, meta in working_hours if start.date() == date ) else: value = current_employee.company_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY result[fields.Date.to_string(date)] = value return result def _get_timesheets_and_working_hours_query(self): return """ SELECT aal.employee_id as employee_id, COALESCE(SUM(aal.unit_amount), 0) as worked_hours FROM account_analytic_line aal WHERE aal.employee_id IN %s AND date >= %s AND date <= %s GROUP BY aal.employee_id """ def get_timesheet_and_working_hours_for_employees(self, date_start, date_stop): """ Method called by the timesheet avatar widget on the frontend in gridview to get information about the hours employees have worked and should work. :param date_start: date start of the interval to search :param state_stop: date stop of the interval to search :return: Dictionary of dictionary for each employee id => number of units to work, what unit type are we using the number of worked units by the employees """ result = {} uom = str(self.env.company.timesheet_encode_uom_id.name).lower() hours_per_day_per_employee = {} employees_work_days_data = {} if self: employees_work_days_data = self._get_employees_working_hours(self, date_start, date_stop) for employee in self: units_to_work = sum_intervals(employees_work_days_data[employee.resource_id.id]) # Adjustments if we work with a different unit of measure if uom == 'days': calendar = employee.resource_calendar_id or employee.company_id.resource_calendar_id hours_per_day_per_employee[employee.id] = calendar.hours_per_day units_to_work = units_to_work / hours_per_day_per_employee[employee.id] rounding = len(str(self.env.company.timesheet_encode_uom_id.rounding).split('.')[1]) units_to_work = round(units_to_work, rounding) result[employee.id] = {'units_to_work': units_to_work, 'uom': uom, 'worked_hours': 0.0} query = self._get_timesheets_and_working_hours_query() self.env.cr.execute(query, (tuple(self.ids), date_start, date_stop)) for data_row in self.env.cr.dictfetchall(): worked_hours = data_row['worked_hours'] if uom == 'days': worked_hours /= hours_per_day_per_employee[data_row['employee_id']] rounding = len(str(self.env.company.timesheet_encode_uom_id.rounding).split('.')[1]) worked_hours = round(worked_hours, rounding) result[data_row['employee_id']]['worked_hours'] = worked_hours return result def _get_user_m2o_to_empty_on_archived_employees(self): return super()._get_user_m2o_to_empty_on_archived_employees() + ['timesheet_manager_id'] def action_timesheet_from_employee(self): action = super().action_timesheet_from_employee() action['context']['group_expand'] = True return action def get_last_validated_timesheet_date(self): if self.user_has_groups('hr_timesheet.group_timesheet_manager'): return {} if not self.user_has_groups('hr_timesheet.group_hr_timesheet_user'): raise UserError(_('You are not allowed to see timesheets.')) return { employee.id: employee.last_validated_timesheet_date for employee in self.sudo() } class HrEmployeePublic(models.Model): _inherit = 'hr.employee.public' timesheet_manager_id = fields.Many2one('res.users', string='Timesheet', help="User responsible of timesheet validation. Should be Timesheet Manager.")