# Part of Odoo. See LICENSE file for full copyright and licensing details. import pytz from dateutil.relativedelta import relativedelta from collections import defaultdict from odoo.tools.intervals import Intervals from odoo import models, fields, api, exceptions, _ class HrEmployee(models.Model): _inherit = "hr.employee" attendance_manager_id = fields.Many2one( 'res.users', store=True, readonly=False, string="Attendance Approver", domain="[('share', '=', False), ('company_ids', 'in', company_id)]", groups="hr_attendance.group_hr_attendance_officer", help="The user set in Attendance will access the attendance of the employee through the dedicated app and will be able to edit them.") attendance_ids = fields.One2many( 'hr.attendance', 'employee_id', groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") last_attendance_id = fields.Many2one( 'hr.attendance', compute='_compute_last_attendance_id', store=True, groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") last_check_in = fields.Datetime( related='last_attendance_id.check_in', store=True, groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user", tracking=False) last_check_out = fields.Datetime( related='last_attendance_id.check_out', store=True, groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user", tracking=False) attendance_state = fields.Selection( string="Attendance Status", compute='_compute_attendance_state', selection=[('checked_out', "Checked out"), ('checked_in', "Checked in")], groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") hours_last_month = fields.Float(compute='_compute_hours_last_month') hours_last_month_overtime = fields.Float(compute='_compute_hours_last_month') hours_today = fields.Float( compute='_compute_hours_today', groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") hours_previously_today = fields.Float( compute='_compute_hours_today', groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") last_attendance_worked_hours = fields.Float( compute='_compute_hours_today', groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") hours_last_month_display = fields.Char( compute='_compute_hours_last_month', groups="hr.group_hr_user") overtime_ids = fields.One2many( 'hr.attendance.overtime.line', 'employee_id', groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") total_overtime = fields.Float(compute='_compute_total_overtime', compute_sudo=True) display_extra_hours = fields.Boolean(related='company_id.hr_attendance_display_overtime') ruleset_id = fields.Many2one(readonly=False, related="version_id.ruleset_id", inherited=True, groups="hr.group_hr_manager") @api.model_create_multi def create(self, vals_list): officer_group = self.env.ref('hr_attendance.group_hr_attendance_officer', raise_if_not_found=False) group_updates = [] for vals in vals_list: if officer_group and vals.get('attendance_manager_id'): group_updates.append((4, vals['attendance_manager_id'])) if group_updates: officer_group.sudo().write({'user_ids': group_updates}) return super().create(vals_list) def write(self, vals): old_officers = self.env['res.users'] if 'attendance_manager_id' in vals: old_officers = self.attendance_manager_id # Officer was added if vals['attendance_manager_id']: officer = self.env['res.users'].browse(vals['attendance_manager_id']) officers_group = self.env.ref('hr_attendance.group_hr_attendance_officer', raise_if_not_found=False) if officers_group and not officer.has_group('hr_attendance.group_hr_attendance_officer'): officer.sudo().write({'group_ids': [(4, officers_group.id)]}) res = super().write(vals) old_officers.sudo()._clean_attendance_officers() return res @api.depends('overtime_ids.manual_duration', 'overtime_ids', 'overtime_ids.status') def _compute_total_overtime(self): mapped_validated_overtimes = dict( self.env['hr.attendance.overtime.line']._read_group( domain=[('status', '=', 'approved')], groupby=['employee_id'], aggregates=['manual_duration:sum'] )) for employee in self: employee.total_overtime = mapped_validated_overtimes.get(employee, 0) def _compute_hours_last_month(self): """ Compute hours and overtime hours in the current month, if we are the 15th of october, will compute from 1 oct to 15 oct """ now = fields.Datetime.now() now_utc = pytz.utc.localize(now) for employee in self: tz = pytz.timezone(employee.tz or 'UTC') now_tz = now_utc.astimezone(tz) start_tz = now_tz.replace(day=1, hour=0, minute=0, second=0, microsecond=0) start_naive = start_tz.astimezone(pytz.utc).replace(tzinfo=None) end_tz = now_tz end_naive = end_tz.astimezone(pytz.utc).replace(tzinfo=None) current_month_attendances = employee.attendance_ids.filtered( lambda att: att.check_in >= start_naive and att.check_out and att.check_out <= end_naive ) hours = 0 overtime_hours = 0 for att in current_month_attendances: hours += att.worked_hours or 0 overtime_hours += att.validated_overtime_hours or 0 employee.hours_last_month = round(hours, 2) employee.hours_last_month_display = "%g" % employee.hours_last_month # overtime_adjustments = sum( # ot.duration or 0 # for ot in employee.overtime_ids.filtered( # lambda ot: ot.date >= start_tz.date() and ot.date <= end_tz.date() and ot.adjustment # ) # ) employee.hours_last_month_overtime = round(overtime_hours, 2) def _compute_hours_today(self): now = fields.Datetime.now() now_utc = pytz.utc.localize(now) for employee in self: # start of day in the employee's timezone might be the previous day in utc tz = pytz.timezone(employee.tz) now_tz = now_utc.astimezone(tz) start_tz = now_tz + relativedelta(hour=0, minute=0) # day start in the employee's timezone start_naive = start_tz.astimezone(pytz.utc).replace(tzinfo=None) attendances = self.env['hr.attendance'].search([ ('employee_id', 'in', employee.ids), ('check_in', '<=', now), '|', ('check_out', '>=', start_naive), ('check_out', '=', False), ], order='check_in asc') hours_previously_today = 0 worked_hours = 0 attendance_worked_hours = 0 for attendance in attendances: delta = (attendance.check_out or now) - max(attendance.check_in, start_naive) attendance_worked_hours = delta.total_seconds() / 3600.0 worked_hours += attendance_worked_hours hours_previously_today += attendance_worked_hours employee.last_attendance_worked_hours = attendance_worked_hours hours_previously_today -= attendance_worked_hours employee.hours_previously_today = hours_previously_today employee.hours_today = worked_hours @api.depends('attendance_ids') def _compute_last_attendance_id(self): current_datetime = fields.Datetime.now() for employee in self: employee.last_attendance_id = self.env['hr.attendance'].search([ ('employee_id', 'in', employee.ids), ('check_in', '<=', current_datetime), ], order="check_in desc", limit=1) @api.depends('last_attendance_id.check_in', 'last_attendance_id.check_out', 'last_attendance_id') def _compute_attendance_state(self): for employee in self: att = employee.last_attendance_id.sudo() employee.attendance_state = att and not att.check_out and 'checked_in' or 'checked_out' def _attendance_action_change(self, geo_information=None): """ Check In/Check Out action Check In: create a new attendance record Check Out: modify check_out field of appropriate attendance record """ self.ensure_one() action_date = fields.Datetime.now() if self.attendance_state != 'checked_in': if geo_information: vals = { 'employee_id': self.id, 'check_in': action_date, **{'in_%s' % key: geo_information[key] for key in geo_information} } else: vals = { 'employee_id': self.id, 'check_in': action_date, } return self.env['hr.attendance'].create(vals) attendance = self.env['hr.attendance'].search([('employee_id', '=', self.id), ('check_out', '=', False)], limit=1) if attendance: if geo_information: attendance.write({ 'check_out': action_date, **{'out_%s' % key: geo_information[key] for key in geo_information} }) else: attendance.write({ 'check_out': action_date }) else: raise exceptions.UserError(_( 'Cannot perform check out on %(empl_name)s, could not find corresponding check in. ' 'Your attendances have probably been modified manually by human resources.', empl_name=self.sudo().name)) return attendance @api.model def get_overtime_data(self, domain=None, employee_id=None): domain = [] if domain is None else domain validated_overtime = { attendance[0].id: attendance[1] for attendance in self.env["hr.attendance"]._read_group( domain=domain, groupby=['employee_id'], aggregates=['validated_overtime_hours:sum'] ) } return {"validated_overtime": validated_overtime, "overtime_adjustments": {}} def action_open_last_month_attendances(self): self.ensure_one() return { "type": "ir.actions.act_window", "name": _("Attendances This Month"), "res_model": "hr.attendance", "views": [[self.env.ref('hr_attendance.hr_attendance_employee_simple_tree_view').id, "list"]], "context": { "create": 0, "search_default_check_in_filter": 1, "employee_id": self.id, "display_extra_hours": self.display_extra_hours, }, "domain": [('employee_id', '=', self.id)] } @api.depends("user_id.im_status", "attendance_state") def _compute_presence_state(self): """ Override to include checkin/checkout in the presence state Attendance has the second highest priority after login """ super()._compute_presence_state() employees = self.filtered(lambda e: e.hr_presence_state != "present") employee_to_check_working = self.filtered(lambda e: e.sudo().attendance_state == "checked_out" and e.hr_presence_state == "out_of_working_hour") working_now_list = employee_to_check_working._get_employee_working_now() for employee in employees: if employee.sudo().attendance_state == "checked_out" and employee.hr_presence_state == "out_of_working_hour" and \ employee.id in working_now_list: employee.hr_presence_state = "absent" elif employee.sudo().attendance_state == "checked_in": employee.hr_presence_state = "present" def _compute_presence_icon(self): res = super()._compute_presence_icon() # All employee must chek in or check out. Everybody must have an icon for employee in self: employee.show_hr_icon_display = employee.company_id.hr_presence_control_attendance or bool(employee.user_id) return res def open_barcode_scanner(self): return { "type": "ir.actions.client", "tag": "employee_barcode_scanner", "name": "Badge Scanner" } def _get_schedules_by_employee_by_work_type(self, start, stop, version_periods_by_employee): employees_by_calendar = defaultdict(lambda: self.env['hr.employee']) leave_intervals_by_cal_by_resource = defaultdict(lambda: defaultdict(Intervals)) attendance_intervals_by_cal = defaultdict(Intervals) lunch_intervals_by_cal = defaultdict(Intervals) for employee, intervals in version_periods_by_employee.items(): for (_start, _stop, version) in intervals: employees_by_calendar[version.resource_calendar_id] |= employee for cal, employees in employees_by_calendar.items(): if not cal: # employees are fully flex continue cal_leave_intervals_by_resource = cal._leave_intervals_batch( start, stop, resources=employees.resource_id, ) for resource, leave_intervals in cal_leave_intervals_by_resource.items(): naive_leave_intervals = Intervals([( i_start.replace(tzinfo=None), i_stop.replace(tzinfo=None), i_model ) for (i_start, i_stop, i_model) in leave_intervals]) leave_intervals_by_cal_by_resource[cal][resource] = naive_leave_intervals cal_attendance_intervals = cal._attendance_intervals_batch( start, stop, )[False] attendance_intervals_by_cal[cal] = Intervals([( i_start.replace(tzinfo=None), i_stop.replace(tzinfo=None), i_model ) for (i_start, i_stop, i_model) in cal_attendance_intervals]) cal_lunch_intervals = cal._attendance_intervals_batch( start, stop, lunch=True )[False] lunch_intervals_by_cal[cal] = Intervals([( i_start.replace(tzinfo=None), i_stop.replace(tzinfo=None), i_model ) for (i_start, i_stop, i_model) in cal_lunch_intervals]) full_schedule_by_employee = { 'leave': defaultdict(Intervals), 'schedule': defaultdict(lambda: { 'work': Intervals([]), 'lunch': Intervals([]), }), 'fully_flexible': defaultdict(Intervals) } for employee, intervals in version_periods_by_employee.items(): for (p_start, p_stop, version) in intervals: interval = Intervals([(p_start.replace(tzinfo=None), p_stop.replace(tzinfo=None), self.env['resource.calendar'])]) calendar = version.resource_calendar_id if not calendar: full_schedule_by_employee['fully_flexible'][employee] |= interval continue employee_leaves = leave_intervals_by_cal_by_resource[calendar][employee.resource_id.id] full_schedule_by_employee['leave'][employee] |= employee_leaves & interval employee_attendances = attendance_intervals_by_cal[calendar] full_schedule_by_employee['schedule'][employee]['work'] |= employee_attendances & interval employee_lunches = lunch_intervals_by_cal[calendar] full_schedule_by_employee['schedule'][employee]['lunch'] |= employee_lunches & interval return full_schedule_by_employee