Files
odoo_source/addons/hr_holidays/tests/test_leave_requests.py
T
Yannick Tivisse 4afdebbb7e [FIX] hr_holidays: Fix undeterministic crashing tests
The tests in hr_holidays/tests/test_company_leave.py seem like they randomly fail if the default value for
date_to is computed on a different second than the date_from (it's a dict of fields so not ordered).

Cfr https://runbot.odoo.com/runbot/build/3727451 2020-07-30 08:28:32 vs 2020-07-30 08:28:31
for which date_from > date_to.

I imagine the call to leave._onchange_request_parameters() is meant to fix it, but it occurs after
the create() which is flushed immediately, so it's too late and the date_from/date_to SQL constraint
still triggers.

It could happen for real production usage too, so we might want to do something about that outside of tests.
Options include:

1/ making the date_from/date_to fields writeable computed fields, with the compute method setting
   better defaults on both fields at once
2/ alternatively, remove the default=fields.Datetime.now and override default_get instead for
   deterministic defaults on both fields at once

The second solution seemed to be the better one.

closes odoo/odoo#55880

Taskid: 2312029
X-original-commit: 6908ea471dd18182f83c81046f0000a00898fb66
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
2020-08-14 09:29:30 +00:00

420 lines
21 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, date
from dateutil.relativedelta import relativedelta
from pytz import timezone, UTC
from odoo import fields
from odoo.exceptions import ValidationError
from odoo.tools import mute_logger
from odoo.tests.common import Form
from odoo.tests import tagged
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@tagged('leave_requests')
class TestLeaveRequests(TestHrHolidaysCommon):
def _check_holidays_status(self, holiday_status, ml, lt, rl, vrl):
self.assertEqual(holiday_status.max_leaves, ml,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.leaves_taken, lt,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.remaining_leaves, rl,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.virtual_remaining_leaves, vrl,
'hr_holidays: wrong type days computation')
def setUp(self):
super(TestLeaveRequests, self).setUp()
# Make sure we have the rights to create, validate and delete the leaves, leave types and allocations
LeaveType = self.env['hr.leave.type'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True)
self.holidays_type_1 = LeaveType.create({
'name': 'NotLimitedHR',
'allocation_type': 'no',
'leave_validation_type': 'hr',
'validity_start': False,
})
self.holidays_type_2 = LeaveType.create({
'name': 'Limited',
'allocation_type': 'fixed_allocation',
'leave_validation_type': 'hr',
'validity_start': False,
})
self.holidays_type_3 = LeaveType.create({
'name': 'TimeNotLimited',
'allocation_type': 'no',
'leave_validation_type': 'manager',
'validity_start': fields.Datetime.from_string('2017-01-01 00:00:00'),
'validity_stop': fields.Datetime.from_string('2017-06-01 00:00:00'),
})
self.set_employee_create_date(self.employee_emp_id, '2010-02-03 00:00:00')
self.set_employee_create_date(self.employee_hruser_id, '2010-02-03 00:00:00')
def set_employee_create_date(self, id, newdate):
""" This method is a hack in order to be able to define/redefine the create_date
of the employees.
This is done in SQL because ORM does not allow to write onto the create_date field.
"""
self.env.cr.execute("""
UPDATE
hr_employee
SET create_date = '%s'
WHERE id = %s
""" % (newdate, id))
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_overlapping_requests(self):
""" Employee cannot create a new leave request at the same time, avoid interlapping """
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hol11',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': datetime.today(),
'number_of_days': 1,
})
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hol21',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': datetime.today(),
'number_of_days': 1,
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_limited_type_no_days(self):
""" Employee creates a leave request in a limited category but has not enough days left """
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hol22',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'date_from': (datetime.today() + relativedelta(days=1)).strftime('%Y-%m-%d %H:%M'),
'date_to': (datetime.today() + relativedelta(days=2)),
'number_of_days': 1,
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_limited_type_days_left(self):
""" Employee creates a leave request in a limited category and has enough days left """
aloc1_user_group = self.env['hr.leave.allocation'].with_user(self.user_hruser_id).create({
'name': 'Days for limited category',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 2,
})
aloc1_user_group.action_approve()
holiday_status = self.holidays_type_2.with_user(self.user_employee_id)
self._check_holidays_status(holiday_status, 2.0, 0.0, 2.0, 2.0)
hol = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hol11',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'date_from': (datetime.today() - relativedelta(days=2)),
'date_to': datetime.today(),
'number_of_days': 2,
})
holiday_status.invalidate_cache()
self._check_holidays_status(holiday_status, 2.0, 0.0, 2.0, 0.0)
hol.with_user(self.user_hrmanager_id).action_approve()
holiday_status.invalidate_cache(['max_leaves'])
self._check_holidays_status(holiday_status, 2.0, 2.0, 0.0, 0.0)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_accrual_validity_time_valid(self):
""" Employee ask leave during a valid validity time """
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Valid time period',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_3.id,
'date_from': fields.Datetime.from_string('2017-03-03 06:00:00'),
'date_to': fields.Datetime.from_string('2017-03-11 19:00:00'),
'number_of_days': 1,
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_accrual_validity_time_not_valid(self):
""" Employee ask leav during a not valid validity time """
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Sick Time Off',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_3.id,
'date_from': fields.Datetime.from_string('2017-07-03 06:00:00'),
'date_to': fields.Datetime.from_string('2017-07-11 19:00:00'),
'number_of_days': 1,
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_department_leave(self):
""" Create a department leave """
self.employee_hrmanager.write({'department_id': self.hr_dept.id})
self.assertFalse(self.env['hr.leave'].search([('employee_id', 'in', self.hr_dept.member_ids.ids)]))
leave_form = Form(self.env['hr.leave'].with_user(self.user_hrmanager), view='hr_holidays.hr_leave_view_form_manager')
leave_form.holiday_type = 'department'
leave_form.department_id = self.hr_dept
leave_form.holiday_status_id = self.holidays_type_1
leave_form.request_date_from = date(2019, 5, 6)
leave_form.request_date_to = date(2019, 5, 6)
leave = leave_form.save()
leave.action_approve()
member_ids = self.hr_dept.member_ids.ids
self.assertEqual(self.env['hr.leave'].search_count([('employee_id', 'in', member_ids)]), len(member_ids), "Leave should be created for members of department")
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_allocation_request(self):
""" Create an allocation request """
# employee should be set to current user
allocation_form = Form(self.env['hr.leave.allocation'].with_user(self.user_employee))
allocation_form.name = 'New Allocation Request'
allocation_form.holiday_status_id = self.holidays_type_2
allocation = allocation_form.save()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_employee_is_absent(self):
""" Only the concerned employee should be considered absent """
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hol11',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'date_from': (fields.Datetime.now() - relativedelta(days=1)),
'date_to': fields.Datetime.now() + relativedelta(days=1),
'number_of_days': 2,
})
(self.employee_emp | self.employee_hrmanager).mapped('is_absent') # compute in batch
self.assertTrue(self.employee_emp.is_absent, "He should be considered absent")
self.assertFalse(self.employee_hrmanager.is_absent, "He should not be considered absent")
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_timezone_employee_leave_request(self):
""" Create a leave request for an employee in another timezone """
self.employee_emp.tz = 'NZ' # GMT+12
leave = self.env['hr.leave'].new({
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_1.id,
'request_unit_hours': True,
'request_date_from': date(2019, 5, 6),
'request_date_to': date(2019, 5, 6),
'request_hour_from': '8', # 8:00 AM in the employee's timezone
'request_hour_to': '17', # 5:00 PM in the employee's timezone
})
self.assertEqual(leave.date_from, datetime(2019, 5, 5, 20, 0, 0), "It should have been localized before saving in UTC")
self.assertEqual(leave.date_to, datetime(2019, 5, 6, 5, 0, 0), "It should have been localized before saving in UTC")
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_timezone_company_leave_request(self):
""" Create a leave request for a company in another timezone """
company = self.env['res.company'].create({'name': "Hergé"})
company.resource_calendar_id.tz = 'NZ' # GMT+12
leave = self.env['hr.leave'].new({
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_1.id,
'request_unit_hours': True,
'holiday_type': 'company',
'mode_company_id': company.id,
'request_date_from': date(2019, 5, 6),
'request_date_to': date(2019, 5, 6),
'request_hour_from': '8', # 8:00 AM in the company's timezone
'request_hour_to': '17', # 5:00 PM in the company's timezone
})
self.assertEqual(leave.date_from, datetime(2019, 5, 5, 20, 0, 0), "It should have been localized before saving in UTC")
self.assertEqual(leave.date_to, datetime(2019, 5, 6, 5, 0, 0), "It should have been localized before saving in UTC")
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_timezone_department_leave_request(self):
""" Create a leave request for a department in another timezone """
company = self.env['res.company'].create({'name': "Hergé"})
company.resource_calendar_id.tz = 'NZ' # GMT+12
department = self.env['hr.department'].create({'name': "Museum", 'company_id': company.id})
leave = self.env['hr.leave'].new({
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_1.id,
'request_unit_hours': True,
'holiday_type': 'department',
'department_id': department.id,
'request_date_from': date(2019, 5, 6),
'request_date_to': date(2019, 5, 6),
'request_hour_from': '8', # 8:00 AM in the department's timezone
'request_hour_to': '17', # 5:00 PM in the department's timezone
})
self.assertEqual(leave.date_from, datetime(2019, 5, 5, 20, 0, 0), "It should have been localized before saving in UTC")
self.assertEqual(leave.date_to, datetime(2019, 5, 6, 5, 0, 0), "It should have been localized before saving in UTC")
def test_number_of_hours_display(self):
# Test that the field number_of_hours_dispay doesn't change
# after time off validation, as it takes the attendances
# minus the resource leaves to compute that field.
calendar = self.env['resource.calendar'].create({
'name': 'Monday Morning Else Full Time 38h/week',
'hours_per_day': 7.6,
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17.5, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17.5, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17.5, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17.5, 'day_period': 'afternoon'})
],
})
employee = self.employee_emp
employee.resource_calendar_id = calendar
self.env.user.company_id.resource_calendar_id = calendar
leave_type = self.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'request_unit': 'hour',
'leave_validation_type': 'both',
})
allocation = self.env['hr.leave.allocation'].create({
'name': '20 days allocation',
'holiday_status_id': leave_type.id,
'number_of_days': 20,
'employee_id': employee.id,
})
allocation.action_approve()
leave1 = self.env['hr.leave'].create({
'name': 'Holiday 1 week',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'date_from': fields.Datetime.from_string('2019-12-23 06:00:00'),
'date_to': fields.Datetime.from_string('2019-12-27 20:00:00'),
'number_of_days': 5,
})
self.assertEqual(leave1.number_of_hours_display, 38)
leave1.action_approve()
self.assertEqual(leave1.number_of_hours_display, 38)
leave1.action_validate()
self.assertEqual(leave1.number_of_hours_display, 38)
leave2 = self.env['hr.leave'].create({
'name': 'Holiday 1 Day',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'date_from': fields.Datetime.from_string('2019-12-30 06:00:00'),
'date_to': fields.Datetime.from_string('2019-12-30 13:00:00'),
'number_of_days': 1,
})
self.assertEqual(leave2.number_of_hours_display, 4)
leave2.action_approve()
self.assertEqual(leave2.number_of_hours_display, 4)
leave2.action_validate()
self.assertEqual(leave2.number_of_hours_display, 4)
def test_number_of_hours_display_global_leave(self):
# Check that the field number_of_hours_display
# takes the global leaves into account, even
# after validation
calendar = self.env['resource.calendar'].create({
'name': 'Classic 40h/week',
'hours_per_day': 8.0,
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'})
],
'global_leave_ids': [(0, 0, {
'name': 'Christmas Leave',
'date_from': fields.Datetime.from_string('2019-12-25 00:00:00'),
'date_to': fields.Datetime.from_string('2019-12-26 23:59:59'),
'resource_id': False,
'time_type': 'leave',
})]
})
employee = self.employee_emp
employee.resource_calendar_id = calendar
self.env.user.company_id.resource_calendar_id = calendar
leave_type = self.env['hr.leave.type'].create({
'name': 'Sick',
'request_unit': 'hour',
'leave_validation_type': 'both',
'allocation_type': 'no',
})
leave1 = self.env['hr.leave'].create({
'name': 'Sick 1 week during christmas snif',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'date_from': fields.Datetime.from_string('2019-12-23 06:00:00'),
'date_to': fields.Datetime.from_string('2019-12-27 20:00:00'),
'number_of_days': 5,
})
self.assertEqual(leave1.number_of_hours_display, 24)
leave1.action_approve()
self.assertEqual(leave1.number_of_hours_display, 24)
leave1.action_validate()
self.assertEqual(leave1.number_of_hours_display, 24)
def _test_leave_with_tz(self, tz, local_date_from, local_date_to, number_of_days):
self.user_employee.tz = tz
tz = timezone(tz)
# Mimic what is done by the calendar widget when clicking on a day. It
# will take the local datetime from 7:00 to 19:00 and then convert it
# to UTC before sending it. Values here are for PST (UTC -8) and
# represent a leave on 2019/1/1 from 7:00 to 19:00 local time.
values = {
'date_from': tz.localize(local_date_from).astimezone(UTC).replace(tzinfo=None),
'date_to': tz.localize(local_date_to).astimezone(UTC).replace(tzinfo=None), # note that this can be the next day in UTC
}
values.update(self.env['hr.leave'].with_user(self.user_employee_id)._default_get_request_parameters(values))
# Dates should be local to the user
self.assertEqual(values['request_date_from'], local_date_from.date())
self.assertEqual(values['request_date_to'], local_date_to.date())
values.update({
'name': 'Test',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
})
leave = self.env['hr.leave'].with_user(self.user_employee_id).new(values)
self.assertEqual(leave.number_of_days, number_of_days)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_defaults_with_timezones(self):
""" Make sure that leaves start with correct defaults for non-UTC timezones """
timezones_to_test = ('UTC', 'Pacific/Midway', 'US/Pacific', 'Asia/Taipei', 'Pacific/Kiritimati') # UTC, UTC -11, UTC -8, UTC +8, UTC +14
# January 2020
# Su Mo Tu We Th Fr Sa
# 1 2 3 4
# 5 6 7 8 9 10 11
# 12 13 14 15 16 17 18
# 19 20 21 22 23 24 25
# 26 27 28 29 30 31
local_date_from = datetime(2019, 1, 1, 7, 0, 0)
local_date_to = datetime(2019, 1, 1, 19, 0, 0)
for tz in timezones_to_test:
self._test_leave_with_tz(tz, local_date_from, local_date_to, 1)
# We, Th, Fr, Mo, Tu, We => 6 days
local_date_from = datetime(2019, 1, 1, 7, 0, 0)
local_date_to = datetime(2019, 1, 8, 19, 0, 0)
for tz in timezones_to_test:
self._test_leave_with_tz(tz, local_date_from, local_date_to, 6)