# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import pytz from dateutil.relativedelta import relativedelta from odoo import models, fields, api, exceptions, _ from odoo.tools import float_round class HrEmployee(models.Model): _inherit = "hr.employee" attendance_manager_id = fields.Many2one( 'res.users', store=True, readonly=False, domain="[('share', '=', False), ('company_ids', 'in', company_id)]", groups="hr_attendance.group_hr_attendance_manager", 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', groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") 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') overtime_ids = fields.One2many( 'hr.attendance.overtime', 'employee_id', groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") total_overtime = fields.Float( compute='_compute_total_overtime', compute_sudo=True) @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({'users': group_updates}) return super().create(vals_list) def write(self, values): old_officers = self.env['res.users'] if 'attendance_manager_id' in values: old_officers = self.attendance_manager_id # Officer was added if values['attendance_manager_id']: officer = self.env['res.users'].browse(values['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({'groups_id': [(4, officers_group.id)]}) res = super(HrEmployee, self).write(values) old_officers.sudo()._clean_attendance_officers() return res @api.depends('overtime_ids.duration', 'attendance_ids') def _compute_total_overtime(self): for employee in self: if employee.company_id.hr_attendance_overtime: employee.total_overtime = float_round(sum(employee.overtime_ids.mapped('duration')), 2) else: employee.total_overtime = 0 def _compute_hours_last_month(self): """ Compute hours in the current month, if we are the 15th of october, will compute hours 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) hours = sum( att.worked_hours or 0 for att in employee.attendance_ids.filtered( lambda att: att.check_in >= start_naive and att.check_out and att.check_out <= end_naive ) ) employee.hours_last_month = round(hours, 2) employee.hours_last_month_display = "%g" % employee.hours_last_month 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', '=', employee.id), ('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): for employee in self: employee.last_attendance_id = self.env['hr.attendance'].search([ ('employee_id', '=', employee.id), ], 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 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, "tree"]], "context": { "create": 0 }, "domain": [('employee_id', '=', self.id), ('check_in', ">=", fields.datetime.today().replace(day=1, hour=0, minute=0))] } def action_open_last_month_overtime(self): self.ensure_one() return { "type": "ir.actions.act_window", "name": _("Overtime"), "res_model": "hr.attendance.overtime", "views": [[False, "tree"]], "context": { "create": 0 }, "domain": [('employee_id', '=', self.id)] }