Skip to content
Snippets Groups Projects
Commit 6878c9f3 authored by RomainLibert's avatar RomainLibert
Browse files

[FIX] hr_payroll: handle timezones coherently in hr.benefit


This commit simplifies the way the timezones are handled in hr_benefit.
Creating a benefit now uses naive datetimes obtained by converting from
the employee's timezone into UTC and then stripping timezone
information.

closes odoo/odoo#31968

Signed-off-by: default avatarYannick Tivisse (yti) <yti@odoo.com>
parent 20ae38a6
No related branches found
No related tags found
No related merge requests found
......@@ -139,7 +139,9 @@ class HrBenefit(models.Model):
r['employee_id'][0],
r['benefit_type_id'][0] if r['benefit_type_id'] else False,
) for r in month_recs}
new_vals = [v for v in vals_list if (v['date_start'].replace(tzinfo=None), v['date_stop'].replace(tzinfo=None), v['employee_id'], v['benefit_type_id']) not in existing_entries]
assert all(v['date_start'].tzinfo is None for v in vals_list)
assert all(v['date_stop'].tzinfo is None for v in vals_list)
new_vals = [v for v in vals_list if (v['date_start'], v['date_stop'], v['employee_id'], v['benefit_type_id']) not in existing_entries]
# Remove duplicates from vals_list, shouldn't be necessary from saas-12.2
unique_new_vals = set()
for values in new_vals:
......@@ -184,8 +186,6 @@ class HrBenefit(models.Model):
if benefit.date_start.date() == benefit.date_stop.date():
new_benefits |= benefit
else:
tz = pytz.timezone(benefit.employee_id.tz)
benefit_start, benefit_stop = tz.localize(benefit.date_start), tz.localize(benefit.date_stop)
values = {
'name': benefit.name,
'employee_id': benefit.employee_id.id,
......@@ -193,9 +193,9 @@ class HrBenefit(models.Model):
}
benefit_state = benefit.state
benefits_to_unlink |= benefit
for start, stop in _split_range_by_day(benefit_start, benefit_stop):
values['date_start'] = start.astimezone(pytz.utc)
values['date_stop'] = stop.astimezone(pytz.utc)
for start, stop in _split_range_by_day(benefit.date_start, benefit.date_stop):
values['date_start'] = start
values['date_stop'] = stop
new_benefit = self.create(values)
# Write the state after the creation due to the ir.rule on benefit state
new_benefit.state = benefit_state
......@@ -244,8 +244,8 @@ class HrBenefit(models.Model):
def _duplicate_to_calendar_attendance(self):
mapped_data = {
benefit: [
pytz.utc.localize(benefit.date_start).astimezone(pytz.timezone(benefit.employee_id.tz)), # Start date
pytz.utc.localize(benefit.date_stop).astimezone(pytz.timezone(benefit.employee_id.tz)) # End date
benefit.date_start,
benefit.date_stop,
] for benefit in self
}
......
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.addons.resource.models.resource import Intervals
from datetime import datetime, timedelta
from datetime import datetime
import pytz
from odoo import api, fields, models
class HrEmployee(models.Model):
_inherit = 'hr.employee'
......@@ -22,20 +22,18 @@ class HrEmployee(models.Model):
@api.multi
def generate_benefit(self, date_start, date_stop):
date_start = date_start.replace(tzinfo=pytz.utc)
date_stop = date_stop.replace(tzinfo=pytz.utc)
date_start = fields.Datetime.to_datetime(date_start)
date_stop = fields.Datetime.to_datetime(date_stop)
attendance_type = self.env.ref('hr_payroll.benefit_type_attendance')
vals_list = []
leaves = self.env['hr.leave']
for employee in self:
# Approved leaves
emp_leaves = employee.resource_calendar_id.leave_ids.filtered(
lambda r:
r.resource_id == employee.resource_id and
r.date_from.replace(tzinfo=pytz.utc) <= date_stop and
r.date_to.replace(tzinfo=pytz.utc) >= date_start
r.date_from <= date_stop and
r.date_to >= date_start
)
global_leaves = employee.resource_calendar_id.global_leave_ids
......@@ -43,9 +41,8 @@ class HrEmployee(models.Model):
vals_list.extend(employee_leaves._get_benefits_values())
for contract in employee._get_contracts(date_start, date_stop):
date_start_benefits = max(date_start, datetime.combine(contract.date_start, datetime.min.time()).replace(tzinfo=pytz.utc))
date_stop_benefits = min(date_stop, datetime.combine(contract.date_end or datetime.max.date(), datetime.max.time()).replace(tzinfo=pytz.utc))
date_start_benefits = max(date_start, fields.Datetime.to_datetime(contract.date_start)).replace(tzinfo=pytz.utc)
date_stop_benefits = min(date_stop, datetime.combine(contract.date_end or datetime.max.date(), datetime.max.time())).replace(tzinfo=pytz.utc)
calendar = contract.resource_calendar_id
resource = employee.resource_id
......@@ -53,10 +50,11 @@ class HrEmployee(models.Model):
# Attendances
for interval in attendances:
benefit_type_id = interval[2].mapped('benefit_type_id')[:1] or attendance_type
# All benefits generated here are using datetimes converted from the employee's timezone
vals_list += [{
'name': "%s: %s" % (benefit_type_id.name, employee.name),
'date_start': interval[0].astimezone(pytz.utc),
'date_stop': interval[1].astimezone(pytz.utc),
'date_start': interval[0].astimezone(pytz.utc).replace(tzinfo=None),
'date_stop': interval[1].astimezone(pytz.utc).replace(tzinfo=None),
'benefit_type_id': benefit_type_id.id,
'employee_id': employee.id,
'state': 'confirmed',
......
......@@ -2,9 +2,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
import pytz
from dateutil.relativedelta import relativedelta
from odoo import exceptions
import pytz
from odoo.fields import Datetime, Date
from odoo.tests.common import tagged
from odoo.addons.hr_payroll.tests.common import TestPayslipBase
......@@ -12,15 +12,14 @@ from odoo.addons.hr_payroll.tests.common import TestPayslipBase
@tagged('benefit')
class TestBenefit(TestPayslipBase):
def setUp(self):
super(TestBenefit, self).setUp()
self.tz = pytz.timezone(self.richard_emp.tz)
self.start = self.to_datetime_tz('2015-11-01 01:00:00')
self.end = self.to_datetime_tz('2015-11-30 23:59:59')
self.start = datetime(2015, 11, 1, 1, 0, 0)
self.end = datetime(2015, 11, 30, 23, 59, 59)
self.resource_calendar_id = self.env['resource.calendar'].create({'name': 'Zboub'})
self.env['hr.contract'].create({
'date_start': self.start - relativedelta(days=5),
'date_start': self.start.date() - relativedelta(days=5),
'name': 'dodo',
'resource_calendar_id': self.resource_calendar_id.id,
'wage': 1000,
......@@ -47,24 +46,6 @@ class TestBenefit(TestPayslipBase):
'code': 'WORK200'
})
def to_datetime_tz(self, datetime_str, tz=None):
tz = tz or self.tz
return tz.localize(Datetime.to_datetime(datetime_str))
def assertDatetimeTzEqual(self, value, target, tz=None):
"""
Assert equality between two dates.
:param value: timezone naive datetime
:param target: datetime string
:param tz: timezone to interpret the tartget string
:raises AssertionError: raises exception if the two dates are not equal
"""
tz = tz or self.tz
self.assertEqual(
pytz.utc.localize(value).astimezone(tz),
self.to_datetime_tz(target, tz=tz)
)
def test_no_duplicate(self):
self.richard_emp.generate_benefit(self.start, self.end)
pou1 = self.env['hr.benefit'].search_count([])
......@@ -73,16 +54,14 @@ class TestBenefit(TestPayslipBase):
self.assertEqual(pou1, pou2, "Benefits should not be duplicated")
def test_benefit(self):
self.richard_emp.generate_benefit(self.start, self.end)
attendance_nb = len(self.resource_calendar_id._attendance_intervals(self.start, self.end))
attendance_nb = len(self.resource_calendar_id._attendance_intervals(self.start.replace(tzinfo=pytz.utc), self.end.replace(tzinfo=pytz.utc)))
benefit_nb = self.env['hr.benefit'].search_count([('employee_id', '=', self.richard_emp.id)])
self.assertEqual(attendance_nb, benefit_nb, "One benefit should be generated for each calendar attendance")
def test_split_benefit_by_day(self):
start = self.to_datetime_tz('2015-11-01 09:00:00')
end = self.to_datetime_tz('2015-11-03 18:00:00')
start = datetime(2015, 11, 1, 9, 0, 0)
end = datetime(2015, 11, 3, 18, 0, 0)
# Benefit of type attendance should be split in three
benefit = self.env['hr.benefit'].create({
......@@ -95,18 +74,18 @@ class TestBenefit(TestPayslipBase):
benefits = benefit._split_by_day()
self.assertEqual(len(benefits), 3, "Benefit should be split in three")
self.assertDatetimeTzEqual(benefits[0].date_start, '2015-11-01 09:00:00')
self.assertDatetimeTzEqual(benefits[0].date_stop, '2015-11-01 23:59:59')
self.assertEqual(benefits[0].date_start, datetime(2015, 11, 1, 9, 0, 0))
self.assertEqual(benefits[0].date_stop, datetime(2015, 11, 1, 23, 59, 59))
self.assertDatetimeTzEqual(benefits[1].date_start, '2015-11-02 00:00:00')
self.assertDatetimeTzEqual(benefits[1].date_stop, '2015-11-02 23:59:59')
self.assertEqual(benefits[1].date_start, datetime(2015, 11, 2, 0, 0, 0))
self.assertEqual(benefits[1].date_stop, datetime(2015, 11, 2, 23, 59, 59))
self.assertDatetimeTzEqual(benefits[2].date_start, '2015-11-03 00:00:00')
self.assertDatetimeTzEqual(benefits[2].date_stop, '2015-11-03 18:00:00')
self.assertEqual(benefits[2].date_start, datetime(2015, 11, 3, 0, 0, 0))
self.assertEqual(benefits[2].date_stop, datetime(2015, 11, 3, 18, 0, 0))
# Test with end at mid-night -> should not create benefit starting and ending at the same time (at 00:00)
start = self.to_datetime_tz('2013-11-01 00:00:00')
end = self.to_datetime_tz('2013-11-04 00:00:00')
start = datetime(2013, 11, 1, 0, 0, 0)
end = datetime(2013, 11, 4, 0, 0, 0)
benefit = self.env['hr.benefit'].create({
'name': '1',
......@@ -116,19 +95,18 @@ class TestBenefit(TestPayslipBase):
})
benefits = benefit._split_by_day()
self.assertEqual(len(benefits), 3, "Benefit should be split in three")
self.assertDatetimeTzEqual(benefits[0].date_start, '2013-11-01 00:00:00')
self.assertDatetimeTzEqual(benefits[0].date_stop, '2013-11-01 23:59:59')
self.assertEqual(benefits[0].date_start, datetime(2013, 11, 1, 0, 0, 0))
self.assertEqual(benefits[0].date_stop, datetime(2013, 11, 1, 23, 59, 59))
self.assertDatetimeTzEqual(benefits[1].date_start, '2013-11-02 00:00:00')
self.assertDatetimeTzEqual(benefits[1].date_stop, '2013-11-02 23:59:59')
self.assertEqual(benefits[1].date_start, datetime(2013, 11, 2, 0, 0, 0))
self.assertEqual(benefits[1].date_stop, datetime(2013, 11, 2, 23, 59, 59))
self.assertDatetimeTzEqual(benefits[2].date_start, '2013-11-03 00:00:00')
self.assertDatetimeTzEqual(benefits[2].date_stop, '2013-11-03 23:59:59')
self.assertEqual(benefits[2].date_start, datetime(2013, 11, 3, 0, 0, 0))
self.assertEqual(benefits[2].date_stop, datetime(2013, 11, 3, 23, 59, 59))
def test_approve_multiple_day_benefit(self):
start = self.to_datetime_tz('2015-11-01 09:00:00')
end = self.to_datetime_tz('2015-11-03 18:00:00')
start = datetime(2015, 11, 1, 9, 0, 0)
end = datetime(2015, 11, 3, 18, 0, 0)
# Benefit of type attendance should be split in three
benefit = self.env['hr.benefit'].create({
......@@ -144,8 +122,8 @@ class TestBenefit(TestPayslipBase):
self.assertEqual(len(benefits), 3, "Benefit should be split in three")
def test_duplicate_global_benefit_to_attendance(self):
start = self.to_datetime_tz('2015-11-01 09:00:00')
end = self.to_datetime_tz('2015-11-03 18:00:00')
start = datetime(2015, 11, 1, 9, 0, 0)
end = datetime(2015, 11, 3, 18, 0, 0)
benef = self.env['hr.benefit'].create({
'name': '1',
......@@ -162,8 +140,8 @@ class TestBenefit(TestPayslipBase):
self.assertEqual(attendance_nb, 0, "It should not duplicate the 'normal/global' benefit type")
def test_duplicate_benefit_to_attendance(self):
start = self.to_datetime_tz('2015-11-01 09:00:00')
end = self.to_datetime_tz('2015-11-03 18:00:00')
start = datetime(2015, 11, 1, 9, 0, 0)
end = datetime(2015, 11, 3, 18, 0, 0)
# Benefit (not leave) should be split in three attendance
benef = self.env['hr.benefit'].create({
......@@ -199,8 +177,8 @@ class TestBenefit(TestPayslipBase):
]))
def test_create_benefit_leave(self):
start = self.to_datetime_tz('2015-11-01 09:00:00')
end = self.to_datetime_tz('2015-11-03 18:00:00')
start = datetime(2015, 11, 1, 9, 0, 0)
end = datetime(2015, 11, 3, 18, 0, 0)
benef = self.env['hr.benefit'].create({
'name': 'Richard leave from benef',
......@@ -215,8 +193,8 @@ class TestBenefit(TestPayslipBase):
self.assertEqual(calendar_leave.benefit_type_id, benef.benefit_type_id, "It should have the same benefit type")
def test_validate_conflict_benefit(self):
start = self.to_datetime_tz('2015-11-01 09:00:00')
end = self.to_datetime_tz('2015-11-01 13:00:00')
start = datetime(2015, 11, 1, 9, 0, 0)
end = datetime(2015, 11, 1, 13, 0, 0)
benef1 = self.env['hr.benefit'].create({
'name': '1',
'employee_id': self.richard_emp.id,
......@@ -224,7 +202,7 @@ class TestBenefit(TestPayslipBase):
'date_start': start,
'date_stop': end + relativedelta(hours=5),
})
benef2 = self.env['hr.benefit'].create({
self.env['hr.benefit'].create({
'name': '2',
'employee_id': self.richard_emp.id,
'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id,
......@@ -243,7 +221,7 @@ class TestBenefit(TestPayslipBase):
'date_start': self.start,
'date_stop': self.end,
})
leave_1 = self.env['hr.leave'].create({
self.env['hr.leave'].create({
'name': 'Doctor Appointment',
'employee_id': self.richard_emp.id,
'holiday_status_id': self.leave_type.id,
......@@ -251,7 +229,7 @@ class TestBenefit(TestPayslipBase):
'date_to': self.start + relativedelta(days=1),
'number_of_days': 2,
})
self.assertFalse(benef1.action_validate(benef1.ids),"It should not validate benefits conflicting with non approved leaves")
self.assertFalse(benef1.action_validate(benef1.ids), "It should not validate benefits conflicting with non approved leaves")
self.assertTrue(benef1.display_warning)
def test_validate_undefined_benefit(self):
......@@ -261,11 +239,11 @@ class TestBenefit(TestPayslipBase):
'date_start': self.start,
'date_stop': self.end,
})
self.assertFalse(benef1.action_validate(benef1.ids),"It should not validate benefits without a type")
self.assertFalse(benef1.action_validate(benef1.ids), "It should not validate benefits without a type")
def test_approve_leave_benefit(self):
start = self.to_datetime_tz('2015-11-01 09:00:00')
end = self.to_datetime_tz('2015-11-03 13:00:00')
start = datetime(2015, 11, 1, 9, 0, 0)
end = datetime(2015, 11, 3, 13, 0, 0)
leave = self.env['hr.leave'].create({
'name': 'Doctor Appointment',
'employee_id': self.richard_emp.id,
......@@ -285,13 +263,13 @@ class TestBenefit(TestPayslipBase):
leave.action_approve()
new_leave_benef = self.env['hr.benefit'].search([
('date_start', '=', Datetime.to_datetime('2015-11-01 09:00:00')),
('date_stop', '=', Datetime.to_datetime('2015-11-02 09:00:00')),
('date_start', '=', Datetime.to_datetime(datetime(2015, 11, 1, 9, 0, 0))),
('date_stop', '=', Datetime.to_datetime(datetime(2015, 11, 2, 9, 0, 0))),
('benefit_type_id.is_leave', '=', True)
])
new_benef = self.env['hr.benefit'].search([
('date_start', '=', Datetime.to_datetime('2015-11-02 09:00:01')),
('date_start', '=', Datetime.to_datetime(datetime(2015, 11, 2, 9, 0, 1))),
('date_stop', '=', end),
('benefit_type_id.is_leave', '=', False)
])
......@@ -302,8 +280,8 @@ class TestBenefit(TestPayslipBase):
self.assertTrue(benef.action_validate((new_benef | new_leave_benef).ids), "It should be able to validate the benefits")
def test_refuse_leave_benefit(self):
start = self.to_datetime_tz('2015-11-01 09:00:00')
end = self.to_datetime_tz('2015-11-03 13:00:00')
start = datetime(2015, 11, 1, 9, 0, 0)
end = datetime(2015, 11, 3, 13, 0, 0)
leave = self.env['hr.leave'].create({
'name': 'Doctor Appointment',
'employee_id': self.richard_emp.id,
......@@ -331,8 +309,8 @@ class TestBenefit(TestPayslipBase):
self.assertEqual(data['hours'], 168.0)
def test_time_extra_benefit(self):
start = self.to_datetime_tz('2015-11-01 10:00:00')
end = self.to_datetime_tz('2015-11-01 17:00:00')
start = datetime(2015, 11, 1, 10, 0, 0)
end = datetime(2015, 11, 1, 17, 0, 0)
benef = self.env['hr.benefit'].create({
'name': '1',
'employee_id': self.richard_emp.id,
......@@ -346,8 +324,8 @@ class TestBenefit(TestPayslipBase):
def test_time_week_leave_benefit(self):
# /!\ this is a week day => it exists an calendar attendance at this time
start = self.to_datetime_tz('2015-11-02 10:00:00', tz=pytz.utc)
end = self.to_datetime_tz('2015-11-02 17:00:00', tz=pytz.utc)
start = datetime(2015, 11, 2, 10, 0, 0)
end = datetime(2015, 11, 2, 17, 0, 0)
leave_benef = self.env['hr.benefit'].create({
'name': '1leave',
'employee_id': self.richard_emp.id,
......@@ -361,8 +339,8 @@ class TestBenefit(TestPayslipBase):
def test_time_weekend_leave_benefit(self):
# /!\ this is in the weekend => no calendar attendance at this time
start = self.to_datetime_tz('2015-11-01 10:00:00', tz=pytz.utc)
end = self.to_datetime_tz('2015-11-01 17:00:00', tz=pytz.utc)
start = datetime(2015, 11, 1, 10, 0, 0)
end = datetime(2015, 11, 1, 17, 0, 0)
leave_benef = self.env['hr.benefit'].create({
'name': '1leave',
'employee_id': self.richard_emp.id,
......@@ -376,8 +354,8 @@ class TestBenefit(TestPayslipBase):
def test_payslip_generation_with_leave(self):
# /!\ this is a week day => it exists an calendar attendance at this time
start = self.to_datetime_tz('2015-11-02 10:00:00', tz=pytz.utc)
end = self.to_datetime_tz('2015-11-02 17:00:00', tz=pytz.utc)
start = datetime(2015, 11, 2, 10, 0, 0)
end = datetime(2015, 11, 2, 17, 0, 0)
leave_benef = self.env['hr.benefit'].create({
'name': '1leave',
'employee_id': self.richard_emp.id,
......@@ -399,8 +377,8 @@ class TestBenefit(TestPayslipBase):
def test_payslip_generation_with_extra_work(self):
# /!\ this is in the weekend (Sunday) => no calendar attendance at this time
start = self.to_datetime_tz('2015-11-01 10:00:00', tz=pytz.utc)
end = self.to_datetime_tz('2015-11-01 17:00:00', tz=pytz.utc)
start = datetime(2015, 11, 1, 10, 0, 0)
end = datetime(2015, 11, 1, 17, 0, 0)
benef = self.env['hr.benefit'].create({
'name': 'Extra',
'employee_id': self.richard_emp.id,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment