diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py
index 104a82144b5..38ef9c96c26 100644
--- a/addons/hr/models/hr_employee.py
+++ b/addons/hr/models/hr_employee.py
@@ -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
# ---------------------------------------------------------
diff --git a/addons/hr_attendance/__init__.py b/addons/hr_attendance/__init__.py
index 7d34c7c054a..5905766d867 100644
--- a/addons/hr_attendance/__init__.py
+++ b/addons/hr_attendance/__init__.py
@@ -3,3 +3,4 @@
from . import controllers
from . import models
+from . import report
diff --git a/addons/hr_attendance/__manifest__.py b/addons/hr_attendance/__manifest__.py
index 3ccc8093dab..0daa9b331c5 100644
--- a/addons/hr_attendance/__manifest__.py
+++ b/addons/hr_attendance/__manifest__.py
@@ -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',
diff --git a/addons/hr_attendance/data/hr_attendance_demo.xml b/addons/hr_attendance/data/hr_attendance_demo.xml
index 278c72798b5..d946af0ccfd 100644
--- a/addons/hr_attendance/data/hr_attendance_demo.xml
+++ b/addons/hr_attendance/data/hr_attendance_demo.xml
@@ -16,74 +16,74 @@
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
+
diff --git a/addons/hr_attendance/models/__init__.py b/addons/hr_attendance/models/__init__.py
index ac648b8357f..ebbcbe2deb6 100644
--- a/addons/hr_attendance/models/__init__.py
+++ b/addons/hr_attendance/models/__init__.py
@@ -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
diff --git a/addons/hr_attendance/models/hr_attendance.py b/addons/hr_attendance/models/hr_attendance.py
index 5e91d90b886..c7e93c186d5 100644
--- a/addons/hr_attendance/models/hr_attendance.py
+++ b/addons/hr_attendance/models/hr_attendance.py
@@ -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.'))
diff --git a/addons/hr_attendance/models/hr_attendance_overtime.py b/addons/hr_attendance/models/hr_attendance_overtime.py
new file mode 100644
index 00000000000..02eb362d286
--- /dev/null
+++ b/addons/hr_attendance/models/hr_attendance_overtime.py
@@ -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))
diff --git a/addons/hr_attendance/models/hr_employee.py b/addons/hr_attendance/models/hr_employee.py
index 0f65f37251e..87ddfad7a7a 100644
--- a/addons/hr_attendance/models/hr_employee.py
+++ b/addons/hr_attendance/models/hr_employee.py
@@ -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):
diff --git a/addons/hr_attendance/models/res_company.py b/addons/hr_attendance/models/res_company.py
new file mode 100644
index 00000000000..e418f756594
--- /dev/null
+++ b/addons/hr_attendance/models/res_company.py
@@ -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
diff --git a/addons/hr_attendance/models/res_config_settings.py b/addons/hr_attendance/models/res_config_settings.py
index 5e67d2e50df..7516aec2b0f 100644
--- a/addons/hr_attendance/models/res_config_settings.py
+++ b/addons/hr_attendance/models/res_config_settings.py
@@ -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})
diff --git a/addons/hr_attendance/models/res_users.py b/addons/hr_attendance/models/res_users.py
index cc499cdb125..69a9e63054b 100644
--- a/addons/hr_attendance/models/res_users.py
+++ b/addons/hr_attendance/models/res_users.py
@@ -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'
]
diff --git a/addons/hr_attendance/report/__init__.py b/addons/hr_attendance/report/__init__.py
new file mode 100644
index 00000000000..3588798d571
--- /dev/null
+++ b/addons/hr_attendance/report/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import hr_attendance_report
diff --git a/addons/hr_attendance/report/hr_attendance_report.py b/addons/hr_attendance/report/hr_attendance_report.py
new file mode 100644
index 00000000000..7b1385031ce
--- /dev/null
+++ b/addons/hr_attendance/report/hr_attendance_report.py
@@ -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))
diff --git a/addons/hr_attendance/report/hr_attendance_report_views.xml b/addons/hr_attendance/report/hr_attendance_report_views.xml
new file mode 100644
index 00000000000..09e8b6198b2
--- /dev/null
+++ b/addons/hr_attendance/report/hr_attendance_report_views.xml
@@ -0,0 +1,71 @@
+
+
+
+ hr.attendance.report.view.search
+ hr.attendance.report
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.attendance.report.view.pivot
+ hr.attendance.report
+
+
+
+
+
+
+
+
+
+
+
+ hr.attendance.report.view.graph
+ hr.attendance.report
+
+
+
+
+
+
+
+
+
+
+
+ Attendance Analysis
+ hr.attendance.report
+ graph,pivot
+
+ {'group_by': ['check_in:day', 'employee_id']}
+
+
+
+ Attendance Analysis
+ hr.attendance.report
+ graph,pivot
+
+ {
+ 'group_by': ['check_in:day', 'employee_id'],
+ 'search_default_department_id': [active_id]}
+
+
+
+
+
diff --git a/addons/hr_attendance/security/hr_attendance_security.xml b/addons/hr_attendance/security/hr_attendance_security.xml
index 401f68899bb..da89fe7c437 100644
--- a/addons/hr_attendance/security/hr_attendance_security.xml
+++ b/addons/hr_attendance/security/hr_attendance_security.xml
@@ -83,5 +83,30 @@
+
+
+ Employee multi company rule
+
+
+ ['|',('employee_id.company_id','=',False),('employee_id.company_id', 'in', company_ids)]
+
+
+
+ attendance officer: full access
+
+ [(1,'=',1)]
+
+
+
+
+ user: read and modify own attendance only
+
+ [('employee_id.user_id', '=', user.id)]
+
+
+
+
+
+
diff --git a/addons/hr_attendance/security/ir.model.access.csv b/addons/hr_attendance/security/ir.model.access.csv
index 0c0cd4df5c8..f6e896f1405 100644
--- a/addons/hr_attendance/security/ir.model.access.csv
+++ b/addons/hr_attendance/security/ir.model.access.csv
@@ -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
diff --git a/addons/hr_attendance/static/src/js/greeting_message.js b/addons/hr_attendance/static/src/js/greeting_message.js
index 51ff2682f1b..3c816087b36 100644
--- a/addons/hr_attendance/static/src/js/greeting_message.js
+++ b/addons/hr_attendance/static/src/js/greeting_message.js
@@ -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';
diff --git a/addons/hr_attendance/static/src/xml/attendance.xml b/addons/hr_attendance/static/src/xml/attendance.xml
index 949566bddb7..d353702873a 100644
--- a/addons/hr_attendance/static/src/xml/attendance.xml
+++ b/addons/hr_attendance/static/src/xml/attendance.xml
@@ -118,6 +118,18 @@
Checked out at
+
+
+ Total extra hours:
+
+
+
+
+
+ Total extra hours:
+
+
+
diff --git a/addons/hr_attendance/static/tests/hr_attendance_tests.js b/addons/hr_attendance/static/tests/hr_attendance_tests.js
index 72e3ae93d9a..4536b0ec965 100644
--- a/addons/hr_attendance/static/tests/hr_attendance_tests.js
+++ b/addons/hr_attendance/static/tests/hr_attendance_tests.js
@@ -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,
diff --git a/addons/hr_attendance/tests/__init__.py b/addons/hr_attendance/tests/__init__.py
index 44d12aeb002..6a6d40c2a32 100644
--- a/addons/hr_attendance/tests/__init__.py
+++ b/addons/hr_attendance/tests/__init__.py
@@ -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
diff --git a/addons/hr_attendance/tests/test_hr_attendance_overtime.py b/addons/hr_attendance/tests/test_hr_attendance_overtime.py
new file mode 100644
index 00000000000..af4c8976863
--- /dev/null
+++ b/addons/hr_attendance/tests/test_hr_attendance_overtime.py
@@ -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.')
diff --git a/addons/hr_attendance/views/hr_attendance_overtime_view.xml b/addons/hr_attendance/views/hr_attendance_overtime_view.xml
new file mode 100644
index 00000000000..14e0162abc5
--- /dev/null
+++ b/addons/hr_attendance/views/hr_attendance_overtime_view.xml
@@ -0,0 +1,31 @@
+
+
+
+ hr.attendance.overtime.tree
+ hr.attendance.overtime
+
+
+
+
+
+
+
+
+
+
+ hr.attendance.overtime.search
+ hr.attendance.overtime
+
+
+
+
+
+
+
+
+
+ Extra Hours
+ hr.attendance.overtime
+ tree
+
+
diff --git a/addons/hr_attendance/views/hr_attendance_view.xml b/addons/hr_attendance/views/hr_attendance_view.xml
index 851ee0cbaf5..eb1e724df58 100644
--- a/addons/hr_attendance/views/hr_attendance_view.xml
+++ b/addons/hr_attendance/views/hr_attendance_view.xml
@@ -107,30 +107,6 @@
-
- hr.attendance.pivot
- hr.attendance
-
-
-
-
-
-
-
-
-
-
- hr.attendance.graph
- hr.attendance
-
-
-
-
-
-
-
-
-
@@ -176,34 +152,6 @@
-
- Attendance Analysis
- hr.attendance
- graph,pivot
-
-
-
- No data yet!
-
-
-
-
-
- Attendance Analysis
- hr.attendance
- graph,pivot
- {
- 'search_default_department_id': [active_id],
- 'default_department_id': active_id}
-
-
-
-
- No data yet!
-
-
-
-
Attendances
hr_attendance_kiosk_mode
@@ -232,6 +180,4 @@
-
-
diff --git a/addons/hr_attendance/views/hr_department_view.xml b/addons/hr_attendance/views/hr_department_view.xml
index 4b5fc67c9de..a2f845c126c 100644
--- a/addons/hr_attendance/views/hr_department_view.xml
+++ b/addons/hr_attendance/views/hr_department_view.xml
@@ -1,6 +1,5 @@
-
hr.department.kanban.inherit
hr.department
@@ -8,13 +7,11 @@
-
-
+
Attendances
-
diff --git a/addons/hr_attendance/views/hr_employee_view.xml b/addons/hr_attendance/views/hr_employee_view.xml
index ab347ca3ec0..416e65a60ff 100644
--- a/addons/hr_attendance/views/hr_employee_view.xml
+++ b/addons/hr_attendance/views/hr_employee_view.xml
@@ -53,6 +53,22 @@
+
@@ -108,6 +124,26 @@
+
+
+
@@ -120,6 +156,7 @@
+
diff --git a/addons/hr_attendance/views/res_config_settings_views.xml b/addons/hr_attendance/views/res_config_settings_views.xml
index 3ae3aafefea..54d49173cec 100644
--- a/addons/hr_attendance/views/res_config_settings_views.xml
+++ b/addons/hr_attendance/views/res_config_settings_views.xml
@@ -5,9 +5,9 @@
res.config.settings
-
-
-
+
+
+
+ Extra Hours
+
+
+
+
+
+
+
+
+
+ Compare attendance with working hours set on employee.
+
+
+
+
+
+
+
+
+
+ Allow a period of time (around working hours) where extra time will not be counted, in benefit of the company
+
+
Time Period Minutes
+
+
+
+
+ Allow a period of time (around working hours) where extra time will not be deducted, in benefit of the employee
+
+
Time Period Minutes
+
+
+
+
diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py
index 8f67aa3e971..854ca2d7bb0 100644
--- a/addons/hr_holidays/models/hr_leave_type.py
+++ b/addons/hr_holidays/models/hr_leave_type.py
@@ -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:
diff --git a/addons/hr_work_entry_holidays/tests/test_performance.py b/addons/hr_work_entry_holidays/tests/test_performance.py
index b3f72f8ed64..615b0faf33c 100644
--- a/addons/hr_work_entry_holidays/tests/test_performance.py
+++ b/addons/hr_work_entry_holidays/tests/test_performance.py
@@ -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()