From c73ed3093743584635bfde788ce0eac73e61ab83 Mon Sep 17 00:00:00 2001 From: hugues de keyzer Date: Wed, 20 Oct 2021 11:22:00 +0200 Subject: [PATCH 01/17] [ADD] new module resource_work_time_from_contracts --- resource_work_time_from_contracts/README.rst | 67 +++ resource_work_time_from_contracts/__init__.py | 1 + .../__manifest__.py | 19 + .../models/__init__.py | 1 + .../models/resource_mixin.py | 88 ++++ .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 9 + .../static/description/index.html | 423 ++++++++++++++++++ .../tests/__init__.py | 1 + .../tests/test_work_time.py | 225 ++++++++++ 10 files changed, 837 insertions(+) create mode 100644 resource_work_time_from_contracts/README.rst create mode 100644 resource_work_time_from_contracts/__init__.py create mode 100644 resource_work_time_from_contracts/__manifest__.py create mode 100644 resource_work_time_from_contracts/models/__init__.py create mode 100644 resource_work_time_from_contracts/models/resource_mixin.py create mode 100644 resource_work_time_from_contracts/readme/CONTRIBUTORS.rst create mode 100644 resource_work_time_from_contracts/readme/DESCRIPTION.rst create mode 100644 resource_work_time_from_contracts/static/description/index.html create mode 100644 resource_work_time_from_contracts/tests/__init__.py create mode 100644 resource_work_time_from_contracts/tests/test_work_time.py diff --git a/resource_work_time_from_contracts/README.rst b/resource_work_time_from_contracts/README.rst new file mode 100644 index 000000000..c495dd51c --- /dev/null +++ b/resource_work_time_from_contracts/README.rst @@ -0,0 +1,67 @@ +================================= +Resource Work Time From Contracts +================================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-coopiteasy%2Faddons-lightgray.png?logo=github + :target: https://github.com/coopiteasy/addons/tree/12.0/resource_work_time_from_contracts + :alt: coopiteasy/addons + +|badge1| |badge2| |badge3| + +Take the contracts of an employee into account when computing work time per +day. + +When this module is installed, the number of hours an employee is supposed to +work is only computed from their contracts. Without contracts, the work time +per day is 0, instead of using the default company’s work schedule. + +The start and end dates of contracts are taken into account, but the status +(state) of the contract is ignored. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Coop IT Easy SCRLfs + +Contributors +~~~~~~~~~~~~ + +* `Coop IT Easy SCRLfs `_: + + * hugues de keyzer + +Maintainers +~~~~~~~~~~~ + +This module is part of the `coopiteasy/addons `_ project on GitHub. + +You are welcome to contribute. diff --git a/resource_work_time_from_contracts/__init__.py b/resource_work_time_from_contracts/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/resource_work_time_from_contracts/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/resource_work_time_from_contracts/__manifest__.py b/resource_work_time_from_contracts/__manifest__.py new file mode 100644 index 000000000..1ba302dc5 --- /dev/null +++ b/resource_work_time_from_contracts/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2021 Coop IT Easy SCRLfs +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Resource Work Time From Contracts", + "summary": ( + "Take the contracts of an employee into account when computing work " + "time per day" + ), + "version": "12.0.1.0.0", + "license": "AGPL-3", + "author": "Coop IT Easy SCRLfs", + "website": "https://coopiteasy.be", + "depends": [ + "hr_contract", + ], + "data": [], + "demo": [], +} diff --git a/resource_work_time_from_contracts/models/__init__.py b/resource_work_time_from_contracts/models/__init__.py new file mode 100644 index 000000000..944de4292 --- /dev/null +++ b/resource_work_time_from_contracts/models/__init__.py @@ -0,0 +1 @@ +from . import resource_mixin diff --git a/resource_work_time_from_contracts/models/resource_mixin.py b/resource_work_time_from_contracts/models/resource_mixin.py new file mode 100644 index 000000000..e4972729b --- /dev/null +++ b/resource_work_time_from_contracts/models/resource_mixin.py @@ -0,0 +1,88 @@ +# Copyright 2021 Coop IT Easy SCRLfs +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import datetime +from collections import defaultdict + +from odoo import models + + +class ResourceMixin(models.AbstractModel): + + _inherit = "resource.mixin" + + def list_work_time_per_day( + self, from_datetime, to_datetime, calendar=None, domain=None + ): + default_work_time = super().list_work_time_per_day( + from_datetime, to_datetime, calendar, domain + ) + if calendar or not hasattr(self, "contract_ids"): + return default_work_time + contracts = self._get_active_contracts( + from_datetime.date(), to_datetime.date() + ) + work_time_results = self._get_work_time_per_contract( + contracts, from_datetime, to_datetime, domain + ) + result = [] + for work_time_per_day in default_work_time: + day = work_time_per_day[0] + hours = 0.0 + for work_time in work_time_results: + hours += work_time[day] + result.append((day, hours)) + return result + + def _get_active_contracts(self, date_start, date_end): + """ + Get active contracts for the provided date range. + """ + return ( + self.env["hr.contract"] + .sudo() + .search( + [ + ("employee_id", "=", self.id), + ("date_start", "<=", date_end), + "|", + ("date_end", "=", None), + ("date_end", ">=", date_start), + ] + ) + ) + + def _get_work_time_per_contract( + self, contracts, from_datetime, to_datetime, domain + ): + """ + Return the work time per day per contract. + """ + work_time_results = [] + for contract in contracts: + from_dt = from_datetime + to_dt = to_datetime + date_start = contract.date_start + if from_dt.date() < date_start: + from_dt = datetime.datetime( + date_start.year, date_start.month, date_start.day + ) + date_end = contract.date_end + if date_end and to_dt.date() > date_end: + # limit to midnight on the day after date_end to completely + # include date_end. + to_dt = datetime.datetime( + date_end.year, date_end.month, date_end.day + ) + datetime.timedelta(days=1) + work_time_results.append( + defaultdict( + float, + super().list_work_time_per_day( + from_dt, + to_dt, + contract.resource_calendar_id, + domain, + ), + ) + ) + return work_time_results diff --git a/resource_work_time_from_contracts/readme/CONTRIBUTORS.rst b/resource_work_time_from_contracts/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..f1efe98f1 --- /dev/null +++ b/resource_work_time_from_contracts/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Coop IT Easy SCRLfs `_: + + * hugues de keyzer diff --git a/resource_work_time_from_contracts/readme/DESCRIPTION.rst b/resource_work_time_from_contracts/readme/DESCRIPTION.rst new file mode 100644 index 000000000..489300499 --- /dev/null +++ b/resource_work_time_from_contracts/readme/DESCRIPTION.rst @@ -0,0 +1,9 @@ +Take the contracts of an employee into account when computing work time per +day. + +When this module is installed, the number of hours an employee is supposed to +work is only computed from their contracts. Without contracts, the work time +per day is 0, instead of using the default company’s work schedule. + +The start and end dates of contracts are taken into account, but the status +(state) of the contract is ignored. diff --git a/resource_work_time_from_contracts/static/description/index.html b/resource_work_time_from_contracts/static/description/index.html new file mode 100644 index 000000000..da389381f --- /dev/null +++ b/resource_work_time_from_contracts/static/description/index.html @@ -0,0 +1,423 @@ + + + + + + +Resource Work Time From Contracts + + + +
+

Resource Work Time From Contracts

+ + +

Beta License: AGPL-3 coopiteasy/addons

+

Take the contracts of an employee into account when computing work time per +day.

+

When this module is installed, the number of hours an employee is supposed to +work is only computed from their contracts. Without contracts, the work time +per day is 0, instead of using the default company’s work schedule.

+

The start and end dates of contracts are taken into account, but the status +(state) of the contract is ignored.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Coop IT Easy SCRLfs
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the coopiteasy/addons project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/resource_work_time_from_contracts/tests/__init__.py b/resource_work_time_from_contracts/tests/__init__.py new file mode 100644 index 000000000..b5d15aa3b --- /dev/null +++ b/resource_work_time_from_contracts/tests/__init__.py @@ -0,0 +1 @@ +from . import test_work_time diff --git a/resource_work_time_from_contracts/tests/test_work_time.py b/resource_work_time_from_contracts/tests/test_work_time.py new file mode 100644 index 000000000..2d3953613 --- /dev/null +++ b/resource_work_time_from_contracts/tests/test_work_time.py @@ -0,0 +1,225 @@ +# Copyright 2021 Coop IT Easy SCRLfs +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import date, datetime, timedelta + +from odoo.tests.common import TransactionCase + + +class TestWorkTime(TransactionCase): + def setUp(self): + super().setUp() + + # users + user1_dict = {"name": "User 1", "login": "user1", "password": "user1"} + self.user1 = self.env["res.users"].create(user1_dict) + + # employees + employee1_dict = { + "name": "Employee 1", + "user_id": self.user1.id, + "address_id": self.user1.partner_id.id, + } + self.employee1 = self.env["hr.employee"].create(employee1_dict) + + # working hours + # calendar have default attendance_ids, force it to have none. + self.full_time_calendar = self.env["resource.calendar"].create( + {"name": "Full-time", "attendance_ids": False} + ) + for day in range(5): + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "09", + "hour_to": "17", + "calendar_id": self.full_time_calendar.id, + } + ) + + self.morning_calendar = self.env["resource.calendar"].create( + {"name": "Morning", "attendance_ids": False} + ) + for day in range(5): + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "08", + "hour_to": "12", + "calendar_id": self.morning_calendar.id, + } + ) + + self.afternoon_calendar = self.env["resource.calendar"].create( + {"name": "Afternoon", "attendance_ids": False} + ) + for day in range(5): + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "13", + "hour_to": "17", + "calendar_id": self.afternoon_calendar.id, + } + ) + + self.four_fifths_calendar = self.env["resource.calendar"].create( + {"name": "Four fifth", "attendance_ids": False} + ) + for day in (0, 2, 3, 4): + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "09", + "hour_to": "17", + "calendar_id": self.four_fifths_calendar.id, + } + ) + + def test_no_contract(self): + """ + Work time for an employee without a contract should be 0 + """ + self.assertEqual( + self._get_employee_work_time(), + [ + (date(2021, 10, 19), 0.0), + (date(2021, 10, 20), 0.0), + ], + ) + + def test_single_contract(self): + """ + Single contract + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.full_time_calendar.id, + "date_start": "2020-10-18", + } + ) + self.assertEqual( + self._get_employee_work_time(), + [ + (date(2021, 10, 19), 8.0), + (date(2021, 10, 20), 8.0), + ], + ) + + def test_single_contract_with_start_date(self): + """ + Single contract with a start date in range + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.full_time_calendar.id, + "date_start": "2021-10-20", + } + ) + self.assertEqual( + self._get_employee_work_time(), + [ + (date(2021, 10, 19), 0.0), + (date(2021, 10, 20), 8.0), + ], + ) + + def test_single_contract_with_end_date(self): + """ + Single contract with an end date + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.full_time_calendar.id, + "date_start": "2020-10-18", + "date_end": "2021-10-19", + } + ) + self.assertEqual( + self._get_employee_work_time(), + [ + (date(2021, 10, 19), 8.0), + (date(2021, 10, 20), 0.0), + ], + ) + + def test_multiple_contracts(self): + """ + Multiple simultaneous contracts + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.morning_calendar.id, + "date_start": "2020-10-18", + } + ) + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.afternoon_calendar.id, + "date_start": "2020-10-18", + } + ) + self.assertEqual( + self._get_employee_work_time(), + [ + (date(2021, 10, 19), 8.0), + (date(2021, 10, 20), 8.0), + ], + ) + + def test_multiple_contracts_with_dates(self): + """ + Multiple overlapping contracts with dates + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.morning_calendar.id, + "date_start": "2020-10-18", + "date_end": "2021-10-19", + } + ) + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.four_fifths_calendar.id, + "date_start": "2020-10-19", + } + ) + self.assertEqual( + self._get_employee_work_time(), + [ + (date(2021, 10, 19), 4.0), + (date(2021, 10, 20), 8.0), + ], + ) + + def _get_employee_work_time(self): + from_datetime = datetime(2021, 10, 19) + to_datetime = from_datetime + timedelta(days=2) + return self.employee1.list_work_time_per_day( + from_datetime, to_datetime + ) From fdb9dc72242509e6af3d4e561c5e28584ff856c8 Mon Sep 17 00:00:00 2001 From: hugues de keyzer Date: Wed, 10 Nov 2021 14:42:48 +0100 Subject: [PATCH 02/17] [IMP] handle leave days computation * override resource.mixin.get_work_days_data() to also take contracts into account. this is used for example by the hr_holidays module to compute leave days. * force employees' working hours to be equal to the company's working hours. * hide the working hours field on the employee form view. --- resource_work_time_from_contracts/README.rst | 22 ++- .../__manifest__.py | 5 +- .../models/__init__.py | 1 + .../models/resource_mixin.py | 38 +++- .../models/resource_resource.py | 17 ++ .../readme/DESCRIPTION.rst | 22 ++- .../static/description/index.html | 18 +- .../tests/__init__.py | 1 + .../tests/test_work_days_data.py | 177 ++++++++++++++++++ .../tests/test_work_time.py | 77 +------- .../tests/test_work_time_base.py | 97 ++++++++++ .../views/hr_employee.xml | 17 ++ 12 files changed, 409 insertions(+), 83 deletions(-) create mode 100644 resource_work_time_from_contracts/models/resource_resource.py create mode 100644 resource_work_time_from_contracts/tests/test_work_days_data.py create mode 100644 resource_work_time_from_contracts/tests/test_work_time_base.py create mode 100644 resource_work_time_from_contracts/views/hr_employee.xml diff --git a/resource_work_time_from_contracts/README.rst b/resource_work_time_from_contracts/README.rst index c495dd51c..76c4190cf 100644 --- a/resource_work_time_from_contracts/README.rst +++ b/resource_work_time_from_contracts/README.rst @@ -24,10 +24,28 @@ day. When this module is installed, the number of hours an employee is supposed to work is only computed from their contracts. Without contracts, the work time -per day is 0, instead of using the default company’s work schedule. +per day is 0, instead of using the default company’s working hours. The start and end dates of contracts are taken into account, but the status -(state) of the contract is ignored. +(state) of contracts are ignored. + +For this module to work properly, the company’s working hours should encompass +all possible work days (including weekend days if there are contracts with +weekend days), and each day should have working hours that correspond to the +working hours used in all contracts. This is because the company’s working +hours are used to compute leaves, and the number of hours per day is computed +from it. + +For example, if the company working hours define 8 hours per day, from 8 to 12 +and 13 to 17, all contracts’ working hours should be set from 8 to 12 and/or +from 13 to 17 for the corresponding days. Half days are thus supported. + +If there are contracts with working hours that don’t match the company’s +working hours, the number of days for leaves will be computed incorrectly. + +This module also makes the working hours (resource calendar) of an employee +always equal to the company’s working hours, and hides its field on the +employee form view. **Table of contents** diff --git a/resource_work_time_from_contracts/__manifest__.py b/resource_work_time_from_contracts/__manifest__.py index 1ba302dc5..43c5d1ad8 100644 --- a/resource_work_time_from_contracts/__manifest__.py +++ b/resource_work_time_from_contracts/__manifest__.py @@ -11,9 +11,12 @@ "license": "AGPL-3", "author": "Coop IT Easy SCRLfs", "website": "https://coopiteasy.be", + "category": "Human Resources", "depends": [ "hr_contract", ], - "data": [], + "data": [ + "views/hr_employee.xml", + ], "demo": [], } diff --git a/resource_work_time_from_contracts/models/__init__.py b/resource_work_time_from_contracts/models/__init__.py index 944de4292..c04b4b6d4 100644 --- a/resource_work_time_from_contracts/models/__init__.py +++ b/resource_work_time_from_contracts/models/__init__.py @@ -1 +1,2 @@ from . import resource_mixin +from . import resource_resource diff --git a/resource_work_time_from_contracts/models/resource_mixin.py b/resource_work_time_from_contracts/models/resource_mixin.py index e4972729b..538edc0c8 100644 --- a/resource_work_time_from_contracts/models/resource_mixin.py +++ b/resource_work_time_from_contracts/models/resource_mixin.py @@ -4,13 +4,16 @@ import datetime from collections import defaultdict -from odoo import models +from odoo import fields, models class ResourceMixin(models.AbstractModel): _inherit = "resource.mixin" + # make this field read-only. + resource_calendar_id = fields.Many2one("resource.calendar", readonly=True) + def list_work_time_per_day( self, from_datetime, to_datetime, calendar=None, domain=None ): @@ -34,6 +37,39 @@ def list_work_time_per_day( result.append((day, hours)) return result + def get_work_days_data( + self, + from_datetime, + to_datetime, + compute_leaves=True, + calendar=None, + domain=None, + ): + if calendar or not hasattr(self, "contract_ids"): + return super().get_work_days_data( + from_datetime, to_datetime, compute_leaves, calendar, domain + ) + normal_work_time_per_day = super().list_work_time_per_day( + from_datetime.replace(hour=0, minute=0, second=0), + to_datetime.replace(hour=0, minute=0, second=0) + + datetime.timedelta(days=1), + calendar=None, + domain=domain, + ) + work_time_per_day = self.list_work_time_per_day( + from_datetime, to_datetime, calendar=None, domain=domain + ) + num_days = 0.0 + num_hours = 0.0 + for (_, work_time), (_, normal_work_time) in zip( + work_time_per_day, normal_work_time_per_day + ): + if work_time == 0.0: + continue + num_days += work_time / normal_work_time + num_hours += work_time + return {"days": num_days, "hours": num_hours} + def _get_active_contracts(self, date_start, date_end): """ Get active contracts for the provided date range. diff --git a/resource_work_time_from_contracts/models/resource_resource.py b/resource_work_time_from_contracts/models/resource_resource.py new file mode 100644 index 000000000..ac347633c --- /dev/null +++ b/resource_work_time_from_contracts/models/resource_resource.py @@ -0,0 +1,17 @@ +# Copyright 2021 Coop IT Easy SCRLfs +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResourceResource(models.Model): + + _inherit = "resource.resource" + + # force this field to be equal to the resource_calendar_id of the company. + calendar_id = fields.Many2one( + "resource.calendar", + related="company_id.resource_calendar_id", + readonly=True, + store=True, + ) diff --git a/resource_work_time_from_contracts/readme/DESCRIPTION.rst b/resource_work_time_from_contracts/readme/DESCRIPTION.rst index 489300499..68ac2ab2e 100644 --- a/resource_work_time_from_contracts/readme/DESCRIPTION.rst +++ b/resource_work_time_from_contracts/readme/DESCRIPTION.rst @@ -3,7 +3,25 @@ day. When this module is installed, the number of hours an employee is supposed to work is only computed from their contracts. Without contracts, the work time -per day is 0, instead of using the default company’s work schedule. +per day is 0, instead of using the default company’s working hours. The start and end dates of contracts are taken into account, but the status -(state) of the contract is ignored. +(state) of contracts are ignored. + +For this module to work properly, the company’s working hours should encompass +all possible work days (including weekend days if there are contracts with +weekend days), and each day should have working hours that correspond to the +working hours used in all contracts. This is because the company’s working +hours are used to compute leaves, and the number of hours per day is computed +from it. + +For example, if the company working hours define 8 hours per day, from 8 to 12 +and 13 to 17, all contracts’ working hours should be set from 8 to 12 and/or +from 13 to 17 for the corresponding days. Half days are thus supported. + +If there are contracts with working hours that don’t match the company’s +working hours, the number of days for leaves will be computed incorrectly. + +This module also makes the working hours (resource calendar) of an employee +always equal to the company’s working hours, and hides its field on the +employee form view. diff --git a/resource_work_time_from_contracts/static/description/index.html b/resource_work_time_from_contracts/static/description/index.html index da389381f..4a17e46a3 100644 --- a/resource_work_time_from_contracts/static/description/index.html +++ b/resource_work_time_from_contracts/static/description/index.html @@ -372,9 +372,23 @@

Resource Work Time From Contracts

day.

When this module is installed, the number of hours an employee is supposed to work is only computed from their contracts. Without contracts, the work time -per day is 0, instead of using the default company’s work schedule.

+per day is 0, instead of using the default company’s working hours.

The start and end dates of contracts are taken into account, but the status -(state) of the contract is ignored.

+(state) of contracts are ignored.

+

For this module to work properly, the company’s working hours should encompass +all possible work days (including weekend days if there are contracts with +weekend days), and each day should have working hours that correspond to the +working hours used in all contracts. This is because the company’s working +hours are used to compute leaves, and the number of hours per day is computed +from it.

+

For example, if the company working hours define 8 hours per day, from 8 to 12 +and 13 to 17, all contracts’ working hours should be set from 8 to 12 and/or +from 13 to 17 for the corresponding days. Half days are thus supported.

+

If there are contracts with working hours that don’t match the company’s +working hours, the number of days for leaves will be computed incorrectly.

+

This module also makes the working hours (resource calendar) of an employee +always equal to the company’s working hours, and hides its field on the +employee form view.

Table of contents

    diff --git a/resource_work_time_from_contracts/tests/__init__.py b/resource_work_time_from_contracts/tests/__init__.py index b5d15aa3b..0d3bad2e6 100644 --- a/resource_work_time_from_contracts/tests/__init__.py +++ b/resource_work_time_from_contracts/tests/__init__.py @@ -1 +1,2 @@ +from . import test_work_days_data from . import test_work_time diff --git a/resource_work_time_from_contracts/tests/test_work_days_data.py b/resource_work_time_from_contracts/tests/test_work_days_data.py new file mode 100644 index 000000000..fbdec5ba3 --- /dev/null +++ b/resource_work_time_from_contracts/tests/test_work_days_data.py @@ -0,0 +1,177 @@ +# Copyright 2021 Coop IT Easy SCRLfs +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from .test_work_time_base import TestWorkTimeBase + + +class TestWorkDaysData(TestWorkTimeBase): + def setUp(self): + super().setUp() + self.company_calendar = self.env["resource.calendar"].create( + {"name": "Company", "attendance_ids": False} + ) + # the company calendar must contain full-time days + for day in range(7): + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "08", + "hour_to": "12", + "calendar_id": self.company_calendar.id, + } + ) + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "13", + "hour_to": "17", + "calendar_id": self.company_calendar.id, + } + ) + self.employee1.company_id.resource_calendar_id = self.company_calendar + + def test_no_contract(self): + """ + Work days for an employee without a contract should be 0 + """ + self.assertEqual( + self._get_employee_work_days(), + { + "days": 0.0, + "hours": 0.0, + }, + ) + + def test_single_contract(self): + """ + Single contract + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.full_time_calendar.id, + "date_start": "2020-10-24", + } + ) + self.assertEqual( + self._get_employee_work_days(), + { + "days": 5.0, + "hours": 40.0, + }, + ) + + def test_single_contract_with_start_date(self): + """ + Single contract with a start date in range + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.full_time_calendar.id, + "date_start": "2021-10-27", + } + ) + self.assertEqual( + self._get_employee_work_days(), + { + "days": 3.0, + "hours": 24.0, + }, + ) + + def test_single_contract_with_end_date(self): + """ + Single contract with an end date + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.full_time_calendar.id, + "date_start": "2020-10-24", + "date_end": "2021-10-26", + } + ) + self.assertEqual( + self._get_employee_work_days(), + { + "days": 2.0, + "hours": 16.0, + }, + ) + + def test_multiple_contracts(self): + """ + Multiple simultaneous contracts + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.morning_calendar.id, + "date_start": "2020-10-24", + } + ) + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.afternoon_calendar.id, + "date_start": "2020-10-24", + } + ) + self.assertEqual( + self._get_employee_work_days(), + { + "days": 5.0, + "hours": 40.0, + }, + ) + + def test_multiple_contracts_with_dates(self): + """ + Multiple overlapping contracts with dates + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.morning_calendar.id, + "date_start": "2020-10-24", + "date_end": "2021-10-25", + } + ) + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.four_fifths_calendar.id, + "date_start": "2020-10-24", + } + ) + self.assertEqual( + self._get_employee_work_days(), + { + "days": 4.5, + "hours": 36.0, + }, + ) + + def _get_employee_work_days(self): + from_datetime = datetime(2021, 10, 25) + to_datetime = from_datetime + timedelta(days=7) + return self.employee1.get_work_days_data(from_datetime, to_datetime) diff --git a/resource_work_time_from_contracts/tests/test_work_time.py b/resource_work_time_from_contracts/tests/test_work_time.py index 2d3953613..6475c6491 100644 --- a/resource_work_time_from_contracts/tests/test_work_time.py +++ b/resource_work_time_from_contracts/tests/test_work_time.py @@ -3,83 +3,10 @@ from datetime import date, datetime, timedelta -from odoo.tests.common import TransactionCase +from .test_work_time_base import TestWorkTimeBase -class TestWorkTime(TransactionCase): - def setUp(self): - super().setUp() - - # users - user1_dict = {"name": "User 1", "login": "user1", "password": "user1"} - self.user1 = self.env["res.users"].create(user1_dict) - - # employees - employee1_dict = { - "name": "Employee 1", - "user_id": self.user1.id, - "address_id": self.user1.partner_id.id, - } - self.employee1 = self.env["hr.employee"].create(employee1_dict) - - # working hours - # calendar have default attendance_ids, force it to have none. - self.full_time_calendar = self.env["resource.calendar"].create( - {"name": "Full-time", "attendance_ids": False} - ) - for day in range(5): - self.env["resource.calendar.attendance"].create( - { - "name": "Attendance", - "dayofweek": str(day), - "hour_from": "09", - "hour_to": "17", - "calendar_id": self.full_time_calendar.id, - } - ) - - self.morning_calendar = self.env["resource.calendar"].create( - {"name": "Morning", "attendance_ids": False} - ) - for day in range(5): - self.env["resource.calendar.attendance"].create( - { - "name": "Attendance", - "dayofweek": str(day), - "hour_from": "08", - "hour_to": "12", - "calendar_id": self.morning_calendar.id, - } - ) - - self.afternoon_calendar = self.env["resource.calendar"].create( - {"name": "Afternoon", "attendance_ids": False} - ) - for day in range(5): - self.env["resource.calendar.attendance"].create( - { - "name": "Attendance", - "dayofweek": str(day), - "hour_from": "13", - "hour_to": "17", - "calendar_id": self.afternoon_calendar.id, - } - ) - - self.four_fifths_calendar = self.env["resource.calendar"].create( - {"name": "Four fifth", "attendance_ids": False} - ) - for day in (0, 2, 3, 4): - self.env["resource.calendar.attendance"].create( - { - "name": "Attendance", - "dayofweek": str(day), - "hour_from": "09", - "hour_to": "17", - "calendar_id": self.four_fifths_calendar.id, - } - ) - +class TestWorkTime(TestWorkTimeBase): def test_no_contract(self): """ Work time for an employee without a contract should be 0 diff --git a/resource_work_time_from_contracts/tests/test_work_time_base.py b/resource_work_time_from_contracts/tests/test_work_time_base.py new file mode 100644 index 000000000..3c245e851 --- /dev/null +++ b/resource_work_time_from_contracts/tests/test_work_time_base.py @@ -0,0 +1,97 @@ +# Copyright 2021 Coop IT Easy SCRLfs +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class TestWorkTimeBase(TransactionCase): + def setUp(self): + super().setUp() + + # users + user1_dict = {"name": "User 1", "login": "user1", "password": "user1"} + self.user1 = self.env["res.users"].create(user1_dict) + + # employees + employee1_dict = { + "name": "Employee 1", + "user_id": self.user1.id, + "address_id": self.user1.partner_id.id, + } + self.employee1 = self.env["hr.employee"].create(employee1_dict) + + # working hours + # calendar have default attendance_ids, force it to have none. + self.full_time_calendar = self.env["resource.calendar"].create( + {"name": "Full-time", "attendance_ids": False} + ) + for day in range(5): + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "08", + "hour_to": "12", + "calendar_id": self.full_time_calendar.id, + } + ) + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "13", + "hour_to": "17", + "calendar_id": self.full_time_calendar.id, + } + ) + + self.morning_calendar = self.env["resource.calendar"].create( + {"name": "Morning", "attendance_ids": False} + ) + for day in range(5): + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "08", + "hour_to": "12", + "calendar_id": self.morning_calendar.id, + } + ) + + self.afternoon_calendar = self.env["resource.calendar"].create( + {"name": "Afternoon", "attendance_ids": False} + ) + for day in range(5): + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "13", + "hour_to": "17", + "calendar_id": self.afternoon_calendar.id, + } + ) + + self.four_fifths_calendar = self.env["resource.calendar"].create( + {"name": "Four fifth", "attendance_ids": False} + ) + for day in (0, 2, 3, 4): + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "08", + "hour_to": "12", + "calendar_id": self.four_fifths_calendar.id, + } + ) + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "13", + "hour_to": "17", + "calendar_id": self.four_fifths_calendar.id, + } + ) diff --git a/resource_work_time_from_contracts/views/hr_employee.xml b/resource_work_time_from_contracts/views/hr_employee.xml new file mode 100644 index 000000000..6b84f26b6 --- /dev/null +++ b/resource_work_time_from_contracts/views/hr_employee.xml @@ -0,0 +1,17 @@ + + + + + hr.employee.form (in resource_work_time_from_contracts) + hr.employee + + + + True + + + + From 6b76ebf0a8dec5c8d7dc58985c74858a8832e2b0 Mon Sep 17 00:00:00 2001 From: hugues de keyzer Date: Thu, 11 Nov 2021 11:33:00 +0100 Subject: [PATCH 03/17] [IMP] hide working time from resource list view hide working time column from resource list view. --- .../__manifest__.py | 1 + .../views/resource_resource.xml | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 resource_work_time_from_contracts/views/resource_resource.xml diff --git a/resource_work_time_from_contracts/__manifest__.py b/resource_work_time_from_contracts/__manifest__.py index 43c5d1ad8..b35a7bcf1 100644 --- a/resource_work_time_from_contracts/__manifest__.py +++ b/resource_work_time_from_contracts/__manifest__.py @@ -17,6 +17,7 @@ ], "data": [ "views/hr_employee.xml", + "views/resource_resource.xml", ], "demo": [], } diff --git a/resource_work_time_from_contracts/views/resource_resource.xml b/resource_work_time_from_contracts/views/resource_resource.xml new file mode 100644 index 000000000..80aa7ed34 --- /dev/null +++ b/resource_work_time_from_contracts/views/resource_resource.xml @@ -0,0 +1,17 @@ + + + + + resource.resource.tree (in resource_work_time_from_contracts) + resource.resource + + + + True + + + + From adb136d627f6f87f4e7b74175e865bcfa128092f Mon Sep 17 00:00:00 2001 From: hugues de keyzer Date: Thu, 11 Nov 2021 17:48:39 +0100 Subject: [PATCH 04/17] [IMP] add (failing) tests with leaves --- .../tests/test_work_days_data.py | 90 ++++++++++++++++++- .../tests/test_work_time.py | 74 ++++++++++++++- .../tests/test_work_time_base.py | 14 +++ 3 files changed, 170 insertions(+), 8 deletions(-) diff --git a/resource_work_time_from_contracts/tests/test_work_days_data.py b/resource_work_time_from_contracts/tests/test_work_days_data.py index fbdec5ba3..26a2d72e2 100644 --- a/resource_work_time_from_contracts/tests/test_work_days_data.py +++ b/resource_work_time_from_contracts/tests/test_work_days_data.py @@ -1,7 +1,7 @@ # Copyright 2021 Coop IT Easy SCRLfs # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from datetime import datetime, timedelta +from datetime import timedelta from .test_work_time_base import TestWorkTimeBase @@ -125,7 +125,7 @@ def test_multiple_contracts(self): ) self.env["hr.contract"].create( { - "name": "Contract 1", + "name": "Contract 2", "employee_id": self.employee1.id, "wage": 0.0, "resource_calendar_id": self.afternoon_calendar.id, @@ -156,7 +156,7 @@ def test_multiple_contracts_with_dates(self): ) self.env["hr.contract"].create( { - "name": "Contract 1", + "name": "Contract 2", "employee_id": self.employee1.id, "wage": 0.0, "resource_calendar_id": self.four_fifths_calendar.id, @@ -171,7 +171,89 @@ def test_multiple_contracts_with_dates(self): }, ) + def test_with_leaves(self): + """ + Existing leaves should be subtracted from the work time + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.full_time_calendar.id, + "date_start": "2020-10-24", + } + ) + + self.env["resource.calendar.leaves"].create( + { + "name": "Tuesday morning", + "calendar_id": self.employee1.resource_calendar_id.id, + "date_from": self.to_utc_datetime(2021, 10, 26, 8), + "date_to": self.to_utc_datetime(2021, 10, 26, 12), + "resource_id": self.employee1.resource_id.id, + "time_type": "leave", + } + ) + self.env["resource.calendar.leaves"].create( + { + "name": "Wednesday afternoon", + "calendar_id": self.employee1.resource_calendar_id.id, + "date_from": self.to_utc_datetime(2021, 10, 27, 13), + "date_to": self.to_utc_datetime(2021, 10, 27, 17), + "resource_id": self.employee1.resource_id.id, + "time_type": "leave", + } + ) + self.env["resource.calendar.leaves"].create( + { + "name": "Friday", + "calendar_id": self.employee1.resource_calendar_id.id, + "date_from": self.to_utc_datetime(2021, 10, 29, 8), + "date_to": self.to_utc_datetime(2021, 10, 29, 17), + "resource_id": self.employee1.resource_id.id, + "time_type": "leave", + } + ) + self.assertEqual( + self._get_employee_work_days(), + { + "days": 3.0, + "hours": 24.0, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.to_utc_datetime(2021, 10, 26, 8), + self.to_utc_datetime(2021, 10, 26, 12), + ), + { + "days": 0.0, + "hours": 0.0, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.to_utc_datetime(2021, 10, 26, 13), + self.to_utc_datetime(2021, 10, 26, 17), + ), + { + "days": 0.5, + "hours": 4.0, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.to_utc_datetime(2021, 10, 27, 8), + self.to_utc_datetime(2021, 10, 27, 17), + ), + { + "days": 0.5, + "hours": 4.0, + }, + ) + def _get_employee_work_days(self): - from_datetime = datetime(2021, 10, 25) + from_datetime = self.to_utc_datetime(2021, 10, 25) to_datetime = from_datetime + timedelta(days=7) return self.employee1.get_work_days_data(from_datetime, to_datetime) diff --git a/resource_work_time_from_contracts/tests/test_work_time.py b/resource_work_time_from_contracts/tests/test_work_time.py index 6475c6491..8def68072 100644 --- a/resource_work_time_from_contracts/tests/test_work_time.py +++ b/resource_work_time_from_contracts/tests/test_work_time.py @@ -1,7 +1,7 @@ # Copyright 2021 Coop IT Easy SCRLfs # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from datetime import date, datetime, timedelta +from datetime import date, timedelta from .test_work_time_base import TestWorkTimeBase @@ -98,7 +98,7 @@ def test_multiple_contracts(self): ) self.env["hr.contract"].create( { - "name": "Contract 1", + "name": "Contract 2", "employee_id": self.employee1.id, "wage": 0.0, "resource_calendar_id": self.afternoon_calendar.id, @@ -129,7 +129,7 @@ def test_multiple_contracts_with_dates(self): ) self.env["hr.contract"].create( { - "name": "Contract 1", + "name": "Contract 2", "employee_id": self.employee1.id, "wage": 0.0, "resource_calendar_id": self.four_fifths_calendar.id, @@ -144,8 +144,74 @@ def test_multiple_contracts_with_dates(self): ], ) + def test_with_leaves(self): + """ + Existing leaves should be subtracted from the work time + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.full_time_calendar.id, + "date_start": "2020-10-24", + } + ) + + self.env["resource.calendar.leaves"].create( + { + "name": "Tuesday morning", + "calendar_id": self.employee1.resource_calendar_id.id, + "date_from": self.to_utc_datetime(2021, 10, 19, 8), + "date_to": self.to_utc_datetime(2021, 10, 19, 12), + "resource_id": self.employee1.resource_id.id, + "time_type": "leave", + } + ) + self.env["resource.calendar.leaves"].create( + { + "name": "Wednesday", + "calendar_id": self.employee1.resource_calendar_id.id, + "date_from": self.to_utc_datetime(2021, 10, 20, 8), + "date_to": self.to_utc_datetime(2021, 10, 20, 17), + "resource_id": self.employee1.resource_id.id, + "time_type": "leave", + } + ) + self.assertEqual( + self._get_employee_work_time(), + [ + (date(2021, 10, 19), 4.0), + ], + ) + self.assertEqual( + self.employee1.list_work_time_per_day( + self.to_utc_datetime(2021, 10, 19, 8), + self.to_utc_datetime(2021, 10, 19, 12), + ), + [], + ) + self.assertEqual( + self.employee1.list_work_time_per_day( + self.to_utc_datetime(2021, 10, 19, 13), + self.to_utc_datetime(2021, 10, 19, 17), + ), + [ + (date(2021, 10, 19), 4.0), + ], + ) + self.assertEqual( + self.employee1.list_work_time_per_day( + self.to_utc_datetime(2021, 10, 19, 8), + self.to_utc_datetime(2021, 10, 19, 17), + ), + [ + (date(2021, 10, 19), 4.0), + ], + ) + def _get_employee_work_time(self): - from_datetime = datetime(2021, 10, 19) + from_datetime = self.to_utc_datetime(2021, 10, 19) to_datetime = from_datetime + timedelta(days=2) return self.employee1.list_work_time_per_day( from_datetime, to_datetime diff --git a/resource_work_time_from_contracts/tests/test_work_time_base.py b/resource_work_time_from_contracts/tests/test_work_time_base.py index 3c245e851..fe271f140 100644 --- a/resource_work_time_from_contracts/tests/test_work_time_base.py +++ b/resource_work_time_from_contracts/tests/test_work_time_base.py @@ -1,6 +1,10 @@ # Copyright 2021 Coop IT Easy SCRLfs # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import datetime + +import pytz + from odoo.tests.common import TransactionCase @@ -8,6 +12,8 @@ class TestWorkTimeBase(TransactionCase): def setUp(self): super().setUp() + self.timezone = pytz.timezone(self.env.user.tz) + # users user1_dict = {"name": "User 1", "login": "user1", "password": "user1"} self.user1 = self.env["res.users"].create(user1_dict) @@ -95,3 +101,11 @@ def setUp(self): "calendar_id": self.four_fifths_calendar.id, } ) + + def to_utc_datetime(self, year, month, day, *args, **kwargs): + """ + Create a UTC datetime from local time values + """ + return self.timezone.localize( + datetime.datetime(year, month, day, *args, **kwargs) + ).astimezone(pytz.utc) From e7f3a14c483eb6b09aff8c6d09563998994f8207 Mon Sep 17 00:00:00 2001 From: hugues de keyzer Date: Wed, 17 Nov 2021 15:47:25 +0100 Subject: [PATCH 05/17] [FIX] fix computation with existing leaves * fix work time computation with existing leaves. * add .list_normal_work_time_per_day() method to compute work time ignoring leaves. * fix timezone issues. --- .../models/resource_mixin.py | 202 +++++++++--- .../tests/test_work_days_data.py | 295 +++++++++++++++--- .../tests/test_work_time.py | 197 +++++++++--- .../tests/test_work_time_base.py | 58 +++- 4 files changed, 614 insertions(+), 138 deletions(-) diff --git a/resource_work_time_from_contracts/models/resource_mixin.py b/resource_work_time_from_contracts/models/resource_mixin.py index 538edc0c8..dee4c92a6 100644 --- a/resource_work_time_from_contracts/models/resource_mixin.py +++ b/resource_work_time_from_contracts/models/resource_mixin.py @@ -4,7 +4,12 @@ import datetime from collections import defaultdict +from pytz import timezone, utc + from odoo import fields, models +from odoo.tools import float_utils + +from odoo.addons.resource.models.resource_mixin import ROUNDING_FACTOR class ResourceMixin(models.AbstractModel): @@ -15,26 +20,82 @@ class ResourceMixin(models.AbstractModel): resource_calendar_id = fields.Many2one("resource.calendar", readonly=True) def list_work_time_per_day( - self, from_datetime, to_datetime, calendar=None, domain=None + self, + from_datetime, + to_datetime, + calendar=None, + domain=None, ): - default_work_time = super().list_work_time_per_day( - from_datetime, to_datetime, calendar, domain - ) if calendar or not hasattr(self, "contract_ids"): - return default_work_time - contracts = self._get_active_contracts( - from_datetime.date(), to_datetime.date() + return super().list_work_time_per_day( + from_datetime, to_datetime, calendar, domain + ) + work_time_from_contracts = self._get_work_time_from_contracts( + from_datetime, to_datetime, domain ) - work_time_results = self._get_work_time_per_contract( - contracts, from_datetime, to_datetime, domain + # we need to take leaves into account. instead of going into the + # internals of leaves themselves which are quite complex (and mostly + # private to the resource module), we ask for the default work time + # with and without leaves, and compute the difference, that we + # subtract from the work time from the contracts. + # + # to get the default work time without leaves, we provide a domain + # that will yield no results. the domain argument is used to query + # leave intervals from resource.calendar.leaves (in + # resource.resource.ResourceCalendar._leave_intervals()). the default + # is [('time_type', '=', 'leave')]. to ensure that no leaves are + # found, we want to use a domain that will never return anything. we + # use [("calendar_id", "=", False)] because it will be added to + # another domain asking for a specific calendar_id, resulting in a + # query returning no results. + # + # for this, we query the calendar of the employee, which must be the + # same as the one of the company, and which must contain full-day + # hours for each day. this is the calendar to which leaves are linked, + # and is used by default by + # resource.resource_mixin.list_work_time_per_day() when calendar=None. + default_work_time = dict( + super().list_work_time_per_day( + from_datetime, + to_datetime, + self.resource_calendar_id, + domain=[("calendar_id", "=", False)], + ) + ) + default_work_time_with_leaves = super().list_work_time_per_day( + from_datetime, to_datetime, self.resource_calendar_id, domain ) result = [] - for work_time_per_day in default_work_time: - day = work_time_per_day[0] - hours = 0.0 - for work_time in work_time_results: - hours += work_time[day] - result.append((day, hours)) + for day, hours in default_work_time_with_leaves: + total_hours = work_time_from_contracts[day] - ( + default_work_time[day] - hours + ) + if total_hours <= 0.0: + continue + result.append((day, total_hours)) + return result + + def list_normal_work_time_per_day( + self, from_datetime, to_datetime, domain=None + ): + """ + Same as list_work_time_per_day(), but ignoring leaves + """ + work_time_from_contracts = self._get_work_time_from_contracts( + from_datetime, to_datetime, domain + ) + from_datetime, to_datetime = self._localize_datetimes( + from_datetime, to_datetime + ) + day = from_datetime.date() + to_day = to_datetime.date() + delta = datetime.timedelta(days=1) + result = [] + while day <= to_day: + hours = work_time_from_contracts[day] + if hours != 0.0: + result.append((day, hours)) + day += delta return result def get_work_days_data( @@ -49,24 +110,53 @@ def get_work_days_data( return super().get_work_days_data( from_datetime, to_datetime, compute_leaves, calendar, domain ) - normal_work_time_per_day = super().list_work_time_per_day( - from_datetime.replace(hour=0, minute=0, second=0), - to_datetime.replace(hour=0, minute=0, second=0) - + datetime.timedelta(days=1), - calendar=None, - domain=domain, + # we need the normal work time per day for each day to be able to + # compute the fraction of day that the number of hours represents. + # this is defined in the calendar of the employee, which must be the + # same as the one of the company, and which must contain full-day + # hours for each day. + # + # we need full days, so we replace the hours to start and stop at + # midnight in the timezone of the resource. + # + # the provided domain is to exclude leaves from the computation, as + # explained in list_work_time_per_day(). + from_datetime, to_datetime = self._localize_datetimes( + from_datetime, to_datetime ) - work_time_per_day = self.list_work_time_per_day( - from_datetime, to_datetime, calendar=None, domain=domain + normal_work_time_per_day = dict( + super().list_work_time_per_day( + from_datetime.replace( + hour=0, minute=0, second=0, microsecond=0 + ), + to_datetime.replace(hour=0, minute=0, second=0, microsecond=0) + + datetime.timedelta(days=1), + self.resource_calendar_id, + [("calendar_id", "=", False)], + ) ) + if compute_leaves: + work_time_per_day = self.list_work_time_per_day( + from_datetime, to_datetime, calendar=None, domain=domain + ) + else: + work_time_per_day = self.list_normal_work_time_per_day( + from_datetime, to_datetime, domain=domain + ) num_days = 0.0 num_hours = 0.0 - for (_, work_time), (_, normal_work_time) in zip( - work_time_per_day, normal_work_time_per_day - ): + for day, work_time in work_time_per_day: if work_time == 0.0: continue - num_days += work_time / normal_work_time + normal_work_time = normal_work_time_per_day[day] + # we use the same rounding computation as in + # resource.resource_mixin.get_work_days_data(). + num_days += ( + float_utils.round( + ROUNDING_FACTOR * work_time / normal_work_time + ) + / ROUNDING_FACTOR + ) num_hours += work_time return {"days": num_days, "hours": num_hours} @@ -101,24 +191,58 @@ def _get_work_time_per_contract( date_start = contract.date_start if from_dt.date() < date_start: from_dt = datetime.datetime( - date_start.year, date_start.month, date_start.day + date_start.year, + date_start.month, + date_start.day, + tzinfo=from_dt.tzinfo, ) date_end = contract.date_end if date_end and to_dt.date() > date_end: # limit to midnight on the day after date_end to completely # include date_end. - to_dt = datetime.datetime( - date_end.year, date_end.month, date_end.day - ) + datetime.timedelta(days=1) + to_dt = ( + datetime.datetime( + date_end.year, + date_end.month, + date_end.day, + tzinfo=from_dt.tzinfo, + ) + + datetime.timedelta(days=1) + ) work_time_results.append( - defaultdict( - float, - super().list_work_time_per_day( - from_dt, - to_dt, - contract.resource_calendar_id, - domain, - ), + super().list_work_time_per_day( + from_dt, + to_dt, + contract.resource_calendar_id, + domain, ) ) return work_time_results + + def _get_work_time_from_contracts( + self, from_datetime, to_datetime, domain=None + ): + from_datetime, to_datetime = self._localize_datetimes( + from_datetime, to_datetime + ) + contracts = self._get_active_contracts( + from_datetime.date(), to_datetime.date() + ) + work_time_results = self._get_work_time_per_contract( + contracts, from_datetime, to_datetime, domain + ) + result = defaultdict(float) + for work_time in work_time_results: + for day, hours in work_time: + result[day] += hours + return result + + def _localize_datetimes(self, from_datetime, to_datetime): + # naive datetimes are considered utc + if not from_datetime.tzinfo: + from_datetime = from_datetime.replace(tzinfo=utc) + if not to_datetime.tzinfo: + to_datetime = to_datetime.replace(tzinfo=utc) + + tz = timezone(self.tz) + return from_datetime.astimezone(tz), to_datetime.astimezone(tz) diff --git a/resource_work_time_from_contracts/tests/test_work_days_data.py b/resource_work_time_from_contracts/tests/test_work_days_data.py index 26a2d72e2..185f133a5 100644 --- a/resource_work_time_from_contracts/tests/test_work_days_data.py +++ b/resource_work_time_from_contracts/tests/test_work_days_data.py @@ -1,39 +1,12 @@ # Copyright 2021 Coop IT Easy SCRLfs # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from datetime import timedelta +from datetime import timedelta, timezone from .test_work_time_base import TestWorkTimeBase class TestWorkDaysData(TestWorkTimeBase): - def setUp(self): - super().setUp() - self.company_calendar = self.env["resource.calendar"].create( - {"name": "Company", "attendance_ids": False} - ) - # the company calendar must contain full-time days - for day in range(7): - self.env["resource.calendar.attendance"].create( - { - "name": "Attendance", - "dayofweek": str(day), - "hour_from": "08", - "hour_to": "12", - "calendar_id": self.company_calendar.id, - } - ) - self.env["resource.calendar.attendance"].create( - { - "name": "Attendance", - "dayofweek": str(day), - "hour_from": "13", - "hour_to": "17", - "calendar_id": self.company_calendar.id, - } - ) - self.employee1.company_id.resource_calendar_id = self.company_calendar - def test_no_contract(self): """ Work days for an employee without a contract should be 0 @@ -63,7 +36,7 @@ def test_single_contract(self): self._get_employee_work_days(), { "days": 5.0, - "hours": 40.0, + "hours": 38.0, }, ) @@ -84,7 +57,9 @@ def test_single_contract_with_start_date(self): self._get_employee_work_days(), { "days": 3.0, - "hours": 24.0, + # this is 22.8, but writing 22.8 will cause a failure because + # of floating-point precision. + "hours": 7.6 * 3, }, ) @@ -106,7 +81,7 @@ def test_single_contract_with_end_date(self): self._get_employee_work_days(), { "days": 2.0, - "hours": 16.0, + "hours": 15.2, }, ) @@ -136,7 +111,7 @@ def test_multiple_contracts(self): self._get_employee_work_days(), { "days": 5.0, - "hours": 40.0, + "hours": 38.0, }, ) @@ -167,13 +142,13 @@ def test_multiple_contracts_with_dates(self): self._get_employee_work_days(), { "days": 4.5, - "hours": 36.0, + "hours": 34.2, }, ) def test_with_leaves(self): """ - Existing leaves should be subtracted from the work time + Existing leaves should by default be subtracted from the work time """ self.env["hr.contract"].create( { @@ -189,8 +164,8 @@ def test_with_leaves(self): { "name": "Tuesday morning", "calendar_id": self.employee1.resource_calendar_id.id, - "date_from": self.to_utc_datetime(2021, 10, 26, 8), - "date_to": self.to_utc_datetime(2021, 10, 26, 12), + "date_from": self.to_utc_datetime(2021, 10, 26, 8, 42), + "date_to": self.to_utc_datetime(2021, 10, 26, 12, 30), "resource_id": self.employee1.resource_id.id, "time_type": "leave", } @@ -199,8 +174,8 @@ def test_with_leaves(self): { "name": "Wednesday afternoon", "calendar_id": self.employee1.resource_calendar_id.id, - "date_from": self.to_utc_datetime(2021, 10, 27, 13), - "date_to": self.to_utc_datetime(2021, 10, 27, 17), + "date_from": self.to_utc_datetime(2021, 10, 27, 13, 30), + "date_to": self.to_utc_datetime(2021, 10, 27, 17, 18), "resource_id": self.employee1.resource_id.id, "time_type": "leave", } @@ -209,8 +184,8 @@ def test_with_leaves(self): { "name": "Friday", "calendar_id": self.employee1.resource_calendar_id.id, - "date_from": self.to_utc_datetime(2021, 10, 29, 8), - "date_to": self.to_utc_datetime(2021, 10, 29, 17), + "date_from": self.to_utc_datetime(2021, 10, 29, 8, 42), + "date_to": self.to_utc_datetime(2021, 10, 29, 17, 18), "resource_id": self.employee1.resource_id.id, "time_type": "leave", } @@ -219,13 +194,26 @@ def test_with_leaves(self): self._get_employee_work_days(), { "days": 3.0, - "hours": 24.0, + # this is 22.8, but writing 22.8 will cause a failure because + # of floating-point precision. + "hours": 7.6 * 3, }, ) self.assertEqual( self.employee1.get_work_days_data( - self.to_utc_datetime(2021, 10, 26, 8), - self.to_utc_datetime(2021, 10, 26, 12), + self.local_datetime(2021, 10, 25), + self.local_datetime(2021, 11, 1), + compute_leaves=False, + ), + { + "days": 5.0, + "hours": 38.0, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.local_datetime(2021, 10, 26, 8, 42), + self.local_datetime(2021, 10, 26, 12, 30), ), { "days": 0.0, @@ -234,26 +222,233 @@ def test_with_leaves(self): ) self.assertEqual( self.employee1.get_work_days_data( - self.to_utc_datetime(2021, 10, 26, 13), - self.to_utc_datetime(2021, 10, 26, 17), + self.local_datetime(2021, 10, 26, 8, 42), + self.local_datetime(2021, 10, 26, 12, 30), + compute_leaves=False, + ), + { + "days": 0.5, + "hours": 3.8, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.local_datetime(2021, 10, 26, 13, 30), + self.local_datetime(2021, 10, 26, 17, 18), + ), + { + "days": 0.5, + "hours": 3.8, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.local_datetime(2021, 10, 26, 13, 30), + self.local_datetime(2021, 10, 26, 17, 18), + compute_leaves=False, + ), + { + "days": 0.5, + "hours": 3.8, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.local_datetime(2021, 10, 27, 8, 42), + self.local_datetime(2021, 10, 27, 17, 18), + ), + { + "days": 0.5, + "hours": 3.8, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.local_datetime(2021, 10, 27, 8, 42), + self.local_datetime(2021, 10, 27, 17, 18), + compute_leaves=False, + ), + { + "days": 1.0, + "hours": 7.6, + }, + ) + + def test_precision(self): + """ + Days should be rounded to the 1/16th. + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.full_time_calendar.id, + "date_start": "2020-10-24", + } + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.local_datetime(2021, 10, 26, 8, 42), + self.local_datetime(2021, 10, 26, 8, 48), + ), + { + "days": 0.0, + "hours": 0.1, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.local_datetime(2021, 10, 26, 8, 42), + self.local_datetime(2021, 10, 26, 9, 6), + ), + { + "days": 0.0625, + "hours": 0.4, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.local_datetime(2021, 10, 26, 8, 42), + self.local_datetime(2021, 10, 26, 9, 18), + ), + { + "days": 0.0625, + "hours": 0.6, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.local_datetime(2021, 10, 26, 8, 42), + self.local_datetime(2021, 10, 26, 9, 36), + ), + { + "days": 0.125, + "hours": 0.9, + }, + ) + + def test_timezone(self): + """ + It should take the timezone into account. + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.full_time_calendar.id, + "date_start": "2020-10-24", + } + ) + self.env["resource.calendar.leaves"].create( + { + "name": "Leave", + "calendar_id": self.employee1.resource_calendar_id.id, + "date_from": self.to_utc_datetime(2021, 10, 26, 8, 42), + "date_to": self.to_utc_datetime(2021, 10, 26, 9, 30), + "resource_id": self.employee1.resource_id.id, + "time_type": "leave", + } + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.local_datetime(2021, 10, 26, 8, 42), + self.local_datetime(2021, 10, 26, 12, 30), + ), + { + "days": 0.375, + "hours": 3.0, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.local_datetime(2021, 10, 26, 8, 42), + self.local_datetime(2021, 10, 26, 12, 30), + compute_leaves=False, + ), + { + "days": 0.5, + "hours": 3.8, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.to_utc_datetime(2021, 10, 26, 8, 42), + self.to_utc_datetime(2021, 10, 26, 12, 30), + ), + { + "days": 0.375, + "hours": 3.0, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.to_utc_datetime(2021, 10, 26, 8, 42), + self.to_utc_datetime(2021, 10, 26, 12, 30), + compute_leaves=False, + ), + { + "days": 0.5, + "hours": 3.8, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.to_utc_datetime(2021, 10, 26, 8, 42).replace(tzinfo=None), + self.to_utc_datetime(2021, 10, 26, 12, 30).replace( + tzinfo=None + ), + ), + { + "days": 0.375, + "hours": 3.0, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.to_utc_datetime(2021, 10, 26, 8, 42).replace(tzinfo=None), + self.to_utc_datetime(2021, 10, 26, 12, 30).replace( + tzinfo=None + ), + compute_leaves=False, ), { "days": 0.5, - "hours": 4.0, + "hours": 3.8, + }, + ) + self.assertEqual( + self.employee1.get_work_days_data( + self.to_utc_datetime(2021, 10, 26, 8, 42).astimezone( + timezone(timedelta(hours=23)) + ), + self.to_utc_datetime(2021, 10, 26, 12, 30).astimezone( + timezone(timedelta(hours=-23)) + ), + ), + { + "days": 0.375, + "hours": 3.0, }, ) self.assertEqual( self.employee1.get_work_days_data( - self.to_utc_datetime(2021, 10, 27, 8), - self.to_utc_datetime(2021, 10, 27, 17), + self.to_utc_datetime(2021, 10, 26, 8, 42).astimezone( + timezone(timedelta(hours=23)) + ), + self.to_utc_datetime(2021, 10, 26, 12, 30).astimezone( + timezone(timedelta(hours=-23)) + ), + compute_leaves=False, ), { "days": 0.5, - "hours": 4.0, + "hours": 3.8, }, ) def _get_employee_work_days(self): - from_datetime = self.to_utc_datetime(2021, 10, 25) + from_datetime = self.local_datetime(2021, 10, 25) to_datetime = from_datetime + timedelta(days=7) return self.employee1.get_work_days_data(from_datetime, to_datetime) diff --git a/resource_work_time_from_contracts/tests/test_work_time.py b/resource_work_time_from_contracts/tests/test_work_time.py index 8def68072..ba53a051d 100644 --- a/resource_work_time_from_contracts/tests/test_work_time.py +++ b/resource_work_time_from_contracts/tests/test_work_time.py @@ -1,7 +1,7 @@ # Copyright 2021 Coop IT Easy SCRLfs # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from datetime import date, timedelta +from datetime import date, timedelta, timezone from .test_work_time_base import TestWorkTimeBase @@ -11,13 +11,7 @@ def test_no_contract(self): """ Work time for an employee without a contract should be 0 """ - self.assertEqual( - self._get_employee_work_time(), - [ - (date(2021, 10, 19), 0.0), - (date(2021, 10, 20), 0.0), - ], - ) + self.assertEqual(self._get_employee_work_time(), []) def test_single_contract(self): """ @@ -35,8 +29,8 @@ def test_single_contract(self): self.assertEqual( self._get_employee_work_time(), [ - (date(2021, 10, 19), 8.0), - (date(2021, 10, 20), 8.0), + (date(2021, 10, 19), 7.6), + (date(2021, 10, 20), 7.6), ], ) @@ -55,10 +49,7 @@ def test_single_contract_with_start_date(self): ) self.assertEqual( self._get_employee_work_time(), - [ - (date(2021, 10, 19), 0.0), - (date(2021, 10, 20), 8.0), - ], + [(date(2021, 10, 20), 7.6)], ) def test_single_contract_with_end_date(self): @@ -77,10 +68,7 @@ def test_single_contract_with_end_date(self): ) self.assertEqual( self._get_employee_work_time(), - [ - (date(2021, 10, 19), 8.0), - (date(2021, 10, 20), 0.0), - ], + [(date(2021, 10, 19), 7.6)], ) def test_multiple_contracts(self): @@ -108,8 +96,8 @@ def test_multiple_contracts(self): self.assertEqual( self._get_employee_work_time(), [ - (date(2021, 10, 19), 8.0), - (date(2021, 10, 20), 8.0), + (date(2021, 10, 19), 7.6), + (date(2021, 10, 20), 7.6), ], ) @@ -139,14 +127,14 @@ def test_multiple_contracts_with_dates(self): self.assertEqual( self._get_employee_work_time(), [ - (date(2021, 10, 19), 4.0), - (date(2021, 10, 20), 8.0), + (date(2021, 10, 19), 3.8), + (date(2021, 10, 20), 7.6), ], ) def test_with_leaves(self): """ - Existing leaves should be subtracted from the work time + Existing leaves should by default be subtracted from the work time """ self.env["hr.contract"].create( { @@ -162,8 +150,8 @@ def test_with_leaves(self): { "name": "Tuesday morning", "calendar_id": self.employee1.resource_calendar_id.id, - "date_from": self.to_utc_datetime(2021, 10, 19, 8), - "date_to": self.to_utc_datetime(2021, 10, 19, 12), + "date_from": self.to_utc_datetime(2021, 10, 19, 8, 42), + "date_to": self.to_utc_datetime(2021, 10, 19, 12, 30), "resource_id": self.employee1.resource_id.id, "time_type": "leave", } @@ -172,8 +160,8 @@ def test_with_leaves(self): { "name": "Wednesday", "calendar_id": self.employee1.resource_calendar_id.id, - "date_from": self.to_utc_datetime(2021, 10, 20, 8), - "date_to": self.to_utc_datetime(2021, 10, 20, 17), + "date_from": self.to_utc_datetime(2021, 10, 20, 8, 42), + "date_to": self.to_utc_datetime(2021, 10, 20, 17, 18), "resource_id": self.employee1.resource_id.id, "time_type": "leave", } @@ -181,37 +169,172 @@ def test_with_leaves(self): self.assertEqual( self._get_employee_work_time(), [ - (date(2021, 10, 19), 4.0), + (date(2021, 10, 19), 3.8), ], ) self.assertEqual( self.employee1.list_work_time_per_day( - self.to_utc_datetime(2021, 10, 19, 8), - self.to_utc_datetime(2021, 10, 19, 12), + self.local_datetime(2021, 10, 19, 8, 42), + self.local_datetime(2021, 10, 19, 12, 30), ), [], ) + self.assertEqual( + self.employee1.list_normal_work_time_per_day( + self.local_datetime(2021, 10, 19, 8, 42), + self.local_datetime(2021, 10, 19, 12, 30), + ), + [ + (date(2021, 10, 19), 3.8), + ], + ) self.assertEqual( self.employee1.list_work_time_per_day( - self.to_utc_datetime(2021, 10, 19, 13), - self.to_utc_datetime(2021, 10, 19, 17), + self.local_datetime(2021, 10, 19, 13, 30), + self.local_datetime(2021, 10, 19, 17, 18), ), [ - (date(2021, 10, 19), 4.0), + (date(2021, 10, 19), 3.8), + ], + ) + self.assertEqual( + self.employee1.list_normal_work_time_per_day( + self.local_datetime(2021, 10, 19, 13, 30), + self.local_datetime(2021, 10, 19, 17, 18), + ), + [ + (date(2021, 10, 19), 3.8), ], ) self.assertEqual( self.employee1.list_work_time_per_day( - self.to_utc_datetime(2021, 10, 19, 8), - self.to_utc_datetime(2021, 10, 19, 17), + self.local_datetime(2021, 10, 19, 8, 42), + self.local_datetime(2021, 10, 19, 17, 18), + ), + [ + (date(2021, 10, 19), 3.8), + ], + ) + self.assertEqual( + self.employee1.list_normal_work_time_per_day( + self.local_datetime(2021, 10, 19, 8, 42), + self.local_datetime(2021, 10, 19, 17, 18), + ), + [ + (date(2021, 10, 19), 7.6), + ], + ) + + def test_timezone(self): + """ + It should take the timezone into account. + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.full_time_calendar.id, + "date_start": "2020-10-24", + } + ) + self.env["resource.calendar.leaves"].create( + { + "name": "Leave", + "calendar_id": self.employee1.resource_calendar_id.id, + "date_from": self.to_utc_datetime(2021, 10, 19, 8, 42), + "date_to": self.to_utc_datetime(2021, 10, 19, 9, 30), + "resource_id": self.employee1.resource_id.id, + "time_type": "leave", + } + ) + self.assertEqual( + self.employee1.list_work_time_per_day( + self.local_datetime(2021, 10, 19, 8, 42), + self.local_datetime(2021, 10, 19, 12, 30), + ), + [ + (date(2021, 10, 19), 3.0), + ], + ) + self.assertEqual( + self.employee1.list_normal_work_time_per_day( + self.local_datetime(2021, 10, 19, 8, 42), + self.local_datetime(2021, 10, 19, 12, 30), + ), + [ + (date(2021, 10, 19), 3.8), + ], + ) + self.assertEqual( + self.employee1.list_work_time_per_day( + self.to_utc_datetime(2021, 10, 19, 8, 42), + self.to_utc_datetime(2021, 10, 19, 12, 30), + ), + [ + (date(2021, 10, 19), 3.0), + ], + ) + self.assertEqual( + self.employee1.list_normal_work_time_per_day( + self.to_utc_datetime(2021, 10, 19, 8, 42), + self.to_utc_datetime(2021, 10, 19, 12, 30), + ), + [ + (date(2021, 10, 19), 3.8), + ], + ) + self.assertEqual( + self.employee1.list_work_time_per_day( + self.to_utc_datetime(2021, 10, 19, 8, 42).replace(tzinfo=None), + self.to_utc_datetime(2021, 10, 19, 12, 30).replace( + tzinfo=None + ), + ), + [ + (date(2021, 10, 19), 3.0), + ], + ) + self.assertEqual( + self.employee1.list_normal_work_time_per_day( + self.to_utc_datetime(2021, 10, 19, 8, 42).replace(tzinfo=None), + self.to_utc_datetime(2021, 10, 19, 12, 30).replace( + tzinfo=None + ), + ), + [ + (date(2021, 10, 19), 3.8), + ], + ) + self.assertEqual( + self.employee1.list_work_time_per_day( + self.to_utc_datetime(2021, 10, 19, 8, 42).astimezone( + timezone(timedelta(hours=23)) + ), + self.to_utc_datetime(2021, 10, 19, 12, 30).astimezone( + timezone(timedelta(hours=-23)) + ), + ), + [ + (date(2021, 10, 19), 3.0), + ], + ) + self.assertEqual( + self.employee1.list_normal_work_time_per_day( + self.to_utc_datetime(2021, 10, 19, 8, 42).astimezone( + timezone(timedelta(hours=23)) + ), + self.to_utc_datetime(2021, 10, 19, 12, 30).astimezone( + timezone(timedelta(hours=-23)) + ), ), [ - (date(2021, 10, 19), 4.0), + (date(2021, 10, 19), 3.8), ], ) def _get_employee_work_time(self): - from_datetime = self.to_utc_datetime(2021, 10, 19) + from_datetime = self.local_datetime(2021, 10, 19) to_datetime = from_datetime + timedelta(days=2) return self.employee1.list_work_time_per_day( from_datetime, to_datetime diff --git a/resource_work_time_from_contracts/tests/test_work_time_base.py b/resource_work_time_from_contracts/tests/test_work_time_base.py index fe271f140..468a2de83 100644 --- a/resource_work_time_from_contracts/tests/test_work_time_base.py +++ b/resource_work_time_from_contracts/tests/test_work_time_base.py @@ -36,8 +36,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": "08", - "hour_to": "12", + "hour_from": 8.7, + "hour_to": 12.5, "calendar_id": self.full_time_calendar.id, } ) @@ -45,8 +45,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": "13", - "hour_to": "17", + "hour_from": 13.5, + "hour_to": 17.3, "calendar_id": self.full_time_calendar.id, } ) @@ -59,8 +59,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": "08", - "hour_to": "12", + "hour_from": 8.7, + "hour_to": 12.5, "calendar_id": self.morning_calendar.id, } ) @@ -73,8 +73,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": "13", - "hour_to": "17", + "hour_from": 13.5, + "hour_to": 17.3, "calendar_id": self.afternoon_calendar.id, } ) @@ -87,8 +87,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": "08", - "hour_to": "12", + "hour_from": 8.7, + "hour_to": 12.5, "calendar_id": self.four_fifths_calendar.id, } ) @@ -96,12 +96,46 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": "13", - "hour_to": "17", + "hour_from": 13.5, + "hour_to": 17.3, "calendar_id": self.four_fifths_calendar.id, } ) + self.company_calendar = self.env["resource.calendar"].create( + {"name": "Company", "attendance_ids": False} + ) + # the company calendar must contain full-time days + # we use a non-default calendar to ensure it works. + for day in range(7): + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": 8.7, + "hour_to": 12.5, + "calendar_id": self.company_calendar.id, + } + ) + self.env["resource.calendar.attendance"].create( + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": 13.5, + "hour_to": 17.3, + "calendar_id": self.company_calendar.id, + } + ) + self.employee1.company_id.resource_calendar_id = self.company_calendar + + def local_datetime(self, year, month, day, *args, **kwargs): + """ + Create a datetime with the local timezone from local time values + """ + return self.timezone.localize( + datetime.datetime(year, month, day, *args, **kwargs) + ) + def to_utc_datetime(self, year, month, day, *args, **kwargs): """ Create a UTC datetime from local time values From 0520eb5db9bd60142a993ba41baaa7d027aee805 Mon Sep 17 00:00:00 2001 From: hugues de keyzer Date: Wed, 17 Nov 2021 18:06:36 +0100 Subject: [PATCH 06/17] [IMP] force leaves to use company calendar force leaves to use the resource calendar of the company. to compute working hours while taking leaves into account, this module requires that all leaves for all resources are defined in the same resource calendar. --- .../models/__init__.py | 1 + .../models/resource_calendar_leaves.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 resource_work_time_from_contracts/models/resource_calendar_leaves.py diff --git a/resource_work_time_from_contracts/models/__init__.py b/resource_work_time_from_contracts/models/__init__.py index c04b4b6d4..3fb1ac327 100644 --- a/resource_work_time_from_contracts/models/__init__.py +++ b/resource_work_time_from_contracts/models/__init__.py @@ -1,2 +1,3 @@ +from . import resource_calendar_leaves from . import resource_mixin from . import resource_resource diff --git a/resource_work_time_from_contracts/models/resource_calendar_leaves.py b/resource_work_time_from_contracts/models/resource_calendar_leaves.py new file mode 100644 index 000000000..a05380a01 --- /dev/null +++ b/resource_work_time_from_contracts/models/resource_calendar_leaves.py @@ -0,0 +1,21 @@ +# Copyright 2021 Coop IT Easy SCRLfs +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResourceCalendarLeaves(models.Model): + + _inherit = "resource.calendar.leaves" + + # force this field to be equal to the resource_calendar_id of the resource + # (which should be equal to the one of the company). this ensures that all + # leaves for all resources are defined in the same resource calendar, + # which is needed to compute working hours while taking leaves into + # account. + calendar_id = fields.Many2one( + "resource.calendar", + related="resource_id.calendar_id", + readonly=True, + store=True, + ) From a4a6124eb197472d375935afc911495aa29e9392 Mon Sep 17 00:00:00 2001 From: Carmen Bianca Bakker Date: Wed, 9 Mar 2022 17:31:54 +0100 Subject: [PATCH 07/17] [FIX] Run pre-commit Signed-off-by: Carmen Bianca Bakker --- .../models/resource_mixin.py | 20 +++++-------------- .../tests/test_work_days_data.py | 8 ++------ .../tests/test_work_time.py | 12 +++-------- 3 files changed, 10 insertions(+), 30 deletions(-) diff --git a/resource_work_time_from_contracts/models/resource_mixin.py b/resource_work_time_from_contracts/models/resource_mixin.py index dee4c92a6..844ea2a72 100644 --- a/resource_work_time_from_contracts/models/resource_mixin.py +++ b/resource_work_time_from_contracts/models/resource_mixin.py @@ -75,9 +75,7 @@ def list_work_time_per_day( result.append((day, total_hours)) return result - def list_normal_work_time_per_day( - self, from_datetime, to_datetime, domain=None - ): + def list_normal_work_time_per_day(self, from_datetime, to_datetime, domain=None): """ Same as list_work_time_per_day(), but ignoring leaves """ @@ -126,9 +124,7 @@ def get_work_days_data( ) normal_work_time_per_day = dict( super().list_work_time_per_day( - from_datetime.replace( - hour=0, minute=0, second=0, microsecond=0 - ), + from_datetime.replace(hour=0, minute=0, second=0, microsecond=0), to_datetime.replace(hour=0, minute=0, second=0, microsecond=0) + datetime.timedelta(days=1), self.resource_calendar_id, @@ -152,9 +148,7 @@ def get_work_days_data( # we use the same rounding computation as in # resource.resource_mixin.get_work_days_data(). num_days += ( - float_utils.round( - ROUNDING_FACTOR * work_time / normal_work_time - ) + float_utils.round(ROUNDING_FACTOR * work_time / normal_work_time) / ROUNDING_FACTOR ) num_hours += work_time @@ -219,15 +213,11 @@ def _get_work_time_per_contract( ) return work_time_results - def _get_work_time_from_contracts( - self, from_datetime, to_datetime, domain=None - ): + def _get_work_time_from_contracts(self, from_datetime, to_datetime, domain=None): from_datetime, to_datetime = self._localize_datetimes( from_datetime, to_datetime ) - contracts = self._get_active_contracts( - from_datetime.date(), to_datetime.date() - ) + contracts = self._get_active_contracts(from_datetime.date(), to_datetime.date()) work_time_results = self._get_work_time_per_contract( contracts, from_datetime, to_datetime, domain ) diff --git a/resource_work_time_from_contracts/tests/test_work_days_data.py b/resource_work_time_from_contracts/tests/test_work_days_data.py index 185f133a5..647f611fb 100644 --- a/resource_work_time_from_contracts/tests/test_work_days_data.py +++ b/resource_work_time_from_contracts/tests/test_work_days_data.py @@ -396,9 +396,7 @@ def test_timezone(self): self.assertEqual( self.employee1.get_work_days_data( self.to_utc_datetime(2021, 10, 26, 8, 42).replace(tzinfo=None), - self.to_utc_datetime(2021, 10, 26, 12, 30).replace( - tzinfo=None - ), + self.to_utc_datetime(2021, 10, 26, 12, 30).replace(tzinfo=None), ), { "days": 0.375, @@ -408,9 +406,7 @@ def test_timezone(self): self.assertEqual( self.employee1.get_work_days_data( self.to_utc_datetime(2021, 10, 26, 8, 42).replace(tzinfo=None), - self.to_utc_datetime(2021, 10, 26, 12, 30).replace( - tzinfo=None - ), + self.to_utc_datetime(2021, 10, 26, 12, 30).replace(tzinfo=None), compute_leaves=False, ), { diff --git a/resource_work_time_from_contracts/tests/test_work_time.py b/resource_work_time_from_contracts/tests/test_work_time.py index ba53a051d..67be4bd46 100644 --- a/resource_work_time_from_contracts/tests/test_work_time.py +++ b/resource_work_time_from_contracts/tests/test_work_time.py @@ -287,9 +287,7 @@ def test_timezone(self): self.assertEqual( self.employee1.list_work_time_per_day( self.to_utc_datetime(2021, 10, 19, 8, 42).replace(tzinfo=None), - self.to_utc_datetime(2021, 10, 19, 12, 30).replace( - tzinfo=None - ), + self.to_utc_datetime(2021, 10, 19, 12, 30).replace(tzinfo=None), ), [ (date(2021, 10, 19), 3.0), @@ -298,9 +296,7 @@ def test_timezone(self): self.assertEqual( self.employee1.list_normal_work_time_per_day( self.to_utc_datetime(2021, 10, 19, 8, 42).replace(tzinfo=None), - self.to_utc_datetime(2021, 10, 19, 12, 30).replace( - tzinfo=None - ), + self.to_utc_datetime(2021, 10, 19, 12, 30).replace(tzinfo=None), ), [ (date(2021, 10, 19), 3.8), @@ -336,6 +332,4 @@ def test_timezone(self): def _get_employee_work_time(self): from_datetime = self.local_datetime(2021, 10, 19) to_datetime = from_datetime + timedelta(days=2) - return self.employee1.list_work_time_per_day( - from_datetime, to_datetime - ) + return self.employee1.list_work_time_per_day(from_datetime, to_datetime) From 9e6f191bff86f798d9b93a708a71e85b1c16488f Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 11 Mar 2022 08:12:32 +0000 Subject: [PATCH 08/17] [UPD] Update resource_work_time_from_contracts.pot --- .../resource_work_time_from_contracts.pot | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 resource_work_time_from_contracts/i18n/resource_work_time_from_contracts.pot diff --git a/resource_work_time_from_contracts/i18n/resource_work_time_from_contracts.pot b/resource_work_time_from_contracts/i18n/resource_work_time_from_contracts.pot new file mode 100644 index 000000000..7f37cedeb --- /dev/null +++ b/resource_work_time_from_contracts/i18n/resource_work_time_from_contracts.pot @@ -0,0 +1,54 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * resource_work_time_from_contracts +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: resource_work_time_from_contracts +#: model:ir.model.fields,help:resource_work_time_from_contracts.field_hr_employee__resource_calendar_id +#: model:ir.model.fields,help:resource_work_time_from_contracts.field_mrp_workcenter__resource_calendar_id +#: model:ir.model.fields,help:resource_work_time_from_contracts.field_resource_calendar_leaves__calendar_id +#: model:ir.model.fields,help:resource_work_time_from_contracts.field_resource_mixin__resource_calendar_id +#: model:ir.model.fields,help:resource_work_time_from_contracts.field_resource_resource__calendar_id +#: model:ir.model.fields,help:resource_work_time_from_contracts.field_resource_test__resource_calendar_id +msgid "Define the schedule of resource" +msgstr "" + +#. module: resource_work_time_from_contracts +#: model:ir.model,name:resource_work_time_from_contracts.model_resource_calendar_leaves +msgid "Resource Leaves Detail" +msgstr "" + +#. module: resource_work_time_from_contracts +#: model:ir.model,name:resource_work_time_from_contracts.model_resource_mixin +msgid "Resource Mixin" +msgstr "" + +#. module: resource_work_time_from_contracts +#: model:ir.model,name:resource_work_time_from_contracts.model_resource_resource +msgid "Resources" +msgstr "" + +#. module: resource_work_time_from_contracts +#: model:ir.model.fields,field_description:resource_work_time_from_contracts.field_hr_employee__resource_calendar_id +#: model:ir.model.fields,field_description:resource_work_time_from_contracts.field_mrp_workcenter__resource_calendar_id +#: model:ir.model.fields,field_description:resource_work_time_from_contracts.field_resource_calendar_leaves__calendar_id +#: model:ir.model.fields,field_description:resource_work_time_from_contracts.field_resource_mixin__resource_calendar_id +#: model:ir.model.fields,field_description:resource_work_time_from_contracts.field_resource_test__resource_calendar_id +msgid "Working Hours" +msgstr "" + +#. module: resource_work_time_from_contracts +#: model:ir.model.fields,field_description:resource_work_time_from_contracts.field_resource_resource__calendar_id +msgid "Working Time" +msgstr "" + From beee10c38b8d17af2f727252e36f4ad5ecd7a206 Mon Sep 17 00:00:00 2001 From: Carmen Bianca Bakker Date: Wed, 29 Jun 2022 11:24:14 +0200 Subject: [PATCH 09/17] =?UTF-8?q?[FIX]=20SCRLfs=20=E2=86=92=20SC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Carmen Bianca Bakker --- resource_work_time_from_contracts/README.rst | 4 ++-- resource_work_time_from_contracts/__manifest__.py | 4 ++-- .../models/resource_calendar_leaves.py | 2 +- resource_work_time_from_contracts/models/resource_mixin.py | 2 +- resource_work_time_from_contracts/models/resource_resource.py | 2 +- resource_work_time_from_contracts/readme/CONTRIBUTORS.rst | 2 +- .../static/description/index.html | 4 ++-- .../tests/test_work_days_data.py | 2 +- resource_work_time_from_contracts/tests/test_work_time.py | 2 +- .../tests/test_work_time_base.py | 2 +- resource_work_time_from_contracts/views/hr_employee.xml | 2 +- resource_work_time_from_contracts/views/resource_resource.xml | 2 +- 12 files changed, 15 insertions(+), 15 deletions(-) diff --git a/resource_work_time_from_contracts/README.rst b/resource_work_time_from_contracts/README.rst index 76c4190cf..1a0291593 100644 --- a/resource_work_time_from_contracts/README.rst +++ b/resource_work_time_from_contracts/README.rst @@ -68,12 +68,12 @@ Credits Authors ~~~~~~~ -* Coop IT Easy SCRLfs +* Coop IT Easy SC Contributors ~~~~~~~~~~~~ -* `Coop IT Easy SCRLfs `_: +* `Coop IT Easy SC `_: * hugues de keyzer diff --git a/resource_work_time_from_contracts/__manifest__.py b/resource_work_time_from_contracts/__manifest__.py index b35a7bcf1..95e7ca837 100644 --- a/resource_work_time_from_contracts/__manifest__.py +++ b/resource_work_time_from_contracts/__manifest__.py @@ -1,4 +1,4 @@ -# Copyright 2021 Coop IT Easy SCRLfs +# Copyright 2021 Coop IT Easy SC # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { @@ -9,7 +9,7 @@ ), "version": "12.0.1.0.0", "license": "AGPL-3", - "author": "Coop IT Easy SCRLfs", + "author": "Coop IT Easy SC", "website": "https://coopiteasy.be", "category": "Human Resources", "depends": [ diff --git a/resource_work_time_from_contracts/models/resource_calendar_leaves.py b/resource_work_time_from_contracts/models/resource_calendar_leaves.py index a05380a01..a2483761b 100644 --- a/resource_work_time_from_contracts/models/resource_calendar_leaves.py +++ b/resource_work_time_from_contracts/models/resource_calendar_leaves.py @@ -1,4 +1,4 @@ -# Copyright 2021 Coop IT Easy SCRLfs +# Copyright 2021 Coop IT Easy SC # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import fields, models diff --git a/resource_work_time_from_contracts/models/resource_mixin.py b/resource_work_time_from_contracts/models/resource_mixin.py index 844ea2a72..017ace055 100644 --- a/resource_work_time_from_contracts/models/resource_mixin.py +++ b/resource_work_time_from_contracts/models/resource_mixin.py @@ -1,4 +1,4 @@ -# Copyright 2021 Coop IT Easy SCRLfs +# Copyright 2021 Coop IT Easy SC # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import datetime diff --git a/resource_work_time_from_contracts/models/resource_resource.py b/resource_work_time_from_contracts/models/resource_resource.py index ac347633c..134f6934a 100644 --- a/resource_work_time_from_contracts/models/resource_resource.py +++ b/resource_work_time_from_contracts/models/resource_resource.py @@ -1,4 +1,4 @@ -# Copyright 2021 Coop IT Easy SCRLfs +# Copyright 2021 Coop IT Easy SC # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import fields, models diff --git a/resource_work_time_from_contracts/readme/CONTRIBUTORS.rst b/resource_work_time_from_contracts/readme/CONTRIBUTORS.rst index f1efe98f1..eb7c015bf 100644 --- a/resource_work_time_from_contracts/readme/CONTRIBUTORS.rst +++ b/resource_work_time_from_contracts/readme/CONTRIBUTORS.rst @@ -1,3 +1,3 @@ -* `Coop IT Easy SCRLfs `_: +* `Coop IT Easy SC `_: * hugues de keyzer diff --git a/resource_work_time_from_contracts/static/description/index.html b/resource_work_time_from_contracts/static/description/index.html index 4a17e46a3..2ee2acb88 100644 --- a/resource_work_time_from_contracts/static/description/index.html +++ b/resource_work_time_from_contracts/static/description/index.html @@ -414,13 +414,13 @@

    Credits

    Authors

      -
    • Coop IT Easy SCRLfs
    • +
    • Coop IT Easy SC

    Contributors

      -
    • Coop IT Easy SCRLfs:
        +
      • Coop IT Easy SC:
        • hugues de keyzer
      • diff --git a/resource_work_time_from_contracts/tests/test_work_days_data.py b/resource_work_time_from_contracts/tests/test_work_days_data.py index 647f611fb..2f166499d 100644 --- a/resource_work_time_from_contracts/tests/test_work_days_data.py +++ b/resource_work_time_from_contracts/tests/test_work_days_data.py @@ -1,4 +1,4 @@ -# Copyright 2021 Coop IT Easy SCRLfs +# Copyright 2021 Coop IT Easy SC # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from datetime import timedelta, timezone diff --git a/resource_work_time_from_contracts/tests/test_work_time.py b/resource_work_time_from_contracts/tests/test_work_time.py index 67be4bd46..b6a744956 100644 --- a/resource_work_time_from_contracts/tests/test_work_time.py +++ b/resource_work_time_from_contracts/tests/test_work_time.py @@ -1,4 +1,4 @@ -# Copyright 2021 Coop IT Easy SCRLfs +# Copyright 2021 Coop IT Easy SC # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from datetime import date, timedelta, timezone diff --git a/resource_work_time_from_contracts/tests/test_work_time_base.py b/resource_work_time_from_contracts/tests/test_work_time_base.py index 468a2de83..6771c056f 100644 --- a/resource_work_time_from_contracts/tests/test_work_time_base.py +++ b/resource_work_time_from_contracts/tests/test_work_time_base.py @@ -1,4 +1,4 @@ -# Copyright 2021 Coop IT Easy SCRLfs +# Copyright 2021 Coop IT Easy SC # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import datetime diff --git a/resource_work_time_from_contracts/views/hr_employee.xml b/resource_work_time_from_contracts/views/hr_employee.xml index 6b84f26b6..0d448a58d 100644 --- a/resource_work_time_from_contracts/views/hr_employee.xml +++ b/resource_work_time_from_contracts/views/hr_employee.xml @@ -1,5 +1,5 @@ - diff --git a/resource_work_time_from_contracts/views/resource_resource.xml b/resource_work_time_from_contracts/views/resource_resource.xml index 80aa7ed34..ee7b9d0ae 100644 --- a/resource_work_time_from_contracts/views/resource_resource.xml +++ b/resource_work_time_from_contracts/views/resource_resource.xml @@ -1,5 +1,5 @@ - From 98e2c2446143f16211e97a51bc4287f509b6200d Mon Sep 17 00:00:00 2001 From: Victor Champonnois Date: Wed, 19 Jul 2023 16:52:45 +0200 Subject: [PATCH 10/17] [IMP] resource_work_time_from_contracts: pre-commit stuff --- resource_work_time_from_contracts/__manifest__.py | 2 +- .../models/resource_mixin.py | 15 ++++++--------- .../odoo/addons/resource_work_time_from_contracts | 1 + setup/resource_work_time_from_contracts/setup.py | 6 ++++++ 4 files changed, 14 insertions(+), 10 deletions(-) create mode 120000 setup/resource_work_time_from_contracts/odoo/addons/resource_work_time_from_contracts create mode 100644 setup/resource_work_time_from_contracts/setup.py diff --git a/resource_work_time_from_contracts/__manifest__.py b/resource_work_time_from_contracts/__manifest__.py index 95e7ca837..670da4aae 100644 --- a/resource_work_time_from_contracts/__manifest__.py +++ b/resource_work_time_from_contracts/__manifest__.py @@ -10,7 +10,7 @@ "version": "12.0.1.0.0", "license": "AGPL-3", "author": "Coop IT Easy SC", - "website": "https://coopiteasy.be", + "website": "https://github.com/coopiteasy/addons", "category": "Human Resources", "depends": [ "hr_contract", diff --git a/resource_work_time_from_contracts/models/resource_mixin.py b/resource_work_time_from_contracts/models/resource_mixin.py index 017ace055..3f175dfd8 100644 --- a/resource_work_time_from_contracts/models/resource_mixin.py +++ b/resource_work_time_from_contracts/models/resource_mixin.py @@ -194,15 +194,12 @@ def _get_work_time_per_contract( if date_end and to_dt.date() > date_end: # limit to midnight on the day after date_end to completely # include date_end. - to_dt = ( - datetime.datetime( - date_end.year, - date_end.month, - date_end.day, - tzinfo=from_dt.tzinfo, - ) - + datetime.timedelta(days=1) - ) + to_dt = datetime.datetime( + date_end.year, + date_end.month, + date_end.day, + tzinfo=from_dt.tzinfo, + ) + datetime.timedelta(days=1) work_time_results.append( super().list_work_time_per_day( from_dt, diff --git a/setup/resource_work_time_from_contracts/odoo/addons/resource_work_time_from_contracts b/setup/resource_work_time_from_contracts/odoo/addons/resource_work_time_from_contracts new file mode 120000 index 000000000..fd9220d86 --- /dev/null +++ b/setup/resource_work_time_from_contracts/odoo/addons/resource_work_time_from_contracts @@ -0,0 +1 @@ +../../../../resource_work_time_from_contracts \ No newline at end of file diff --git a/setup/resource_work_time_from_contracts/setup.py b/setup/resource_work_time_from_contracts/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/resource_work_time_from_contracts/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From fb80e42fc9c7405adfde3a80b88dfd273cfc8260 Mon Sep 17 00:00:00 2001 From: Victor Champonnois Date: Wed, 19 Jul 2023 16:59:50 +0200 Subject: [PATCH 11/17] [MIG] resource_work_time_from_contracts: Migration to 13.0 [FIX] rename get_work_days_data to _get_work_days_data_batch following this commit https://github.com/OCA/OCB/commit/8c96a887d3f05680c923dcd44f9c70c0e5e0bd32 --- .../__manifest__.py | 2 +- .../models/resource_mixin.py | 6 +-- .../tests/test_work_days_data.py | 40 +++++++++---------- .../tests/test_work_time.py | 1 + 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/resource_work_time_from_contracts/__manifest__.py b/resource_work_time_from_contracts/__manifest__.py index 670da4aae..ca8698e98 100644 --- a/resource_work_time_from_contracts/__manifest__.py +++ b/resource_work_time_from_contracts/__manifest__.py @@ -7,7 +7,7 @@ "Take the contracts of an employee into account when computing work " "time per day" ), - "version": "12.0.1.0.0", + "version": "13.0.1.0.0", "license": "AGPL-3", "author": "Coop IT Easy SC", "website": "https://github.com/coopiteasy/addons", diff --git a/resource_work_time_from_contracts/models/resource_mixin.py b/resource_work_time_from_contracts/models/resource_mixin.py index 3f175dfd8..d5d672221 100644 --- a/resource_work_time_from_contracts/models/resource_mixin.py +++ b/resource_work_time_from_contracts/models/resource_mixin.py @@ -96,7 +96,7 @@ def list_normal_work_time_per_day(self, from_datetime, to_datetime, domain=None) day += delta return result - def get_work_days_data( + def _get_work_days_data_batch( self, from_datetime, to_datetime, @@ -105,7 +105,7 @@ def get_work_days_data( domain=None, ): if calendar or not hasattr(self, "contract_ids"): - return super().get_work_days_data( + return super()._get_work_days_data_batch( from_datetime, to_datetime, compute_leaves, calendar, domain ) # we need the normal work time per day for each day to be able to @@ -146,7 +146,7 @@ def get_work_days_data( continue normal_work_time = normal_work_time_per_day[day] # we use the same rounding computation as in - # resource.resource_mixin.get_work_days_data(). + # resource.resource_mixin._get_work_days_data_batch(). num_days += ( float_utils.round(ROUNDING_FACTOR * work_time / normal_work_time) / ROUNDING_FACTOR diff --git a/resource_work_time_from_contracts/tests/test_work_days_data.py b/resource_work_time_from_contracts/tests/test_work_days_data.py index 2f166499d..ca737abef 100644 --- a/resource_work_time_from_contracts/tests/test_work_days_data.py +++ b/resource_work_time_from_contracts/tests/test_work_days_data.py @@ -200,7 +200,7 @@ def test_with_leaves(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 25), self.local_datetime(2021, 11, 1), compute_leaves=False, @@ -211,7 +211,7 @@ def test_with_leaves(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 26, 8, 42), self.local_datetime(2021, 10, 26, 12, 30), ), @@ -221,7 +221,7 @@ def test_with_leaves(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 26, 8, 42), self.local_datetime(2021, 10, 26, 12, 30), compute_leaves=False, @@ -232,7 +232,7 @@ def test_with_leaves(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 26, 13, 30), self.local_datetime(2021, 10, 26, 17, 18), ), @@ -242,7 +242,7 @@ def test_with_leaves(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 26, 13, 30), self.local_datetime(2021, 10, 26, 17, 18), compute_leaves=False, @@ -253,7 +253,7 @@ def test_with_leaves(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 27, 8, 42), self.local_datetime(2021, 10, 27, 17, 18), ), @@ -263,7 +263,7 @@ def test_with_leaves(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 27, 8, 42), self.local_datetime(2021, 10, 27, 17, 18), compute_leaves=False, @@ -288,7 +288,7 @@ def test_precision(self): } ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 26, 8, 42), self.local_datetime(2021, 10, 26, 8, 48), ), @@ -298,7 +298,7 @@ def test_precision(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 26, 8, 42), self.local_datetime(2021, 10, 26, 9, 6), ), @@ -308,7 +308,7 @@ def test_precision(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 26, 8, 42), self.local_datetime(2021, 10, 26, 9, 18), ), @@ -318,7 +318,7 @@ def test_precision(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 26, 8, 42), self.local_datetime(2021, 10, 26, 9, 36), ), @@ -352,7 +352,7 @@ def test_timezone(self): } ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 26, 8, 42), self.local_datetime(2021, 10, 26, 12, 30), ), @@ -362,7 +362,7 @@ def test_timezone(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.local_datetime(2021, 10, 26, 8, 42), self.local_datetime(2021, 10, 26, 12, 30), compute_leaves=False, @@ -373,7 +373,7 @@ def test_timezone(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.to_utc_datetime(2021, 10, 26, 8, 42), self.to_utc_datetime(2021, 10, 26, 12, 30), ), @@ -383,7 +383,7 @@ def test_timezone(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.to_utc_datetime(2021, 10, 26, 8, 42), self.to_utc_datetime(2021, 10, 26, 12, 30), compute_leaves=False, @@ -394,7 +394,7 @@ def test_timezone(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.to_utc_datetime(2021, 10, 26, 8, 42).replace(tzinfo=None), self.to_utc_datetime(2021, 10, 26, 12, 30).replace(tzinfo=None), ), @@ -404,7 +404,7 @@ def test_timezone(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.to_utc_datetime(2021, 10, 26, 8, 42).replace(tzinfo=None), self.to_utc_datetime(2021, 10, 26, 12, 30).replace(tzinfo=None), compute_leaves=False, @@ -415,7 +415,7 @@ def test_timezone(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.to_utc_datetime(2021, 10, 26, 8, 42).astimezone( timezone(timedelta(hours=23)) ), @@ -429,7 +429,7 @@ def test_timezone(self): }, ) self.assertEqual( - self.employee1.get_work_days_data( + self.employee1._get_work_days_data_batch( self.to_utc_datetime(2021, 10, 26, 8, 42).astimezone( timezone(timedelta(hours=23)) ), @@ -447,4 +447,4 @@ def test_timezone(self): def _get_employee_work_days(self): from_datetime = self.local_datetime(2021, 10, 25) to_datetime = from_datetime + timedelta(days=7) - return self.employee1.get_work_days_data(from_datetime, to_datetime) + return self.employee1._get_work_days_data_batch(from_datetime, to_datetime) diff --git a/resource_work_time_from_contracts/tests/test_work_time.py b/resource_work_time_from_contracts/tests/test_work_time.py index b6a744956..2c9ea8792 100644 --- a/resource_work_time_from_contracts/tests/test_work_time.py +++ b/resource_work_time_from_contracts/tests/test_work_time.py @@ -4,6 +4,7 @@ from datetime import date, timedelta, timezone from .test_work_time_base import TestWorkTimeBase +from odoo.addons.test_resource.tests.test_resource import datetime_str class TestWorkTime(TestWorkTimeBase): From e1d912beac1ffcbbbce63e87a6d9d872b882bdbc Mon Sep 17 00:00:00 2001 From: Victor Champonnois Date: Wed, 19 Jul 2023 17:02:16 +0200 Subject: [PATCH 12/17] [MIG] resource_work_time_from_contracts: Migration to 14.0 --- resource_work_time_from_contracts/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_work_time_from_contracts/__manifest__.py b/resource_work_time_from_contracts/__manifest__.py index ca8698e98..9a784463b 100644 --- a/resource_work_time_from_contracts/__manifest__.py +++ b/resource_work_time_from_contracts/__manifest__.py @@ -7,7 +7,7 @@ "Take the contracts of an employee into account when computing work " "time per day" ), - "version": "13.0.1.0.0", + "version": "14.0.1.0.0", "license": "AGPL-3", "author": "Coop IT Easy SC", "website": "https://github.com/coopiteasy/addons", From 557c0c6da34bf1795de65dd39eebabc507bde77a Mon Sep 17 00:00:00 2001 From: Victor Champonnois Date: Wed, 19 Jul 2023 17:04:56 +0200 Subject: [PATCH 13/17] [MIG] resource_work_time_from_contracts: Migration to 15.0 --- resource_work_time_from_contracts/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_work_time_from_contracts/__manifest__.py b/resource_work_time_from_contracts/__manifest__.py index 9a784463b..7f49042bc 100644 --- a/resource_work_time_from_contracts/__manifest__.py +++ b/resource_work_time_from_contracts/__manifest__.py @@ -7,7 +7,7 @@ "Take the contracts of an employee into account when computing work " "time per day" ), - "version": "14.0.1.0.0", + "version": "15.0.1.0.0", "license": "AGPL-3", "author": "Coop IT Easy SC", "website": "https://github.com/coopiteasy/addons", From bdbce4b8d3a8441b6d04a6244b0cbded4557b239 Mon Sep 17 00:00:00 2001 From: Victor Champonnois Date: Wed, 19 Jul 2023 17:07:09 +0200 Subject: [PATCH 14/17] [MIG] resource_work_time_from_contracts: Migration to 16.0 --- resource_work_time_from_contracts/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_work_time_from_contracts/__manifest__.py b/resource_work_time_from_contracts/__manifest__.py index 7f49042bc..ac3088830 100644 --- a/resource_work_time_from_contracts/__manifest__.py +++ b/resource_work_time_from_contracts/__manifest__.py @@ -7,7 +7,7 @@ "Take the contracts of an employee into account when computing work " "time per day" ), - "version": "15.0.1.0.0", + "version": "16.0.1.0.0", "license": "AGPL-3", "author": "Coop IT Easy SC", "website": "https://github.com/coopiteasy/addons", From 8d91405f57eddb9bb8658faee2f435cdf55a4723 Mon Sep 17 00:00:00 2001 From: Victor Champonnois Date: Wed, 19 Jul 2023 17:17:30 +0200 Subject: [PATCH 15/17] [FIX] import ROUNDING_FACTOR error --- resource_work_time_from_contracts/models/resource_mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_work_time_from_contracts/models/resource_mixin.py b/resource_work_time_from_contracts/models/resource_mixin.py index d5d672221..442869f61 100644 --- a/resource_work_time_from_contracts/models/resource_mixin.py +++ b/resource_work_time_from_contracts/models/resource_mixin.py @@ -9,7 +9,7 @@ from odoo import fields, models from odoo.tools import float_utils -from odoo.addons.resource.models.resource_mixin import ROUNDING_FACTOR +from odoo.addons.resource.models.resource import ROUNDING_FACTOR class ResourceMixin(models.AbstractModel): From 8f9538046c34717ce0945313cf7585d946901da9 Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Tue, 5 Sep 2023 12:01:46 +0200 Subject: [PATCH 16/17] [IMP] resource_work_time_from_contracts: Helpful comment Signed-off-by: Carmen Bianca BAKKER --- .../tests/test_work_time.py | 9 +++++- .../tests/test_work_time_base.py | 32 +++++++++---------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/resource_work_time_from_contracts/tests/test_work_time.py b/resource_work_time_from_contracts/tests/test_work_time.py index 2c9ea8792..116f505fa 100644 --- a/resource_work_time_from_contracts/tests/test_work_time.py +++ b/resource_work_time_from_contracts/tests/test_work_time.py @@ -3,9 +3,10 @@ from datetime import date, timedelta, timezone -from .test_work_time_base import TestWorkTimeBase from odoo.addons.test_resource.tests.test_resource import datetime_str +from .test_work_time_base import TestWorkTimeBase + class TestWorkTime(TestWorkTimeBase): def test_no_contract(self): @@ -167,12 +168,14 @@ def test_with_leaves(self): "time_type": "leave", } ) + # Across Tuesday and Wednesday, only work half a day. self.assertEqual( self._get_employee_work_time(), [ (date(2021, 10, 19), 3.8), ], ) + # On Tuesday morning, don't work. self.assertEqual( self.employee1.list_work_time_per_day( self.local_datetime(2021, 10, 19, 8, 42), @@ -180,6 +183,7 @@ def test_with_leaves(self): ), [], ) + # On Tuesday morning, work when ignoring leaves. self.assertEqual( self.employee1.list_normal_work_time_per_day( self.local_datetime(2021, 10, 19, 8, 42), @@ -189,6 +193,7 @@ def test_with_leaves(self): (date(2021, 10, 19), 3.8), ], ) + # On Tuesday afternoon, work. self.assertEqual( self.employee1.list_work_time_per_day( self.local_datetime(2021, 10, 19, 13, 30), @@ -207,6 +212,7 @@ def test_with_leaves(self): (date(2021, 10, 19), 3.8), ], ) + # On all of Tuesday, work half a day. self.assertEqual( self.employee1.list_work_time_per_day( self.local_datetime(2021, 10, 19, 8, 42), @@ -216,6 +222,7 @@ def test_with_leaves(self): (date(2021, 10, 19), 3.8), ], ) + # On all of Tuesday, work the entire day ignoring leaves. self.assertEqual( self.employee1.list_normal_work_time_per_day( self.local_datetime(2021, 10, 19, 8, 42), diff --git a/resource_work_time_from_contracts/tests/test_work_time_base.py b/resource_work_time_from_contracts/tests/test_work_time_base.py index 6771c056f..4398afed0 100644 --- a/resource_work_time_from_contracts/tests/test_work_time_base.py +++ b/resource_work_time_from_contracts/tests/test_work_time_base.py @@ -36,8 +36,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": 8.7, - "hour_to": 12.5, + "hour_from": 8.7, # 8:42 + "hour_to": 12.5, # 12:30 "calendar_id": self.full_time_calendar.id, } ) @@ -45,8 +45,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": 13.5, - "hour_to": 17.3, + "hour_from": 13.5, # 13:30 + "hour_to": 17.3, # 17:18 "calendar_id": self.full_time_calendar.id, } ) @@ -59,8 +59,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": 8.7, - "hour_to": 12.5, + "hour_from": 8.7, # 8:42 + "hour_to": 12.5, # 12:30 "calendar_id": self.morning_calendar.id, } ) @@ -73,8 +73,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": 13.5, - "hour_to": 17.3, + "hour_from": 13.5, # 13:30 + "hour_to": 17.3, # 17:18 "calendar_id": self.afternoon_calendar.id, } ) @@ -87,8 +87,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": 8.7, - "hour_to": 12.5, + "hour_from": 8.7, # 8:42 + "hour_to": 12.5, # 12:30 "calendar_id": self.four_fifths_calendar.id, } ) @@ -96,8 +96,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": 13.5, - "hour_to": 17.3, + "hour_from": 13.5, # 13:30 + "hour_to": 17.3, # 17:18 "calendar_id": self.four_fifths_calendar.id, } ) @@ -112,8 +112,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": 8.7, - "hour_to": 12.5, + "hour_from": 8.7, # 8:42 + "hour_to": 12.5, # 12:30 "calendar_id": self.company_calendar.id, } ) @@ -121,8 +121,8 @@ def setUp(self): { "name": "Attendance", "dayofweek": str(day), - "hour_from": 13.5, - "hour_to": 17.3, + "hour_from": 13.5, # 13:30 + "hour_to": 17.3, # 17:18 "calendar_id": self.company_calendar.id, } ) From 4503e9f20261123e8e34a00670fbc36d6dabb366 Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Tue, 5 Sep 2023 13:53:32 +0200 Subject: [PATCH 17/17] [FIX] resource_work_time_from_contracts: Make ORM accept UTC datetimes Signed-off-by: Carmen Bianca BAKKER --- .../tests/test_work_time.py | 2 -- .../tests/test_work_time_base.py | 13 ++++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/resource_work_time_from_contracts/tests/test_work_time.py b/resource_work_time_from_contracts/tests/test_work_time.py index 116f505fa..ff78cd8ec 100644 --- a/resource_work_time_from_contracts/tests/test_work_time.py +++ b/resource_work_time_from_contracts/tests/test_work_time.py @@ -3,8 +3,6 @@ from datetime import date, timedelta, timezone -from odoo.addons.test_resource.tests.test_resource import datetime_str - from .test_work_time_base import TestWorkTimeBase diff --git a/resource_work_time_from_contracts/tests/test_work_time_base.py b/resource_work_time_from_contracts/tests/test_work_time_base.py index 4398afed0..acaba1f2c 100644 --- a/resource_work_time_from_contracts/tests/test_work_time_base.py +++ b/resource_work_time_from_contracts/tests/test_work_time_base.py @@ -140,6 +140,13 @@ def to_utc_datetime(self, year, month, day, *args, **kwargs): """ Create a UTC datetime from local time values """ - return self.timezone.localize( - datetime.datetime(year, month, day, *args, **kwargs) - ).astimezone(pytz.utc) + return ( + self.timezone.localize( + datetime.datetime(year, month, day, *args, **kwargs) + ).astimezone(pytz.utc) + # Odoo's ORM refuses to work with datetime objects that have tzinfo + # set. Unset it here instead of doing it manually every time. The + # tzinfo is implicit as a result of this function's name being + # 'to_utc_datetime'. + .replace(tzinfo=None) + )