Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADD] hr_holidays_overlap #99

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions hr_holidays_overlap/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
**This file is going to be generated by oca-gen-addon-readme.**

*Manual changes will be overwritten.*

Please provide content in the ``readme`` directory:

* **DESCRIPTION.rst** (required)
* INSTALL.rst (optional)
* CONFIGURE.rst (optional)
* **USAGE.rst** (optional, highly recommended)
* DEVELOP.rst (optional)
* ROADMAP.rst (optional)
* HISTORY.rst (optional, recommended)
* **CONTRIBUTORS.rst** (optional, highly recommended)
* CREDITS.rst (optional)

Content of this README will also be drawn from the addon manifest,
from keys such as name, authors, maintainers, development_status,
and license.

A good, one sentence summary in the manifest is also highly recommended.


Automatic changelog generation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

`HISTORY.rst` can be auto generated using `towncrier <https://pypi.org/project/towncrier>`_.

Just put towncrier compatible changelog fragments into `readme/newsfragments`
and the changelog file will be automatically generated and updated when a new fragment is added.

Please refer to `towncrier` documentation to know more.

NOTE: the changelog will be automatically generated when using `/ocabot merge $option`.
If you need to run it manually, refer to `OCA/maintainer-tools README <https://github.com/OCA/maintainer-tools>`_.
1 change: 1 addition & 0 deletions hr_holidays_overlap/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
22 changes: 22 additions & 0 deletions hr_holidays_overlap/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2023 Hunki Enterprises BV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0)

{
"name": "Overlapping leaves",
"summary": "Allows to configure holidays that overlap with others",
"version": "15.0.1.0.0",
"development_status": "Beta",
"category": "Human Resources/Time Off",
"website": "https://github.com/OCA/hr-holidays",
"author": "Hunki Enterprises BV, Odoo Community Association (OCA)",
"maintainers": ["hbrunn"],
"license": "AGPL-3",
"depends": [
"hr_holidays",
],
"data": [
"data/hr_leave_type.xml",
"views/hr_leave_type.xml",
],
"demo": [],
}
8 changes: 8 additions & 0 deletions hr_holidays_overlap/data/hr_leave_type.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 Hunki Enterprises BV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0) -->
<data>
<record id="hr_holidays.holiday_status_sl" model="hr.leave.type">
<field name="can_overlap" eval="True" />
</record>
</data>
2 changes: 2 additions & 0 deletions hr_holidays_overlap/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import hr_leave
from . import hr_leave_type
36 changes: 36 additions & 0 deletions hr_holidays_overlap/models/hr_leave.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2023 Hunki Enterprises BV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0)


from odoo import api, models

exclude_sentinel = object()


class HrLeave(models.Model):
_inherit = "hr.leave"

@api.constrains("date_from", "date_to", "employee_id")
def _check_date(self):
"""Allow overlapping if the holiday type allows it"""
return super(
HrLeave,
self.with_context(hr_holidays_overlap_exclude=exclude_sentinel).filtered(
lambda x: not x.holiday_status_id.can_overlap
),
)._check_date()

@api.model
def search_count(self, args):
"""Inject condition to exclude leaves whose type allows overlaps if asked so"""
if self.env.context.get("hr_holidays_overlap_exclude") == exclude_sentinel:
args += [("holiday_status_id.can_overlap", "=", False)]
return super().search_count(args)

def _get_leaves_on_public_holiday(self):
"""Don't count leaves with a type that allows overlap"""
return (
super()
._get_leaves_on_public_holiday()
.filtered(lambda x: not x.holiday_status_id.can_overlap)
)
86 changes: 86 additions & 0 deletions hr_holidays_overlap/models/hr_leave_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright 2023 Hunki Enterprises BV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0)


from odoo import fields, models


class HrLeaveType(models.Model):
_inherit = "hr.leave.type"

can_overlap = fields.Boolean("Allow overlap with other leaves")

def _get_employees_days_per_allocation(self, employee_ids, date=None):
"""Remove overlapping days"""
result = super()._get_employees_days_per_allocation(employee_ids, date=date)

if not date:
date = fields.Date.to_date(
self.env.context.get("default_date_from")
) or fields.Date.context_today(self)

for employee_id in employee_ids:
for this in result[employee_id]:
allocation_dict = result[employee_id][this]
for possible_overlap, _overlap, number_of_days in self._get_overlap(
employee_id
):
for allocation, allocation_days in allocation_dict.items():
if (
not allocation
or isinstance(allocation, str)
or allocation.date_to
and (
allocation.date_to < date or allocation.date_from > date
)
):
continue
allocation_days["virtual_remaining_leaves"] += number_of_days
allocation_days["virtual_leaves_taken"] -= number_of_days
if possible_overlap.state == "validate":
allocation_days["remaining_leaves"] += number_of_days
allocation_days["leaves_taken"] -= number_of_days
break
if "error" in allocation_dict:
allocation_dict["error"][
"virtual_remaining_leaves"
] += number_of_days
allocation_dict[False][
"virtual_remaining_leaves"
] += number_of_days
if not allocation_dict["error"]["virtual_remaining_leaves"]:
del allocation_dict["error"]
del allocation_dict[False]
return result

def _get_overlap(self, employee_id):
"""Return overlapping leaves and the working time of the overlap"""
HrLeave = self.env["hr.leave"]

for possible_overlap in HrLeave.search(
[
("employee_id", "=", employee_id),
("state", "=", "validate"),
("holiday_status_id.can_overlap", "=", True),
]
):
for overlap in HrLeave.search(
[
("employee_id", "=", employee_id),
("state", "in", ("confirm", "validate1", "validate")),
("id", "not in", possible_overlap.ids),
("date_from", "<=", possible_overlap.date_to),
("date_to", ">=", possible_overlap.date_from),
("holiday_status_id", "in", self.ids),
]
):
number_of_days = overlap.employee_id._get_work_days_data_batch(
possible_overlap.date_from
if possible_overlap.date_from >= overlap.date_from
else overlap.date_from,
possible_overlap.date_to
if possible_overlap.date_to <= overlap.date_to
else overlap.date_to,
compute_leaves=False,
)[employee_id]["days"]
yield possible_overlap, overlap, number_of_days
5 changes: 5 additions & 0 deletions hr_holidays_overlap/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
To configure this module, you need to:

1. Go to _Time Off / Configuration / Time Off Types_
2. Open a type you want to allow to overlap with other types
3. Check the box _Allow overlap with other leaves_
9 changes: 9 additions & 0 deletions hr_holidays_overlap/readme/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
This module was developed because standard Odoo does not allow overlapping leaves.

For legislations where being sick during vacation counts as sick day and not a vacation
day, you'd have to cancel the original vacation, and recreate sick leaves / vacation
leaves as necessary.

With this module, you can simply allow overlapping for sick leaves, and add the sick
leave on top of the vacation leave. This will also deduct the sick days taken from the
vacation days taken.
1 change: 1 addition & 0 deletions hr_holidays_overlap/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Holger Brunn <[email protected]> (https://hunki-enterprises.com)
20 changes: 20 additions & 0 deletions hr_holidays_overlap/readme/CREDITS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!--
Audience: everybody.

Purpose: thank financial contributors.

⚠️ Cautions:

- DO NOT include contributors. These go into ./CONTRIBUTORS.rst.
- DO NOT include authors. These go in the "author" key in ../__manifest__.py.
- DO NOT include maintainers. These go in the "maintainers" key in ../__manifest__.py.
- Only include other companies or persons that have financed the development
of this module. Maybe through direct funding, crowdfunding, etc.

⛔ REMOVE THIS FILE if there are no other financial supporters.
-->

The development of this module has been financially supported by:

- Company 1 name
- Company 2 name
1 change: 1 addition & 0 deletions hr_holidays_overlap/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This module makes it possible to have overlapping leaves.
5 changes: 5 additions & 0 deletions hr_holidays_overlap/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
To use this module, you need to:

1. Go to the Time Off app
2. Create and approve a paid leave
3. Note that you can create a sick leave for the same employee in the same interval
Binary file added hr_holidays_overlap/static/description/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions hr_holidays_overlap/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_hr_holidays_overlap
94 changes: 94 additions & 0 deletions hr_holidays_overlap/tests/test_hr_holidays_overlap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Copyright 2023 Hunki Enterprises BV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0)


from datetime import timedelta

from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase


class TestHrHolidaysOverlap(TransactionCase):
def setUp(self):
super().setUp()
# add our leave after the existing demo sick leave to avoid
# unintended overlaps
demo_sick_leave = self.env.ref("hr_holidays.hr_holidays_cl_qdp")
self.paid_leave = demo_sick_leave.copy(
default={
"name": "Paid leave",
"state": "confirm",
"holiday_status_id": self.env.ref("hr_holidays.holiday_status_cl").id,
# those two are needed to bypass hr.leave#copy_data's check
"date_from": demo_sick_leave.date_to,
"date_to": demo_sick_leave.date_to + timedelta(days=2),
}
)
self.paid_leave.action_approve()
self.paid_leave.action_validate()

def test_overlap(self):
"""Test that sick leaves may overlap paid leaves and deducts amounts"""
sick_leave = self.paid_leave.copy(
default={
"name": "Sick leave",
"state": "confirm",
"holiday_status_id": self.env.ref("hr_holidays.holiday_status_sl").id,
"date_from": self.paid_leave.date_from,
"date_to": self.paid_leave.date_from + timedelta(days=1),
}
)
sick_leave.action_validate()
paid_leave = self.paid_leave.with_context(
employee_id=self.paid_leave.employee_id.id
)
# allocation is 20, with 2 days of paid leave it was 18, adding the sick day makes it 19
self.assertEqual(
paid_leave.holiday_status_id.remaining_leaves,
19,
)
# be sure we can use all 19 days for leave
new_leave = self.paid_leave.copy(
default={
"name": "Paid leave",
"state": "confirm",
"holiday_status_id": self.env.ref("hr_holidays.holiday_status_cl").id,
"date_from": self.paid_leave.date_to,
"date_to": self.paid_leave.date_to + timedelta(days=27),
}
)
new_leave.action_validate()
self.assertEqual(new_leave.number_of_days, 19)
self.assertEqual(new_leave.holiday_status_id.virtual_remaining_leaves, 0)

with self.assertRaisesRegex(
ValidationError,
"The number of remaining time off is not sufficient for this time off type.",
):
self.paid_leave.copy(
default={
"name": "Paid leave",
"state": "confirm",
"holiday_status_id": self.env.ref(
"hr_holidays.holiday_status_cl"
).id,
"date_from": new_leave.date_from,
"date_to": new_leave.date_to + timedelta(days=1),
}
)

def test_no_overlap(self):
"""Test that we still don't allow overlapping for types not configured accordingly"""
with self.assertRaisesRegex(
ValidationError, "You can not set 2 time off that overlaps"
):
self.paid_leave.copy(
default={
"state": "confirm",
"holiday_status_id": self.env.ref(
"hr_holidays.holiday_status_unpaid"
).id,
"date_from": self.paid_leave.date_from,
"date_to": self.paid_leave.date_to,
}
)
17 changes: 17 additions & 0 deletions hr_holidays_overlap/views/hr_leave_type.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 Hunki Enterprises BV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0) -->
<data>
<record id="edit_holiday_status_form" model="ir.ui.view">
<field name="model">hr.leave.type</field>
<field name="inherit_id" ref="hr_holidays.edit_holiday_status_form" />
<field name="arch" type="xml">
<field name="time_type" position="after">
<field
name="can_overlap"
attrs="{'invisible': [('time_type', '!=', 'leave')]}"
/>
</field>
</field>
</record>
</data>
1 change: 1 addition & 0 deletions setup/hr_holidays_overlap/odoo/addons/hr_holidays_overlap
6 changes: 6 additions & 0 deletions setup/hr_holidays_overlap/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
Loading