[IMP] hr_attendance: compute overtime on attendances

This commit adds a new model `hr.attendance.overtime`.

When an employee checks out, an overtime entry will be created when:
    - the option is enabled on the company;
    - the employee worked more or less than the number of hours they
    were supposed to work that day.

TaskID: 1904850
This commit is contained in:
Kevin Baptiste
2021-07-06 11:35:01 +02:00
committed by William Braeckman
parent 731a173d49
commit 9e6d2da504
28 changed files with 942 additions and 98 deletions
+15
View File
@@ -395,6 +395,21 @@ class HrEmployeePrivate(models.Model):
except AccessError:
employee.is_address_home_a_company = False
def _get_tz(self):
# Finds the first valid timezone in his tz, his work hours tz,
# the company calendar tz or UTC and returns it as a string
self.ensure_one()
return self.tz or\
self.resource_calendar_id.tz or\
self.company_id.resource_calendar_id.tz or\
'UTC'
def _get_tz_batch(self):
# Finds the first valid timezone in his tz, his work hours tz,
# the company calendar tz or UTC
# Returns a dict {employee_id: tz}
return {emp.id: emp._get_tz() for emp in self}
# ---------------------------------------------------------
# Business Methods
# ---------------------------------------------------------
+1
View File
@@ -3,3 +3,4 @@
from . import controllers
from . import models
from . import report
+2
View File
@@ -21,6 +21,8 @@ actions(Check in/Check out) performed by them.
'security/hr_attendance_security.xml',
'security/ir.model.access.csv',
'views/hr_attendance_view.xml',
'views/hr_attendance_overtime_view.xml',
'report/hr_attendance_report_views.xml',
'views/hr_department_view.xml',
'views/hr_employee_view.xml',
'views/res_config_settings_views.xml',
@@ -16,74 +16,74 @@
</record>
<record id="attendance_root1" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 08:00:00')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 12:00:00')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 08:00:24')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 12:01:33')" name="check_out"/>
<field name="employee_id" ref="hr.employee_admin"/>
</record>
<record id="attendance_root2" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 13:00:00')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 18:00:00')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 13:02:58')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 18:09:22')" name="check_out"/>
<field name="employee_id" ref="hr.employee_admin"/>
</record>
<record id="attendance1" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-01 08:21')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-01 11:51')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-01 15:51')" name="check_out"/>
<field name="employee_id" ref="hr.employee_qdp"/>
</record>
<record id="attendance2" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-02 12:47')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-02 19:53')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-02 08:47')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-02 15:53')" name="check_out"/>
<field name="employee_id" ref="hr.employee_qdp"/>
</record>
<record id="attendance3" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-03 07:32')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-03 12:32')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-03 08:32')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-03 15:22')" name="check_out"/>
<field name="employee_id" ref="hr.employee_qdp"/>
</record>
<record id="attendance4" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-04 14:01')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-04 17:21')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-04 08:01')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-04 16:21')" name="check_out"/>
<field name="employee_id" ref="hr.employee_qdp"/>
</record>
<record id="attendance5" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-05 09:10')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-05 12:42')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-05 10:10')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-05 14:42')" name="check_out"/>
<field name="employee_id" ref="hr.employee_qdp"/>
</record>
<record id="attendance6" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-06 13:10')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-06 18:34')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-06 10:10')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-06 17:34')" name="check_out"/>
<field name="employee_id" ref="hr.employee_qdp"/>
</record>
<record id="attendance7" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-07 08:21')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-07 18:21')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-07 17:29')" name="check_out"/>
<field name="employee_id" ref="hr.employee_qdp"/>
</record>
<record id="attendance8" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-08 08:21')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-08 12:54')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-08 09:21')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-08 14:54')" name="check_out"/>
<field name="employee_id" ref="hr.employee_qdp"/>
</record>
<record id="attendance9" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-09 13:32')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-09 19:31')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-09 10:32')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-09 17:31')" name="check_out"/>
<field name="employee_id" ref="hr.employee_qdp"/>
</record>
<record id="attendance10" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-10 08:00')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-10 09:00')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1)).strftime('%Y-%m-10 17:00')" name="check_out"/>
<field name="employee_id" ref="hr.employee_qdp"/>
</record>
</data>
+2
View File
@@ -2,6 +2,8 @@
from . import res_config_settings
from . import hr_attendance
from . import hr_attendance_overtime
from . import hr_employee
from . import ir_ui_menu
from . import res_company
from . import res_users
@@ -1,8 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from datetime import datetime, timedelta
from operator import itemgetter
import pytz
from odoo import models, fields, api, exceptions, _
from odoo.tools import format_datetime
from odoo.osv.expression import AND, OR
from odoo.tools.float_utils import float_is_zero
class HrAttendance(models.Model):
@@ -99,6 +106,192 @@ class HrAttendance(models.Model):
'datetime': format_datetime(self.env, last_attendance_before_check_out.check_in, dt_format=False),
})
@api.model
def _get_day_start_and_day(self, employee, dt):
#Returns a tuple containing the datetime in naive UTC of the employee's start of the day
# and the date it was for that employee
if not dt.tzinfo:
date_employee_tz = pytz.utc.localize(dt).astimezone(pytz.timezone(employee._get_tz()))
else:
date_employee_tz = dt
start_day_employee_tz = date_employee_tz.replace(hour=0, minute=0, second=0)
return (start_day_employee_tz.astimezone(pytz.utc).replace(tzinfo=None), start_day_employee_tz.date())
def _get_attendances_dates(self):
# Returns a dictionnary {employee_id: set((datetimes, dates))}
attendances_emp = defaultdict(set)
for attendance in self.filtered(lambda a: a.employee_id.company_id.hr_attendance_overtime and a.check_in):
check_in_day_start = attendance._get_day_start_and_day(attendance.employee_id, attendance.check_in)
if check_in_day_start[0] < datetime.combine(attendance.employee_id.company_id.overtime_start_date, datetime.min.time()):
continue
attendances_emp[attendance.employee_id].add(check_in_day_start)
if attendance.check_out:
check_out_day_start = attendance._get_day_start_and_day(attendance.employee_id, attendance.check_out)
attendances_emp[attendance.employee_id].add(check_out_day_start)
return attendances_emp
def _update_overtime(self, employee_attendance_dates=None):
if employee_attendance_dates is None:
employee_attendance_dates = self._get_attendances_dates()
overtime_to_unlink = self.env['hr.attendance.overtime']
overtime_vals_list = []
for emp, attendance_dates in employee_attendance_dates.items():
# get_attendances_dates returns the date translated from the local timezone without tzinfo,
# and contains all the date which we need to check for overtime
emp_tz = pytz.timezone(emp._get_tz())
attendance_domain = []
for attendance_date in attendance_dates:
attendance_domain = OR([attendance_domain, [
('check_in', '>=', attendance_date[0]), ('check_in', '<', attendance_date[0] + timedelta(hours=24)),
]])
attendance_domain = AND([[('employee_id', '=', emp.id)], attendance_domain])
# Attendances per LOCAL day
attendances_per_day = defaultdict(lambda: self.env['hr.attendance'])
all_attendances = self.env['hr.attendance'].search(attendance_domain)
for attendance in all_attendances:
check_in_day_start = attendance._get_day_start_and_day(attendance.employee_id, attendance.check_in)
attendances_per_day[check_in_day_start[1]] += attendance
# As _attendance_intervals_batch and _leave_intervals_batch both take localized dates we need to localize those date
start = pytz.utc.localize(min(attendance_dates, key=itemgetter(0))[0])
stop = pytz.utc.localize(max(attendance_dates, key=itemgetter(0))[0] + timedelta(hours=24))
# Retrieve expected attendance intervals
expected_attendances = emp.resource_calendar_id._attendance_intervals_batch(
start, stop, emp.resource_id
)[emp.resource_id.id]
# Substract Global Leaves
expected_attendances -= emp.resource_calendar_id._leave_intervals_batch(start, stop, None)[False]
# working_times = {date: [(start, stop)]}
working_times = defaultdict(lambda: [])
for expected_attendance in expected_attendances:
# Exclude resource.calendar.attendance
working_times[expected_attendance[0].date()].append(expected_attendance[:2])
overtimes = self.env['hr.attendance.overtime'].sudo().search([
('employee_id', '=', emp.id),
('date', 'in', [day_data[1] for day_data in attendance_dates]),
('adjustment', '=', False),
])
company_threshold = emp.company_id.overtime_company_threshold / 60.0
employee_threshold = emp.company_id.overtime_employee_threshold / 60.0
for day_data in attendance_dates:
attendance_date = day_data[1]
attendances = attendances_per_day.get(attendance_date, self.browse())
unfinished_shifts = attendances.filtered(lambda a: not a.check_out)
overtime_duration = 0
overtime_duration_real = 0
# Overtime is not counted if any shift is not closed or if there are no attendances for that day,
# this could happen when deleting attendances.
if not unfinished_shifts and attendances:
# The employee usually doesn't work on that day
if not working_times[attendance_date]:
# User does not have any resource_calendar_attendance for that day (week-end for example)
overtime_duration = sum(attendances.mapped('worked_hours'))
overtime_duration_real = overtime_duration
# The employee usually work on that day
else:
# Compute start and end time for that day
planned_start_dt, planned_end_dt = False, False
planned_work_duration = 0
for calendar_attendance in working_times[attendance_date]:
planned_start_dt = min(planned_start_dt, calendar_attendance[0]) if planned_start_dt else calendar_attendance[0]
planned_end_dt = max(planned_end_dt, calendar_attendance[1]) if planned_end_dt else calendar_attendance[1]
planned_work_duration += (calendar_attendance[1] - calendar_attendance[0]).total_seconds() / 3600.0
# Count time before, during and after 'working hours'
pre_work_time, work_duration, post_work_time = 0, 0, 0
for attendance in attendances:
# consider check_in as planned_start_dt if within threshold
# if delta_in < 0: Checked in after supposed start of the day
# if delta_in > 0: Checked in before supposed start of the day
local_check_in = emp_tz.localize(attendance.check_in)
delta_in = (planned_start_dt - local_check_in).total_seconds() / 3600.0
# Started before or after planned date within the threshold interval
if (delta_in > 0 and delta_in <= company_threshold) or\
(delta_in < 0 and abs(delta_in) <= employee_threshold):
local_check_in = planned_start_dt
local_check_out = emp_tz.localize(attendance.check_out)
# same for check_out as planned_end_dt
delta_out = (local_check_out - planned_end_dt).total_seconds() / 3600.0
# if delta_out < 0: Checked out before supposed start of the day
# if delta_out > 0: Checked out after supposed start of the day
# Finised before or after planned date within the threshold interval
if (delta_out > 0 and delta_out <= company_threshold) or\
(delta_out < 0 and abs(delta_out) <= employee_threshold):
local_check_out = planned_end_dt
# There is an overtime at the start of the day
if local_check_in < planned_start_dt:
pre_work_time += (min(planned_start_dt, local_check_out) - local_check_in).total_seconds() / 3600.0
# Interval inside the working hours -> Considered as working time
if local_check_in <= planned_end_dt and local_check_out >= planned_start_dt:
work_duration += (min(planned_end_dt, local_check_out) - max(planned_start_dt, local_check_in)).total_seconds() / 3600.0
# There is an overtime at the end of the day
if local_check_out > planned_end_dt:
post_work_time += (local_check_out - max(planned_end_dt, local_check_in)).total_seconds() / 3600.0
# Overtime within the planned work hours + overtime before/after work hours is > company threshold
overtime_duration = work_duration - planned_work_duration
if pre_work_time > company_threshold:
overtime_duration += pre_work_time
if post_work_time > company_threshold:
overtime_duration += post_work_time
# Global overtime including the thresholds
overtime_duration_real = sum(attendances.mapped('worked_hours')) - planned_work_duration
overtime = overtimes.filtered(lambda o: o.date == attendance_date)
if not float_is_zero(overtime_duration, 2) or unfinished_shifts:
# Do not create if any attendance doesn't have a check_out, update if exists
if unfinished_shifts:
overtime_duration = 0
if not overtime and overtime_duration:
overtime_vals_list.append({
'employee_id': emp.id,
'date': attendance_date,
'duration': overtime_duration,
'duration_real': overtime_duration_real,
})
elif overtime:
overtime.sudo().write({
'duration': overtime_duration,
'duration_real': overtime_duration
})
elif overtime:
overtime_to_unlink |= overtime
self.env['hr.attendance.overtime'].sudo().create(overtime_vals_list)
overtime_to_unlink.sudo().unlink()
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
res._update_overtime()
return res
def write(self, vals):
attendances_dates = self._get_attendances_dates()
super(HrAttendance, self).write(vals)
if any(field in vals for field in ['employee_id', 'check_in', 'check_out']):
# Merge attendance dates before and after write to recompute the
# overtime if the attendances have been moved to another day
for emp, dates in self._get_attendances_dates().items():
attendances_dates[emp] |= dates
self._update_overtime(attendances_dates)
def unlink(self):
attendances_dates = self._get_attendances_dates()
super(HrAttendance, self).unlink()
self._update_overtime(attendances_dates)
@api.returns('self', lambda value: value.id)
def copy(self):
raise exceptions.UserError(_('You cannot duplicate an attendance.'))
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class HrAttendanceOvertime(models.Model):
_name = "hr.attendance.overtime"
_description = "Attendance Overtime"
_rec_name = 'employee_id'
def _default_employee(self):
return self.env.user.employee_id
employee_id = fields.Many2one(
'hr.employee', string="Employee", default=_default_employee,
required=True, ondelete='cascade', index=True)
company_id = fields.Many2one(related='employee_id.company_id')
date = fields.Date(string='Day')
duration = fields.Float(string='Extra Hours', default=0.0, required=True)
duration_real = fields.Float(
string='Extra Hours (Real)', default=0.0,
help="Extra-hours including the threshold duration")
adjustment = fields.Boolean(default=False)
def init(self):
# Allows only 1 overtime record per employee per day unless it's an adjustment
self.env.cr.execute("""
CREATE UNIQUE INDEX IF NOT EXISTS hr_attendance_overtime_unique_employee_per_day
ON %s (employee_id, date)
WHERE adjustment is false""" % (self._table))
+13 -2
View File
@@ -2,10 +2,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo import models, fields, api, exceptions, _, SUPERUSER_ID
from odoo import models, fields, api, exceptions, _
from odoo.tools import float_round
class HrEmployeeBase(models.AbstractModel):
@@ -19,6 +19,16 @@ class HrEmployeeBase(models.AbstractModel):
hours_last_month = fields.Float(compute='_compute_hours_last_month')
hours_today = fields.Float(compute='_compute_hours_today')
hours_last_month_display = fields.Char(compute='_compute_hours_last_month')
overtime_ids = fields.One2many('hr.attendance.overtime', 'employee_id')
total_overtime = fields.Float(compute='_compute_total_overtime')
@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
@api.depends('user_id.im_status', 'attendance_state')
def _compute_presence_state(self):
@@ -136,6 +146,7 @@ class HrEmployeeBase(models.AbstractModel):
else:
modified_attendance = employee._attendance_action_change()
action_message['attendance'] = modified_attendance.read()[0]
action_message['total_overtime'] = employee.total_overtime
return {'action': action_message}
def _attendance_action_change(self):
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.osv.expression import OR
class ResCompany(models.Model):
_inherit = 'res.company'
hr_attendance_overtime = fields.Boolean(string="Count Extra Hours")
overtime_start_date = fields.Date(string="Extra Hours Starting Date")
overtime_company_threshold = fields.Integer(string="Tolerance Time In Favor Of Company", default=0)
overtime_employee_threshold = fields.Integer(string="Tolerance Time In Favor Of Employee", default=0)
def write(self, vals):
search_domain = False # Overtime to generate
delete_domain = False # Overtime to delete
overtime_enabled_companies = self.filtered('hr_attendance_overtime')
# If we disable overtime
if 'hr_attendance_overtime' in vals and not vals['hr_attendance_overtime'] and overtime_enabled_companies:
delete_domain = [('company_id', 'in', overtime_enabled_companies.ids)]
vals['overtime_start_date'] = False
start_date = vals.get('hr_attendance_overtime') and vals.get('overtime_start_date')
# Also recompute if the threshold have changed
if start_date or 'overtime_company_threshold' in vals or 'overtime_employee_threshold' in vals:
for company in self:
# If we modify the thresholds only
if start_date == company.overtime_start_date and \
(vals.get('overtime_company_threshold') != company.overtime_company_threshold) or\
(vals.get('overtime_employee_threshold') != company.overtime_employee_threshold):
search_domain = OR([search_domain, [('employee_id.company_id', '=', company.id)]])
# If we enabled the overtime with a start date
elif not company.overtime_start_date and start_date:
search_domain = OR([search_domain, [
('employee_id.company_id', '=', company.id),
('check_in', '>=', start_date)]])
# If we move the start date into the past
elif company.overtime_start_date > start_date:
search_domain = OR([search_domain, [
('employee_id.company_id', '=', company.id),
('check_in', '>=', start_date),
('check_in', '<=', company.overtime_start_date)]])
# If we move the start date into the future
elif company.overtime_start_date < start_date:
delete_domain = OR([delete_domain, [
('company_id', '=', company.id),
('date', '<', start_date)]])
res = super().write(vals)
if delete_domain:
self.env['hr.attendance.overtime'].search(delete_domain).unlink()
if search_domain:
self.env['hr.attendance'].search(search_domain)._update_overtime()
return res
@@ -1,11 +1,46 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
group_attendance_use_pin = fields.Boolean(string='Employee PIN',
group_attendance_use_pin = fields.Boolean(
string='Employee PIN',
implied_group="hr_attendance.group_hr_attendance_use_pin")
hr_attendance_overtime = fields.Boolean(
string="Count Extra Hours", readonly=False)
overtime_start_date = fields.Date(string="Extra Hours Starting Date", readonly=False)
overtime_company_threshold = fields.Integer(
string="Tolerance Time In Favor Of Company", readonly=False)
overtime_employee_threshold = fields.Integer(
string="Tolerance Time In Favor Of Employee", readonly=False)
@api.model
def get_values(self):
res = super(ResConfigSettings, self).get_values()
company = self.env.company
res.update({
'hr_attendance_overtime': company.hr_attendance_overtime,
'overtime_start_date': company.overtime_start_date,
'overtime_company_threshold': company.overtime_company_threshold,
'overtime_employee_threshold': company.overtime_employee_threshold,
})
return res
def set_values(self):
super(ResConfigSettings, self).set_values()
company = self.env.company
# Done this way to have all the values written at the same time,
# to avoid recomputing the overtimes several times with
# invalid company configurations
fields_to_check = [
'hr_attendance_overtime',
'overtime_start_date',
'overtime_company_threshold',
'overtime_employee_threshold',
]
if any(self[field] != company[field] for field in fields_to_check):
company.write({field: self[field] for field in fields_to_check})
+3 -1
View File
@@ -12,6 +12,7 @@ class User(models.Model):
attendance_state = fields.Selection(related='employee_id.attendance_state')
last_check_in = fields.Datetime(related='employee_id.last_attendance_id.check_in')
last_check_out = fields.Datetime(related='employee_id.last_attendance_id.check_out')
total_overtime = fields.Float(related='employee_id.total_overtime')
@property
def SELF_READABLE_FIELDS(self):
@@ -20,5 +21,6 @@ class User(models.Model):
'hours_last_month_display',
'attendance_state',
'last_check_in',
'last_check_out'
'last_check_out',
'total_overtime'
]
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import hr_attendance_report
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class HRAttendanceReport(models.Model):
_name = "hr.attendance.report"
_description = "Attendance Statistics"
_auto = False
department_id = fields.Many2one('hr.department', string="Department", readonly=True)
employee_id = fields.Many2one('hr.employee', string="Employee", readonly=True)
check_in = fields.Date("Check In", readonly=True)
worked_hours = fields.Float("Hours Worked", readonly=True)
overtime_hours = fields.Float("Extra Hours", readonly=True)
def init(self):
self.env.cr.execute("""
CREATE OR REPLACE VIEW %s AS (
(
SELECT
hra.id,
department_id,
hra.employee_id,
hra.check_in,
hra.worked_hours,
coalesce(ot.duration, 0) as overtime_hours
FROM (
SELECT
id,
row_number() over (partition by employee_id, CAST(check_in AS DATE)) as ot_check,
employee_id,
CAST(check_in as DATE) as check_in,
worked_hours
FROM
hr_attendance
) as hra
LEFT JOIN
hr_employee
ON hr_employee.id = hra.employee_id
LEFT JOIN
hr_attendance_overtime ot
ON hra.ot_check = 1
AND ot.employee_id = hra.employee_id
AND ot.date = hra.check_in
AND ot.adjustment = FALSE
)
)
""" % (self._table))
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_attendance_report_view_search" model="ir.ui.view">
<field name="name">hr.attendance.report.view.search</field>
<field name="model">hr.attendance.report</field>
<field name="arch" type="xml">
<search string="HR Attendance Search">
<field name="employee_id"/>
<field name="department_id" operator="child_of"/>
<filter name="check_in" string="Check In" date="check_in"/>
<group expand="0" string="Group By">
<filter string="Employee" name="groupby_employee" context="{'group_by': 'employee_id'}"/>
<filter string="Check In" name="groupby_check_in" context="{'group_by': 'check_in'}"/>
</group>
</search>
</field>
</record>
<record id="hr_attendance_report_view_pivot" model="ir.ui.view">
<field name="name">hr.attendance.report.view.pivot</field>
<field name="model">hr.attendance.report</field>
<field name="arch" type="xml">
<pivot string="Attendance" disable_linking="1">
<field name="employee_id" type="row"/>
<field name="check_in" type="col"/>
<field name="worked_hours" type="measure"/>
<field name="overtime_hours" type="measure"/>
</pivot>
</field>
</record>
<record id="hr_attendance_report_view_graph" model="ir.ui.view">
<field name="name">hr.attendance.report.view.graph</field>
<field name="model">hr.attendance.report</field>
<field name="arch" type="xml">
<graph string="Attendance Statistics" stacked="False" disable_linking="1">
<field name="employee_id" type="row"/>
<field name="check_in" type="col"/>
<field name="overtime_hours" type="measure" />
<field name="worked_hours" type="measure" />
</graph>
</field>
</record>
<record id="hr_attendance_report_action" model="ir.actions.act_window">
<field name="name">Attendance Analysis</field>
<field name="res_model">hr.attendance.report</field>
<field name="view_mode">graph,pivot</field>
<field name="search_view_id" ref="hr_attendance_report_view_search"/>
<field name="context">{'group_by': ['check_in:day', 'employee_id']}</field>
</record>
<record id="hr_attendance_report_action_filtered" model="ir.actions.act_window">
<field name="name">Attendance Analysis</field>
<field name="res_model">hr.attendance.report</field>
<field name="view_mode">graph,pivot</field>
<field name="search_view_id" ref="hr_attendance_report_view_search"/>
<field name="context">{
'group_by': ['check_in:day', 'employee_id'],
'search_default_department_id': [active_id]}
</field>
</record>
<menuitem
id="menu_hr_attendance_report"
name="Reporting"
sequence="30"
parent="hr_attendance.menu_hr_attendance_root"
action="hr_attendance_report_action"
groups="hr_attendance.group_hr_attendance_user"/>
</odoo>
@@ -83,5 +83,30 @@
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
<!-- Overtime -->
<record id="hr_attendance_overtime_rule_employee_company" model="ir.rule">
<field name="name">Employee multi company rule</field>
<field name="model_id" ref="model_hr_attendance_overtime"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('employee_id.company_id','=',False),('employee_id.company_id', 'in', company_ids)]</field>
</record>
<record id="hr_attendance_rule_attendance_overtime_manager" model="ir.rule">
<field name="name">attendance officer: full access</field>
<field name="model_id" ref="model_hr_attendance_overtime"/>
<field name="domain_force">[(1,'=',1)]</field>
<field name="groups" eval="[(4,ref('hr_attendance.group_hr_attendance_user'))]"/>
</record>
<record id="hr_attendance_rule_attendance_overtime_employee" model="ir.rule">
<field name="name">user: read and modify own attendance only</field>
<field name="model_id" ref="model_hr_attendance_overtime"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
</data>
</odoo>
@@ -1,3 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_attendance_user,hr.attendance.user,model_hr_attendance,hr_attendance.group_hr_attendance_user,1,1,1,1
access_hr_attendance_system_user,hr.attendance.system.user,model_hr_attendance,base.group_user,1,1,1,0
access_hr_attendance_overtime_user,hr.attendance.overtime.user,model_hr_attendance_overtime,hr_attendance.group_hr_attendance_user,1,1,1,1
access_hr_attendance_overtime_system_user,hr.attendance.overtime.system.user,model_hr_attendance_overtime,base.group_user,1,0,0,0
access_hr_attendance_report_user,hr.attendance.report.user,model_hr_attendance_report,hr_attendance.group_hr_attendance_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_attendance_user hr.attendance.user model_hr_attendance hr_attendance.group_hr_attendance_user 1 1 1 1
3 access_hr_attendance_system_user hr.attendance.system.user model_hr_attendance base.group_user 1 1 1 0
4 access_hr_attendance_overtime_user hr.attendance.overtime.user model_hr_attendance_overtime hr_attendance.group_hr_attendance_user 1 1 1 1
5 access_hr_attendance_overtime_system_user hr.attendance.overtime.system.user model_hr_attendance_overtime base.group_user 1 0 0 0
6 access_hr_attendance_report_user hr.attendance.report.user model_hr_attendance_report hr_attendance.group_hr_attendance_user 1 1 1 1
@@ -4,6 +4,7 @@ odoo.define('hr_attendance.greeting_message', function (require) {
var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
var time = require('web.time');
var field_utils = require('web.field_utils');
var _t = core._t;
@@ -52,6 +53,10 @@ var GreetingMessage = AbstractAction.extend({
this.attendance.check_in_time = this.attendance.check_in && this.attendance.check_in.format(this.format_time);
this.attendance.check_out_time = this.attendance.check_out && this.attendance.check_out.format(this.format_time);
// extra hours amount displayed in the greeting message template.
this.total_overtime_float = action.total_overtime; // Used for comparison in template
this.total_overtime = field_utils.format.float_time(this.total_overtime_float)
if (action.hours_today) {
var duration = moment.duration(action.hours_today, "hours");
this.hours_today = duration.hours() + ' hours, ' + duration.minutes() + ' minutes';
@@ -118,6 +118,18 @@
Checked out at <b><t t-esc="widget.attendance.check_out_time"/></b>
<br/><b><t t-esc="widget.hours_today"/></b>
</div>
<t t-if="widget.total_overtime_float &gt; 0">
<div class="alert alert-success h3 mt0" role="status">
Total extra hours:
<span t-esc="widget.total_overtime"/>
</div>
</t>
<t t-if="widget.total_overtime_float &lt; 0">
<div class="alert alert-danger h3 mt0" role="status">
Total extra hours:
<span t-esc="widget.total_overtime"/>
</div>
</t>
<h3 class="o_hr_attendance_random_message mb24"/>
<div class="o_hr_attendance_warning_message mt24 alert alert-warning" style="display:none" role="alert"/>
</t>
@@ -24,6 +24,7 @@ QUnit.module('HR Attendance', {
user_id: {string: 'user ID', type: 'integer'},
barcode: {string:'barcode', type: 'integer'},
hours_today: {string:'Hours today', type: 'float'},
overtime: {string: 'Overtime', type: 'float'},
},
records: [{
id: 1,
+1
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from . import test_hr_attendance_constraints
from . import test_hr_attendance_overtime
from . import test_hr_attendance_process
@@ -0,0 +1,266 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, datetime
from odoo.tests import new_test_user
from odoo.tests.common import tagged, TransactionCase
@tagged('hr_attendance_overtime')
class TestHrAttendanceOvertime(TransactionCase):
""" Tests for overtime """
def setUp(self):
super().setUp()
self.company = self.env['res.company'].create({
'name': 'SweatChopChop Inc.',
'hr_attendance_overtime': True,
'overtime_start_date': datetime(2021, 1, 1),
'overtime_company_threshold': 10,
'overtime_employee_threshold': 10,
})
self.env.company = self.company
self.user = new_test_user(self.env, login='fru', groups='base.group_user,hr_attendance.group_hr_attendance', company_id=self.company.id)
self.employee = self.env['hr.employee'].create({
'name': "Marie-Edouard De La Court",
'user_id': self.user.id,
'company_id': self.company.id,
'tz': 'UTC',
})
self.other_employee = self.env['hr.employee'].create({
'name': 'Yolanda',
'company_id': self.company.id,
})
self.jpn_employee = self.env['hr.employee'].create({
'name': 'Sacha',
'company_id': self.company.id,
'tz': 'Asia/Tokyo',
})
self.honolulu_employee = self.env['hr.employee'].create({
'name': 'Susan',
'company_id': self.company.id,
'tz': 'Pacific/Honolulu',
})
def test_overtime_company_settings(self):
self.company.hr_attendance_overtime = False
self.env['hr.attendance'].create({
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 8, 0),
'check_out': datetime(2021, 1, 4, 20, 0)
})
overtime = self.env['hr.attendance.overtime'].search_count([('employee_id', '=', self.employee.id), ('date', '=', date(2021, 1, 4))])
self.assertFalse(overtime, 'No overtime should be created')
self.company.write({
'hr_attendance_overtime': True,
'overtime_start_date': date(2021, 1, 1)
})
overtime = self.env['hr.attendance.overtime'].search_count([('employee_id', '=', self.employee.id), ('date', '=', date(2021, 1, 4))])
self.assertTrue(overtime, 'Overtime should be created')
self.env['hr.attendance'].create({
'employee_id': self.employee.id,
'check_in': datetime(2020, 12, 30, 8, 0),
'check_out': datetime(2020, 12, 30, 20, 0)
})
overtime = self.env['hr.attendance.overtime'].search_count([('employee_id', '=', self.employee.id), ('date', '=', date(2020, 12, 30))])
self.assertFalse(overtime, 'No overtime should be created before the start date')
def test_simple_overtime(self):
checkin_am = self.env['hr.attendance'].create({
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 8, 0)
})
self.env['hr.attendance'].create({
'employee_id': self.other_employee.id,
'check_in': datetime(2021, 1, 4, 8, 0),
'check_out': datetime(2021, 1, 4, 22, 0)
})
overtime = self.env['hr.attendance.overtime'].search([('employee_id', '=', self.employee.id), ('date', '=', date(2021, 1, 4))])
self.assertFalse(overtime, 'No overtime record should exist for that employee')
checkin_am.write({'check_out': datetime(2021, 1, 4, 12, 0)})
overtime = self.env['hr.attendance.overtime'].search([('employee_id', '=', self.employee.id), ('date', '=', date(2021, 1, 4))])
self.assertTrue(overtime, 'An overtime record should be created')
self.assertEqual(overtime.duration, -4)
checkin_pm = self.env['hr.attendance'].create({
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 13, 0)
})
self.assertEqual(overtime.duration, 0, 'Overtime duration should be 0 when an attendance has not been checked out.')
checkin_pm.write({'check_out': datetime(2021, 1, 4, 18, 0)})
self.assertTrue(overtime.exists(), 'Overtime should not be deleted')
self.assertAlmostEqual(overtime.duration, 1)
self.assertAlmostEqual(self.employee.total_overtime, 1)
def test_overtime_weekend(self):
self.env['hr.attendance'].create({
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 2, 8, 0),
'check_out': datetime(2021, 1, 2, 11, 0)
})
overtime = self.env['hr.attendance.overtime'].search([('employee_id', '=', self.employee.id), ('date', '=', date(2021, 1, 2))])
self.assertTrue(overtime, 'Overtime should be created')
self.assertEqual(overtime.duration, 3, 'Should have 3 hours of overtime')
self.assertEqual(self.employee.total_overtime, 3, 'Should still have 3 hours of overtime')
def test_overtime_multiple(self):
attendance = self.env['hr.attendance'].create({
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 2, 8, 0),
'check_out': datetime(2021, 1, 2, 19, 0)
})
self.assertEqual(self.employee.total_overtime, 11)
self.env['hr.attendance'].create({
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 7, 0),
'check_out': datetime(2021, 1, 4, 16, 0)
})
self.assertEqual(self.employee.total_overtime, 12)
attendance.unlink()
self.assertEqual(self.employee.total_overtime, 1)
def test_overtime_change_employee(self):
attendance = self.env['hr.attendance'].create({
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 7, 0),
'check_out': datetime(2021, 1, 4, 18, 0)
})
self.assertEqual(self.employee.total_overtime, 3)
self.assertEqual(self.other_employee.total_overtime, 0)
attendance.employee_id = self.other_employee.id
self.assertEqual(self.other_employee.total_overtime, 3)
self.assertEqual(self.employee.total_overtime, 0)
def test_overtime_far_timezones(self):
self.env['hr.attendance'].create({
'employee_id': self.jpn_employee.id,
# Since dates have to be stored in utc these are the tokyo timezone times for 7-18 (UTC+9)
'check_in': datetime(2021, 1, 3, 22, 0),
'check_out': datetime(2021, 1, 4, 9, 0),
})
self.env['hr.attendance'].create({
'employee_id': self.honolulu_employee.id,
# Same but for alaskan times (UTC-10)
'check_in': datetime(2021, 1, 4, 17, 0),
'check_out': datetime(2021, 1, 5, 4, 0),
})
self.assertEqual(self.jpn_employee.total_overtime, 3)
self.assertEqual(self.honolulu_employee.total_overtime, 3)
def test_overtime_unclosed(self):
attendance = self.env['hr.attendance'].create({
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 8, 0),
})
overtime = self.env['hr.attendance.overtime'].search([('employee_id', '=', self.employee.id)])
self.assertFalse(overtime, 'Overtime entry should not exist at this point.')
# Employees goes to eat
attendance.write({
'check_out': datetime(2021, 1, 4, 12, 0),
})
overtime = self.env['hr.attendance.overtime'].search([('employee_id', '=', self.employee.id)])
self.assertTrue(overtime, 'An overtime entry should have been created.')
self.assertEqual(overtime.duration, -4, 'User still has to work the afternoon.')
self.env['hr.attendance'].create({
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 13, 0),
})
self.assertEqual(overtime.duration, 0, 'Overtime entry has been reset due to an unclosed attendance.')
def test_overtime_company_threshold(self):
self.env['hr.attendance'].create([
{
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 7, 55),
'check_out': datetime(2021, 1, 4, 12, 0),
},
{
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 13, 0),
'check_out': datetime(2021, 1, 4, 17, 5),
}
])
overtime = self.env['hr.attendance.overtime'].search([('employee_id', '=', self.employee.id)])
self.assertFalse(overtime, 'No overtime should be counted because of the threshold.')
self.company.write({
'hr_attendance_overtime': True,
'overtime_start_date': date(2021, 1, 1),
'overtime_company_threshold': 4,
})
overtime = self.env['hr.attendance.overtime'].search([('employee_id', '=', self.employee.id)])
self.assertTrue(overtime, 'Overtime entry should exist since the threshold has been lowered.')
self.assertAlmostEqual(overtime.duration, 10 / 60, msg='Overtime should be equal to 10 minutes.')
def test_overtime_employee_threshold(self):
self.env['hr.attendance'].create([
{
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 8, 5),
'check_out': datetime(2021, 1, 4, 12, 0),
},
{
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 13, 0),
'check_out': datetime(2021, 1, 4, 16, 55),
}
])
overtime = self.env['hr.attendance.overtime'].search([('employee_id', '=', self.employee.id)])
self.assertFalse(overtime, 'No overtime should be counted because of the threshold.')
self.company.write({
'hr_attendance_overtime': True,
'overtime_start_date': date(2021, 1, 1),
'overtime_employee_threshold': 4,
})
overtime = self.env['hr.attendance.overtime'].search([('employee_id', '=', self.employee.id)])
self.assertTrue(overtime, 'Overtime entry should exist since the threshold has been lowered.')
self.assertAlmostEqual(overtime.duration, -(10 / 60), msg='Overtime should be equal to -10 minutes.')
def test_overtime_both_threshold(self):
self.env['hr.attendance'].create([
{
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 8, 5),
'check_out': datetime(2021, 1, 4, 12, 0),
},
{
'employee_id': self.employee.id,
'check_in': datetime(2021, 1, 4, 13, 0),
'check_out': datetime(2021, 1, 4, 17, 5),
}
])
overtime = self.env['hr.attendance.overtime'].search([('employee_id', '=', self.employee.id)])
self.assertFalse(overtime, 'No overtime should be counted because of the threshold.')
self.company.write({
'hr_attendance_overtime': True,
'overtime_start_date': date(2021, 1, 1),
'overtime_employee_threshold': 4,
})
overtime = self.env['hr.attendance.overtime'].search([('employee_id', '=', self.employee.id)])
self.assertTrue(overtime, 'Overtime entry should exist since the employee threshold has been lowered.')
self.assertAlmostEqual(overtime.duration, -(5 / 60), msg='Overtime should be equal to -5 minutes.')
self.company.write({
'hr_attendance_overtime': True,
'overtime_start_date': date(2021, 1, 1),
'overtime_company_threshold': 4,
})
overtime = self.env['hr.attendance.overtime'].search([('employee_id', '=', self.employee.id)])
self.assertFalse(overtime, 'Overtime entry should be unlinked since both overtime cancel each other.')
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_attendance_overtime_tree" model="ir.ui.view">
<field name="name">hr.attendance.overtime.tree</field>
<field name="model">hr.attendance.overtime</field>
<field name="arch" type="xml">
<tree edit="0">
<field name="date"/>
<field name="employee_id"/>
<field name="duration" widget="float_time"/>
</tree>
</field>
</record>
<record id="view_attendance_overtime_search" model="ir.ui.view">
<field name="name">hr.attendance.overtime.search</field>
<field name="model">hr.attendance.overtime</field>
<field name="arch" type="xml">
<search>
<field name="employee_id"/>
<field name="duration" filter_domain="[('duration', '>=', self)]"/>
</search>
</field>
</record>
<record id="hr_attendance_overtime_action" model="ir.actions.act_window">
<field name="name">Extra Hours</field>
<field name="res_model">hr.attendance.overtime</field>
<field name="view_mode">tree</field>
</record>
</odoo>
@@ -107,30 +107,6 @@
</field>
</record>
<record id="hr_attendance_view_pivot" model="ir.ui.view">
<field name="name">hr.attendance.pivot</field>
<field name="model">hr.attendance</field>
<field name="arch" type="xml">
<pivot string="Attendance" sample="1">
<field name="employee_id" type="row"/>
<field name="check_in" type="col"/>
<field name="worked_hours" type="measure" widget="float_time"/>
</pivot>
</field>
</record>
<record id="hr_attendance_view_graph" model="ir.ui.view">
<field name="name">hr.attendance.graph</field>
<field name="model">hr.attendance</field>
<field name="arch" type="xml">
<graph string="Attendance" sample="1">
<field name="employee_id"/>
<field name="check_in"/>
<field name="worked_hours" type="measure"/>
</graph>
</field>
</record>
<!-- actions -->
<record id="hr_attendance_action" model="ir.actions.act_window">
@@ -176,34 +152,6 @@
</field>
</record>
<record id="hr_attendance_action_graph" model="ir.actions.act_window">
<field name="name">Attendance Analysis</field>
<field name="res_model">hr.attendance</field>
<field name="view_mode">graph,pivot</field>
<field name="view_id"></field> <!-- force empty -->
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No data yet!
</p>
</field>
</record>
<record id="hr_attendance_action_graph_filtered" model="ir.actions.act_window">
<field name="name">Attendance Analysis</field>
<field name="res_model">hr.attendance</field>
<field name="view_mode">graph,pivot</field>
<field name="context">{
'search_default_department_id': [active_id],
'default_department_id': active_id}
</field>
<field name="view_id"></field> <!-- force empty -->
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No data yet!
</p>
</field>
</record>
<record id="hr_attendance_action_kiosk_mode" model="ir.actions.client">
<field name="name">Attendances</field>
<field name="tag">hr_attendance_kiosk_mode</field>
@@ -232,6 +180,4 @@
<menuitem id="menu_hr_attendance_kiosk_no_user_mode" name="Kiosk Mode" parent="menu_hr_attendance_root" sequence="10" groups="hr_attendance.group_hr_attendance_kiosk" action="hr_attendance_action_kiosk_mode"/>
<menuitem id="menu_hr_attendance_view_attendances" name="Attendances" parent="menu_hr_attendance_root" sequence="10" groups="hr_attendance.group_hr_attendance_user" action="hr_attendance_action"/>
<menuitem id="menu_hr_attendance_report" name="Reporting" parent="menu_hr_attendance_root" sequence="30" groups="hr_attendance.group_hr_attendance_user" action="hr_attendance_action_graph"/>
</odoo>
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--Hr Department Inherit Kanban view-->
<record id="hr_department_view_kanban" model="ir.ui.view">
<field name="name">hr.department.kanban.inherit</field>
<field name="model">hr.department</field>
@@ -8,13 +7,11 @@
<field name="groups_id" eval="[(4,ref('hr_attendance.group_hr_attendance_user'))]"/>
<field name="arch" type="xml">
<data>
<xpath expr="//div[hasclass('o_kanban_manage_reports')]" position="inside">
<a role="menuitem" class="dropdown-item" name="%(hr_attendance_action_graph_filtered)d" type="action">
<a role="menuitem" class="dropdown-item" name="%(hr_attendance_report_action_filtered)d" type="action">
Attendances
</a>
</xpath>
</data>
</field>
</record>
@@ -53,6 +53,22 @@
</span>
</div>
</button>
<button name="%(hr_attendance_overtime_action)d"
class="oe_stat_button"
icon="fa-history"
type="action"
attrs="{'invisible': [('total_overtime', '=', 0.0)]}"
context="{'search_default_employee_id': active_id}">
<div class="o_stat_info">
<span class="o_stat_value text-success" attrs="{'invisible': [('total_overtime', '&lt;', 0)]}">
<field name="total_overtime" widget="float_time"/>
</span>
<span class="o_stat_value text-danger" attrs="{'invisible': [('total_overtime', '&gt;=', 0)]}">
<field name="total_overtime" widget="float_time"/>
</span>
<span class="o_stat_text">Extra Hours</span>
</div>
</button>
</xpath>
</field>
</record>
@@ -108,6 +124,26 @@
</div>
</button>
</xpath>
<xpath expr="//div[@name='button_box']" position="inside">
<button name="%(hr_attendance_overtime_action)d"
class="oe_stat_button"
icon="fa-history"
type="action"
attrs="{'invisible': [('total_overtime', '=', 0.0)]}"
context="{'search_default_employee_id': employee_ids}"
groups="base.group_user"
help="Amount of extra hours">
<div class="o_stat_info">
<span class="o_stat_value text-success" attrs="{'invisible': [('total_overtime', '&lt;', 0)]}">
<field name="total_overtime" widget="float_time"/>
</span>
<span class="o_stat_value text-danger" attrs="{'invisible': [('total_overtime', '&gt;=', 0)]}">
<field name="total_overtime" widget="float_time"/>
</span>
<span class="o_stat_text">Extra Hours</span>
</div>
</button>
</xpath>
</field>
</record>
@@ -120,6 +156,7 @@
<kanban class="o_hr_employee_attendance_kanban" create="false">
<field name="attendance_state"/>
<field name="hours_today"/>
<field name="total_overtime"/>
<field name="id"/>
<templates>
<t t-name="kanban-box">
@@ -5,9 +5,9 @@
<field name="model">res.config.settings</field>
<field name="priority" eval="80"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Attendances" string="Attendances" data-key="hr_attendance" groups="hr_attendance.group_hr_attendance_manager">
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Attendances" string="Attendances" data-key="hr_attendance" groups="hr_attendance.group_hr_attendance_manager">
<h2>Check-In/Out</h2>
<div class="row mt16 o_settings_container" name="pincode_setting_container">
<div class="col-12 col-lg-6 o_setting_box" title="Set PIN codes in the employee detail form (in HR Settings tab).">
@@ -22,6 +22,47 @@
</div>
</div>
</div>
<h2>Extra Hours</h2>
<div class="row mt16 o_settings_container" name="overtime_settings">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane" title="Activate the count of employees' extra hours.">
<field name="hr_attendance_overtime"/>
</div>
<div class="o_setting_right_pane">
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." role="img" aria-label="Values set here are company-specific." groups="base.group_multi_company"/>
<label for="hr_attendance_overtime" class="o_form_label">Count of Extra Hours</label>
<div class="text-muted">
Compare attendance with working hours set on employee.
</div>
<div class="mt16" attrs="{'invisible': [('hr_attendance_overtime', '=', False)],
'required': [('hr_attendance_overtime', '=', True)]}">
<div class="mt16 row" title="Count of extra hours is considered from this date. Potential extra hours prior to this date are not considered.">
<label for="overtime_start_date" string="Start from" class="col-3 col-lg-3 o_light_label"/>
<field name="overtime_start_date" class="col-lg-3 p-0" attrs="{'required': [('hr_attendance_overtime', '=', True)]}" />
</div>
<br/>
<label for="overtime_company_threshold" class="o_form_label">
Tolerance Time In Favor Of Company
</label>
<div class="text-muted">
Allow a period of time (around working hours) where extra time will not be counted, in benefit of the company
</div>
<span>Time Period </span><field name="overtime_company_threshold" class="text-center oe_inline"
attrs="{'required': [('hr_attendance_overtime', '=', True)]}"/><span> Minutes</span>
<br/>
<br/>
<label for="overtime_employee_threshold" class="o_form_label">
Tolerance Time In Favor Of Employee
</label>
<div class="text-muted">
Allow a period of time (around working hours) where extra time will not be deducted, in benefit of the employee
</div>
<span>Time Period </span><field name="overtime_employee_threshold" class="text-center oe_inline"
attrs="{'required': [('hr_attendance_overtime', '=', True)]}"/><span> Minutes</span>
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
+12 -9
View File
@@ -251,15 +251,18 @@ class HolidaysType(models.Model):
@api.model
def get_days_all_request(self):
leave_types = sorted(self.search([]).filtered(lambda x: x.virtual_remaining_leaves or x.max_leaves), key=self._model_sorting_key, reverse=True)
return [(lt.name, {
'remaining_leaves': ('%.2f' % lt.remaining_leaves).rstrip('0').rstrip('.'),
'virtual_remaining_leaves': ('%.2f' % lt.virtual_remaining_leaves).rstrip('0').rstrip('.'),
'max_leaves': ('%.2f' % lt.max_leaves).rstrip('0').rstrip('.'),
'leaves_taken': ('%.2f' % lt.leaves_taken).rstrip('0').rstrip('.'),
'virtual_leaves_taken': ('%.2f' % lt.virtual_leaves_taken).rstrip('0').rstrip('.'),
'request_unit': lt.request_unit,
}, lt.allocation_type, lt.validity_stop, lt.id)
for lt in leave_types]
return [lt._get_days_request() for lt in leave_types]
def _get_days_request(self):
self.ensure_one()
return (self.name, {
'remaining_leaves': ('%.2f' % self.remaining_leaves).rstrip('0').rstrip('.'),
'virtual_remaining_leaves': ('%.2f' % self.virtual_remaining_leaves).rstrip('0').rstrip('.'),
'max_leaves': ('%.2f' % self.max_leaves).rstrip('0').rstrip('.'),
'leaves_taken': ('%.2f' % self.leaves_taken).rstrip('0').rstrip('.'),
'virtual_leaves_taken': ('%.2f' % self.virtual_leaves_taken).rstrip('0').rstrip('.'),
'request_unit': self.request_unit,
}, self.allocation_type, self.validity_stop, self.id)
def _get_contextual_employee_id(self):
if 'employee_id' in self._context:
@@ -31,7 +31,7 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
self.richard_emp.generate_work_entries(date(2018, 1, 1), date(2018, 1, 2))
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
with self.assertQueryCount(__system__=96, admin=97):
with self.assertQueryCount(__system__=96, admin=100):
leave.action_validate()
leave.action_refuse()
@@ -40,7 +40,7 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
def test_performance_leave_write(self):
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
with self.assertQueryCount(__system__=20, admin=20):
with self.assertQueryCount(__system__=24, admin=33):
leave.date_to = datetime(2018, 1, 1, 19, 0)
leave.action_refuse()