diff --git a/addons/hr_payroll/models/hr_benefit.py b/addons/hr_payroll/models/hr_benefit.py index 866709f241e..74cc3db97b0 100644 --- a/addons/hr_payroll/models/hr_benefit.py +++ b/addons/hr_payroll/models/hr_benefit.py @@ -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 } diff --git a/addons/hr_payroll/models/hr_employee.py b/addons/hr_payroll/models/hr_employee.py index cbcbb0d8820..54a8edc235e 100644 --- a/addons/hr_payroll/models/hr_employee.py +++ b/addons/hr_payroll/models/hr_employee.py @@ -1,11 +1,11 @@ # -*- 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', diff --git a/addons/hr_payroll/tests/test_benefit.py b/addons/hr_payroll/tests/test_benefit.py index ec1b4c25248..e80f7266c63 100644 --- a/addons/hr_payroll/tests/test_benefit.py +++ b/addons/hr_payroll/tests/test_benefit.py @@ -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,