217 lines
9.8 KiB
Python
217 lines
9.8 KiB
Python
|
# -*- 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)]
|
||
|
}
|