diff --git a/verdigado_attendance/__init__.py b/verdigado_attendance/__init__.py index 0650744..aee8895 100644 --- a/verdigado_attendance/__init__.py +++ b/verdigado_attendance/__init__.py @@ -1 +1,2 @@ from . import models +from . import wizards diff --git a/verdigado_attendance/__manifest__.py b/verdigado_attendance/__manifest__.py index 16ad199..44b0f9b 100644 --- a/verdigado_attendance/__manifest__.py +++ b/verdigado_attendance/__manifest__.py @@ -47,6 +47,7 @@ "views/hr_menu_human_resources_configuration.xml", "views/menu.xml", "views/res_config_settings.xml", + "wizards/verdigado_holidays_wizard.xml", ], "demo": [ "demo/res_users.xml", diff --git a/verdigado_attendance/security/ir.model.access.csv b/verdigado_attendance/security/ir.model.access.csv index b395026..1e862ec 100644 --- a/verdigado_attendance/security/ir.model.access.csv +++ b/verdigado_attendance/security/ir.model.access.csv @@ -3,4 +3,5 @@ access_hr_attendance_report_verdigado,hr.attendance.report.verdigado,hr_attendan access_hr_attendance_user_verdigado,hr.attendance.user.verdigado,hr_attendance.model_hr_attendance,hr_attendance.group_hr_attendance,1,1,1,1 access_resource_calendar_officer_verdigado,resource.calendar.system,resource.model_resource_calendar,hr.group_hr_manager,1,1,1,1 access_resource_calendar_attendance_officer_verdigado,resource.calendar.attendance.system,resource.model_resource_calendar_attendance,hr.group_hr_manager,1,1,1,1 +access_verdigado_holidays_wizard,access_verdigado_holidays_wizard,verdigado_attendance.model_verdigado_holidays_wizard,hr.group_hr_manager,1,1,1,1 hr_attendance_break.access_hr_attendance_break,access_hr_attendance_break,model_hr_attendance_break,base.group_user,1,1,1,1 diff --git a/verdigado_attendance/tests/__init__.py b/verdigado_attendance/tests/__init__.py index dbf018d..4aa5637 100644 --- a/verdigado_attendance/tests/__init__.py +++ b/verdigado_attendance/tests/__init__.py @@ -3,6 +3,7 @@ from . import hr_case from . import test_holidays +from . import test_holiday_wizard from . import test_hr_access from . import test_overtime_calculation from . import test_misc diff --git a/verdigado_attendance/tests/test_holiday_wizard.py b/verdigado_attendance/tests/test_holiday_wizard.py new file mode 100644 index 0000000..3078263 --- /dev/null +++ b/verdigado_attendance/tests/test_holiday_wizard.py @@ -0,0 +1,206 @@ +# Copyright 2023 Hunki Enterprises BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from dateutil.relativedelta import relativedelta + +from odoo import fields +from odoo.tests.common import TransactionCase + + +class TestHolidayWizard(TransactionCase): + def setUp(self): + super().setUp() + self.employee = self.env.ref("hr.employee_qdp") + self.leave_type = self.env.ref("hr_holidays.holiday_status_cl") + + def _test_holidays_wizard(self): + wizard = ( + self.env["verdigado.holidays.wizard"] + .with_context( + active_model="hr.employee", + active_ids=self.employee.ids, + active_id=self.employee.id, + ) + .create({}) + ) + action = wizard.button_assign_vacation() + return self.env[action["res_model"]].search(action["domain"]) + + def test_no_validation(self): + """Test that the holidays wizard creates allocations with slightly changed defaults""" + self.leave_type.allocation_validation_type = "no" + self.employee.calendar_ids.unlink() + allocation = self._test_holidays_wizard() + self.assertFalse(allocation) + + def test_25h_week(self): + """Test an employee with a 2 day week""" + calendar_25h = self.env["resource.calendar"].create( + { + "name": "25h week", + "attendance_ids": [ + ( + 0, + 0, + { + "name": "Monday", + "dayofweek": "0", + "hour_from": 8, + "hour_to": 16, + }, + ), + ( + 0, + 0, + { + "name": "Tuesday", + "dayofweek": "1", + "hour_from": 8, + "hour_to": 16, + }, + ), + ( + 0, + 0, + { + "name": "Wednesday", + "dayofweek": "2", + "hour_from": 8, + "hour_to": 17, + }, + ), + ], + } + ) + self.employee.write( + { + "calendar_ids": [ + (6, 0, []), + ( + 0, + 0, + { + "date_start": fields.Date.today() + + relativedelta(month=1, day=1, years=1), + "date_end": fields.Date.today() + + relativedelta(month=6, day=30, years=1), + "calendar_id": calendar_25h.id, + }, + ), + ], + } + ) + allocation = self._test_holidays_wizard() + self.assertEqual(allocation.employee_id, self.employee) + self.assertEqual(allocation.number_of_days, 9) + + def test_multi_calendar_2_times5days(self): + """Two times 5d week""" + self.employee.write( + { + "calendar_ids": [ + (6, 0, []), + ( + 0, + 0, + { + "date_start": fields.Date.today() + + relativedelta(month=1, day=1, years=1), + "date_end": fields.Date.today() + + relativedelta(month=6, day=30, years=1), + "calendar_id": self.env.ref( + "resource.resource_calendar_std" + ).id, + }, + ), + ( + 0, + 0, + { + "date_start": fields.Date.today() + + relativedelta(month=7, day=1, years=1), + "date_end": fields.Date.today() + + relativedelta(month=12, day=31, years=1), + "calendar_id": self.env.ref( + "resource.resource_calendar_std" + ).id, + }, + ), + ], + } + ) + allocation = self._test_holidays_wizard() + self.assertEqual(allocation.number_of_days, 30) + + def test_multi_calendar_short_4day_long_5day(self): + """4d week in jan/feb, 5d rest""" + four_day_week = self.env.ref("resource.resource_calendar_std_38h") + four_day_week.attendance_ids.filtered(lambda x: x.dayofweek == "4").unlink() + self.employee.write( + { + "calendar_ids": [ + (6, 0, []), + ( + 0, + 0, + { + "date_start": fields.Date.today() + + relativedelta(month=1, day=1, years=1), + "date_end": fields.Date.today() + + relativedelta(month=2, day=28, years=1), + "calendar_id": four_day_week.id, + }, + ), + ( + 0, + 0, + { + "date_start": fields.Date.today() + + relativedelta(month=3, day=1, years=1), + "calendar_id": self.env.ref( + "resource.resource_calendar_std" + ).id, + }, + ), + ], + } + ) + allocation = self._test_holidays_wizard() + self.assertEqual(allocation.number_of_days, 29) + + def test_multi_calendar_short_4day_long_5day_no_month_boundary(self): + """4d week in jan/feb, 5d rest without ending at a month end""" + four_day_week = self.env.ref("resource.resource_calendar_std_38h") + four_day_week.attendance_ids.filtered(lambda x: x.dayofweek == "4").unlink() + self.employee.write( + { + "calendar_ids": [ + (6, 0, []), + ( + 0, + 0, + { + "date_start": fields.Date.today() + + relativedelta(month=1, day=1, years=1), + "date_end": fields.Date.today() + + relativedelta(month=2, day=15, years=1), + "calendar_id": four_day_week.id, + }, + ), + ( + 0, + 0, + { + "date_start": fields.Date.today() + + relativedelta(month=2, day=16, years=1), + "calendar_id": self.env.ref( + "resource.resource_calendar_std" + ).id, + }, + ), + ], + } + ) + allocation = self._test_holidays_wizard() + self.assertEqual(allocation.number_of_days, 29) diff --git a/verdigado_attendance/wizards/__init__.py b/verdigado_attendance/wizards/__init__.py new file mode 100644 index 0000000..0747d40 --- /dev/null +++ b/verdigado_attendance/wizards/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from . import verdigado_holidays_wizard diff --git a/verdigado_attendance/wizards/verdigado_holidays_wizard.py b/verdigado_attendance/wizards/verdigado_holidays_wizard.py new file mode 100644 index 0000000..0e1fc56 --- /dev/null +++ b/verdigado_attendance/wizards/verdigado_holidays_wizard.py @@ -0,0 +1,120 @@ +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +import math +from datetime import date + +from dateutil.relativedelta import relativedelta + +from odoo import _, fields, models + + +class VerdigadoHolidaysWizard(models.TransientModel): + _name = "verdigado.holidays.wizard" + _description = "Create holidays allocations" + + full_vacation = fields.Float( + default=30, + string="Vacation days (100%)", + required=True, + help="Vacation of a FTE in days", + ) + year = fields.Selection( + lambda self: [ + (year, year) + for year in range(fields.Date.today().year, fields.Date.today().year + 3) + ], + default=lambda self: fields.Date.today().year + 1, + required=True, + ) + date_start = fields.Date( + required=True, + string="Validity start", + default=lambda self: fields.Date.today() + + relativedelta(month=1, day=1, years=1), + ) + date_end = fields.Date( + string="Validity end", + default=lambda self: fields.Date.today() + + relativedelta(month=1, day=1, years=1), + ) + leave_type_id = fields.Many2one( + "hr.leave.type", + required=True, + default=lambda self: self.env.ref("hr_holidays.holiday_status_cl", False), + ) + employee_ids = fields.Many2many("hr.employee", string="Employees") + + def button_assign_vacation(self): + interval_start = date(int(self.year), 1, 1) + interval_end = interval_start.replace(month=12, day=31) + days = (interval_end - interval_start).days + allocations = self.env["hr.leave.allocation"].browse([]) + for employee in self.employee_ids or self.env["hr.employee"].browse( + self.env.context.get("active_ids", []) + ): + percentage = 0.0 + for calendar in employee.calendar_ids: + if ( + calendar.date_start + and calendar.date_start >= interval_end + or calendar.date_end + and calendar.date_end <= interval_start + ): + continue + week_days = len( + set(calendar.calendar_id.mapped("attendance_ids.dayofweek")) + ) + # use month precision if calendar starts and ends on a month boundary + # use day precision otherwise + if ( + max(calendar.date_start or interval_start, interval_start).day == 1 + and ( + min(calendar.date_end or interval_end, interval_end) + + relativedelta(days=1) + ).month + != min(calendar.date_end or interval_end, interval_end).month + ): + interval_percentage = round( + float( + min(calendar.date_end or interval_end, interval_end).month + - max( + calendar.date_start or interval_start, interval_start + ).month + + 1 + ) + / (interval_end.month - interval_start.month + 1), + 2, + ) + else: + interval_percentage = round( + ( + min(calendar.date_end or interval_end, interval_end) + - max(calendar.date_start or interval_start, interval_start) + ).days + / days, + 2, + ) + percentage += interval_percentage * round(float(week_days) / 5, 2) + + if percentage: + allocations += self.env["hr.leave.allocation"].create( + { + "name": str(self.date_start.year), + "employee_id": employee.id, + "holiday_status_id": self.leave_type_id.id, + "date_from": self.date_start, + "date_to": self.date_end, + "number_of_days": math.ceil(percentage * self.full_vacation), + } + ) + allocations.filtered(lambda x: x.state == "draft").action_confirm() + allocations.filtered( + lambda x: x.state in ("confirm", "validate1") + ).action_validate() + return { + "type": "ir.actions.act_window", + "res_model": "hr.leave.allocation", + "views": [(False, "list"), (False, "form")], + "domain": [("id", "in", allocations.ids)], + "name": _("Created allocations"), + } diff --git a/verdigado_attendance/wizards/verdigado_holidays_wizard.xml b/verdigado_attendance/wizards/verdigado_holidays_wizard.xml new file mode 100644 index 0000000..93d2cfd --- /dev/null +++ b/verdigado_attendance/wizards/verdigado_holidays_wizard.xml @@ -0,0 +1,50 @@ + + + + verdigado.holidays.wizard + +
+
+ This wizard creates allocations proportional to a FTE with following + values +
+ + + + + + + + +
+
+
+
+
+ + + Allocate vacation + verdigado.holidays.wizard + form + new + + + + +