Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 958d0c4118 | |||
| ee1e1cbe65 | |||
| 004187d9a8 | |||
| 2e392a5afd | |||
| 4d76c78863 |
@@ -1,43 +0,0 @@
|
||||
==========================================================
|
||||
allow_negative_leave_and_allocation_hr_holidays_attendance
|
||||
==========================================================
|
||||
|
||||
manage heritance of Duration in TimeOffCard
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
The module self-installs when ``allow_negative_leave_and_allocation`` and ``hr_holidays_attendance``.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
None yet.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `our issues website <https://github.com/elabore-coop/allow_negative_leave_and_allocation_hr_holidays_attendance/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.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* `Elabore <mailto:laetitia.dacosta@elabore.coop>`
|
||||
|
||||
Funders
|
||||
-------
|
||||
|
||||
The development of this module has been financially supported by:
|
||||
* Elabore (https://elabore.coop)
|
||||
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
This module is maintained by Elabore.
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<t t-name="allow_negative_hr_holidays_attendance.TimeOffCard" t-inherit="hr_holidays.TimeOffCard" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//t[@t-set='duration']" position="replace">
|
||||
<t t-set="duration" t-value="props.requires_allocation
|
||||
? (props.data['allows_negative'] ? data.usable_remaining_leaves : data.virtual_remaining_leaves)
|
||||
: data.overtime_deductible
|
||||
? data.usable_remaining_leaves
|
||||
: data.virtual_leaves_taken" />
|
||||
</xpath>
|
||||
</t>
|
||||
</template>
|
||||
@@ -27,7 +27,7 @@ None yet.
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `our issues website <https://github.com/elabore-coop/allow_negative_leave_and_allocation/issues>`_. In case of
|
||||
Bugs are tracked on `our issues website <https://git.elabore.coop/Elabore/hr-tools/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.
|
||||
@@ -39,7 +39,7 @@ Contributors
|
||||
------------
|
||||
|
||||
* `Alusage : Nicolas JEUDY`
|
||||
* `Elabore <mailto:laetitia.dacosta@elabore.coop>`
|
||||
* `Elabore <mailto:contact@elabore.coop>`
|
||||
|
||||
Funders
|
||||
-------
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hr_employee_stats_sheet",
|
||||
"version": "16.0.2.0.0",
|
||||
"version": "16.0.2.1.0",
|
||||
"description": "Add global sheet for employee stats",
|
||||
"summary": "Add global sheet for employee stats",
|
||||
"author": "Nicolas JEUDY",
|
||||
@@ -8,7 +8,7 @@
|
||||
"license": "LGPL-3",
|
||||
"category": "Human Resources",
|
||||
"depends": [
|
||||
"allow_negative_leave_and_allocation",
|
||||
"hr_negative_leave",
|
||||
"base",
|
||||
"hr",
|
||||
"hr_holidays",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import logging
|
||||
import pytz
|
||||
|
||||
from odoo import api, fields, models
|
||||
from datetime import timedelta
|
||||
from pytz import utc
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -162,7 +164,40 @@ class HrEmployeeStats(models.Model):
|
||||
stat.is_public_holiday = False
|
||||
continue
|
||||
stat.dayofweek = int(stat.date.strftime("%u")) - 1
|
||||
stat.is_public_holiday = bool(stat.sheet_id.employee_id._get_public_holidays(stat.date, stat.date - timedelta(days=1)))
|
||||
stat.is_public_holiday = stat._is_public_holiday_accordig_to_employe_tz()
|
||||
|
||||
def _convert_to_employee_tz(self, date):
|
||||
"""Convert a UTC datetime to the employee's timezone datetime."""
|
||||
self.ensure_one()
|
||||
if not date:
|
||||
return None
|
||||
employee_tz = pytz.timezone(self.employee_id.tz or "UTC")
|
||||
if date.tzinfo is None:
|
||||
dt = pytz.utc.localize(date)
|
||||
return dt.astimezone(employee_tz)
|
||||
|
||||
def _is_public_holiday_accordig_to_employe_tz(self):
|
||||
self.ensure_one()
|
||||
if not self.date or not self.employee_id:
|
||||
return False
|
||||
#get public holidays for the employee
|
||||
public_holidays = self.employee_id._get_public_holidays(
|
||||
self.date, self.date
|
||||
)
|
||||
if not public_holidays:
|
||||
return False
|
||||
ph = public_holidays[0]
|
||||
# Convert public holiday to the employee timezone
|
||||
ph_datetime_from_tz = self._convert_to_employee_tz(ph.date_from)
|
||||
ph_datetime_to_tz = self._convert_to_employee_tz(ph.date_to)
|
||||
# Convert datetime to date
|
||||
ph_date_from = ph_datetime_from_tz.date()
|
||||
ph_date_to = ph_datetime_to_tz.date()
|
||||
# Check if the stat date falls within the public holiday range after conversion in employee tz
|
||||
if ph_date_from <= self.date <= ph_date_to:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _get_gap_hours(self, total_hours, total_recovery_hours, total_leave_hours, total_planned_hours):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -110,8 +110,8 @@ class HrTimesheetSheet(models.Model):
|
||||
("state", "in", ("open", "close")),
|
||||
("date_start", "<=", self.date_end),
|
||||
"|",
|
||||
("date_end", "=", False), # pas de date de fin OU
|
||||
("date_end", ">=", self.date_start), # date de fin après le début
|
||||
("date_end", "=", False), # no end date OR
|
||||
("date_end", ">=", self.date_start), # end date after start
|
||||
],
|
||||
)
|
||||
return contracts
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tests import tagged
|
||||
from datetime import date, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.fields import Date
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestHrEmployeeStatsRecovery(TransactionCase):
|
||||
@@ -19,6 +21,7 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
|
||||
self.employee = self.env['hr.employee'].create({
|
||||
'name': 'Camille',
|
||||
'user_id': self.user.id,
|
||||
'tz': 'Europe/Paris',
|
||||
})
|
||||
self.base_calendar = self.env['resource.calendar'].create({
|
||||
'name': 'Default Calendar',
|
||||
@@ -35,11 +38,13 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
|
||||
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||
],
|
||||
})
|
||||
self.base_calendar.hours_per_day = 7
|
||||
self.employee.resource_calendar_id = self.base_calendar
|
||||
self.env.company.recovery_type_id = self.recovery_type
|
||||
self.env.company.coef = 25
|
||||
|
||||
def _create_timesheet_sheet(self, start_date):
|
||||
#Crée une feuille de temps pour la semaine du lundi au dimanche
|
||||
# Create a timesheet for the week (Monday to Sunday)
|
||||
return self.env['hr_timesheet.sheet'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'date_start': start_date,
|
||||
@@ -47,7 +52,7 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
|
||||
})
|
||||
|
||||
def _create_stats(self, start_date, nb_days, unit_amount):
|
||||
# Crée des temps du lundi au vendredi (ou nb_days)
|
||||
# Create timesheet lines from Monday to Friday (or nb_days)
|
||||
for i in range(nb_days):
|
||||
self.env['account.analytic.line'].create({
|
||||
'employee_id': self.employee.id,
|
||||
@@ -56,16 +61,19 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
|
||||
'account_id': 1,
|
||||
'name': 'Work Entry',
|
||||
})
|
||||
# Génère les hr_employee_stats pour chaque jour de la période
|
||||
# Generate hr.employee.stats for each day of the period
|
||||
stat = self.env['hr.employee.stats'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'date': start_date + timedelta(days=i),
|
||||
})
|
||||
stat._compute_dayofweek()
|
||||
stat._compute_hours()
|
||||
yield stat
|
||||
|
||||
def test_invalide_recovery_type(self):
|
||||
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
|
||||
start_date = date.today() - timedelta(
|
||||
days=date.today().weekday() + 7
|
||||
) # monday of last week
|
||||
self.recovery_type.request_unit = 'day'
|
||||
self.recovery_type.allows_negative = False
|
||||
timesheet_sheet = self._create_timesheet_sheet(start_date)
|
||||
@@ -74,56 +82,58 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
|
||||
timesheet_sheet.action_generate_recovery_allocation()
|
||||
|
||||
def test_no_recovery_hours(self):
|
||||
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
|
||||
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # monday of last week
|
||||
timesheet_sheet = self._create_timesheet_sheet(start_date)
|
||||
for stat in self._create_stats(start_date, 5, 7): #créer 5 stats de 7h chacune
|
||||
# Compare les heures de récupération calculées et le calendrier qui prévoit 7h par jour
|
||||
self.assertEqual(stat.total_hours, 7, "total_hours should be 7",) # l'employé a travaillé 7h chaque jour
|
||||
self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # le calendrier prévoit 7h chaque jour
|
||||
self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # l'employé n'a pas de congé sur ce jour
|
||||
self.assertEqual(stat.total_recovery_hours, 0, "total_recovery_hours should be 0",) # l'employé n'a pas posé de récupération sur ce jour
|
||||
self.assertEqual(stat.gap_hours, 0, "gap_hours should be 0",) # pas de différence entre les heures travaillées et les heures planifiées
|
||||
# La feuille de temps sur cette période doit compter 0h de déficit et 0h de récupération
|
||||
for stat in self._create_stats(start_date, 5, 7): # create 5 stats of 7h each
|
||||
# Compare calculated recovery hours and the calendar which plans 7h per day
|
||||
self.assertEqual(stat.total_hours, 7, "total_hours should be 7",) # the employee worked 7h each day
|
||||
self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # the calendar plans 7h each day
|
||||
self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # the employee has no leave on this day
|
||||
self.assertEqual(stat.total_recovery_hours, 0, "total_recovery_hours should be 0",) # the employee has no recovery on this day
|
||||
self.assertEqual(stat.gap_hours, 0, "gap_hours should be 0",) # no difference between worked and planned hours
|
||||
# The timesheet for this period should count 0 gap hours and 0 recovery hours
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, 0, "timesheet_sheet_gap_hours should be 0",)
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, 0, "timesheet_sheet_recovery_hours should be 0",)
|
||||
|
||||
def test_positive_recovery_hours(self):
|
||||
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
|
||||
start_date = date.today() - timedelta(
|
||||
days=date.today().weekday() + 7
|
||||
) # monday of last week
|
||||
timesheet_sheet = self._create_timesheet_sheet(start_date)
|
||||
for stat in self._create_stats(start_date, 5, 8): #créer 5 stats de 8h chacune
|
||||
# Compare les heures de récupération calculées et le calendrier qui prévoit 7h par jour
|
||||
self.assertEqual(stat.total_hours, 8, "total_hours should be 8",) # l'employé a travaillé 8h chaque jour
|
||||
self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # le calendrier prévoit 7h chaque jour
|
||||
self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # l'employé n'a pas de congé sur ce jour
|
||||
self.assertEqual(stat.total_recovery_hours, 0, "total_recovery_hours should be 0",) # l'employé n'a pas posé de récupération sur ce jour
|
||||
self.assertEqual(stat.gap_hours, 1, "gap_hours should be 1",) # l'employée a travaillé une heure de plus que prévu
|
||||
# La feuille de temps doit compter 5h d'heure sup soit 6,25h de récupération avec la majoration de 25%
|
||||
for stat in self._create_stats(start_date, 5, 8): # create 5 stats of 8h each
|
||||
# Compare calculated recovery hours and the calendar which plans 7h per day
|
||||
self.assertEqual(stat.total_hours, 8, "total_hours should be 8",) # the employee worked 8h each day
|
||||
self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # the calendar plans 7h each day
|
||||
self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # the employee has no leave on this day
|
||||
self.assertEqual(stat.total_recovery_hours, 0, "total_recovery_hours should be 0",) # the employee has no recovery on this day
|
||||
self.assertEqual(stat.gap_hours, 1, "gap_hours should be 1",) # the employee worked one hour more than planned
|
||||
# The timesheet should count 5 overtime hours = 6.25h recovery with 25% coefficient
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, 5, "timesheet_sheet_gap_hours should be 5",)
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, 6.25, "timesheet_sheet_recovery_hours should be 6,25",)
|
||||
# générer l'allocation de récupération depuis la feuille de temps
|
||||
# generate the recovery allocation from the timesheet
|
||||
timesheet_sheet.action_generate_recovery_allocation()
|
||||
recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet.id)])
|
||||
self.assertEqual(len(recovery_allocation), 1, "There should be one recovery")
|
||||
self.assertEqual(recovery_allocation.number_of_days,0.78125, "The recovery allocation should be for 6.25h/8h = 0.78125 day")
|
||||
self.assertEqual(recovery_allocation.number_of_days,0.8928571428571429, "The recovery allocation should be for 6.25h/7h = 0.8928571428571429 day")
|
||||
|
||||
def test_negative_recovery_hours(self):
|
||||
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
|
||||
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # monday of last week
|
||||
timesheet_sheet = self._create_timesheet_sheet(start_date)
|
||||
for stat in self._create_stats(start_date, 5, 6): #créer 5 stats de 6h chacune
|
||||
# Compare les heures de récupération calculées et le calendrier qui prévoit 7h par jour
|
||||
self.assertEqual(stat.total_hours, 6, "total_hours should be 6",) # l'employé a travaillé 6h chaque jour
|
||||
self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # le calendrier prévoit 7h chaque jour
|
||||
self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # l'employé n'a pas de congé sur ce jour
|
||||
self.assertEqual(stat.total_recovery_hours, 0, "total_recovery_hours should be 0",) # l'employé n'a pas posé de récupération sur ce jour
|
||||
self.assertEqual(stat.gap_hours, -1, "gap_hours should be -1",) # l'employée a travaillé une heure de moins que prévu
|
||||
# La feuille de temps doit compter -5h de déficit et -5h de récupération
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, -5, "timesheet_sheet_gap_hours should be -5",) # l'employé a travaillé -5h au total sur la semaine
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, -5, "timesheet_sheet_recovery_hours should be -5",) # -5h sera le montant de l'allocation de récupération (pas de coef appliqué pour les déficites d'heures)
|
||||
# générer l'allocation de récupération depuis la feuille de temps
|
||||
for stat in self._create_stats(start_date, 5, 6): # create 5 stats of 6h each
|
||||
# Compare calculated recovery hours and the calendar which plans 7h per day
|
||||
self.assertEqual(stat.total_hours, 6, "total_hours should be 6",) # the employee worked 6h each day
|
||||
self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # the calendar plans 7h each day
|
||||
self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # the employee has no leave on this day
|
||||
self.assertEqual(stat.total_recovery_hours, 0, "total_recovery_hours should be 0",) # the employee has no recovery on this day
|
||||
self.assertEqual(stat.gap_hours, -1, "gap_hours should be -1",) # the employee worked one hour less than planned
|
||||
# The timesheet should count -5 gap hours and -5 recovery hours
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, -5, "timesheet_sheet_gap_hours should be -5",) # total gap hours for the week
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, -5, "timesheet_sheet_recovery_hours should be -5",) # -5h will be the recovery allocation (no coef applied for deficits)
|
||||
# generate the recovery allocation from the timesheet
|
||||
timesheet_sheet.action_generate_recovery_allocation()
|
||||
recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet.id)])
|
||||
self.assertEqual(len(recovery_allocation), 1, "There should be one recovery")
|
||||
self.assertEqual(recovery_allocation.number_of_days, -0.625, "The recovery allocation should be for -5/8 hours = ")
|
||||
self.assertEqual(recovery_allocation.number_of_days, -0.7142857142857143, "The recovery allocation should be for -5/7 hours = 0.7142857142857143 days")
|
||||
|
||||
def test_recovery_hours_part_time_employee(self):
|
||||
part_time_calendar = self.env['resource.calendar'].create({
|
||||
@@ -140,19 +150,18 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
|
||||
],
|
||||
})
|
||||
self.employee.resource_calendar_id = part_time_calendar.id
|
||||
|
||||
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
|
||||
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # monday of last week
|
||||
timesheet_sheet = self._create_timesheet_sheet(start_date)
|
||||
for stat in self._create_stats(start_date, 4, 8): #créer 4 stats de 8h chacune
|
||||
# Compare les heures de récupération calculées et le calendrier qui prévoit 7h par jour pendant 4 jours
|
||||
self.assertEqual(stat.total_hours, 8, "total_hours should be 8",) # l'employé a travaillé 6h chaque jour
|
||||
self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # le calendrier prévoit 7h chaque jour
|
||||
self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # l'employé n'a pas de congé sur ce jour
|
||||
self.assertEqual(stat.total_recovery_hours, 0, "total_recovery_hours should be 0",) # l'employé n'a pas posé de récupération sur ce jour
|
||||
self.assertEqual(stat.gap_hours, 1, "gap_hours should be 1",) # l'employée a travaillé une heure de moins que prévu
|
||||
# La feuille de temps doit compter 4h d'heure sup soit 5h de récupération avec la majoration de 25%
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, 4, "timesheet_sheet_gap_hours should be 4",) # l'employé a travaillé supplémentaire 4h au total sur la semaine
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, 5, "timesheet_sheet_recovery_hours should be 5",) # 5h sera le montant de l'allocation de récupération (coef de 25% de majoration)
|
||||
for stat in self._create_stats(start_date, 4, 8): # create 4 stats of 8h each
|
||||
# Compare calculated recovery hours and the calendar which plans 7h per day for 4 days
|
||||
self.assertEqual(stat.total_hours, 8, "total_hours should be 8",) # the employee worked 8h each day
|
||||
self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # the calendar plans 7h each day
|
||||
self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # the employee has no leave on this day
|
||||
self.assertEqual(stat.total_recovery_hours, 0, "total_recovery_hours should be 0",) # the employee has no recovery on this day
|
||||
self.assertEqual(stat.gap_hours, 1, "gap_hours should be 1",) # the employee worked one hour more than planned
|
||||
# The timesheet should count 4 overtime hours = 5 recovery hours with 25% coefficient
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, 4, "timesheet_sheet_gap_hours should be 4",) # total extra hours for the week
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, 5, "timesheet_sheet_recovery_hours should be 5",) # 5h will be the recovery allocation (25% coef)
|
||||
|
||||
def test_recovery_hours_change_contract(self):
|
||||
part_time_calendar = self.env['resource.calendar'].create({
|
||||
@@ -172,8 +181,8 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
|
||||
self.env['hr.contract'].create({
|
||||
'name': 'Contract 1',
|
||||
'employee_id': self.employee.id,
|
||||
'date_start': date.today() - timedelta(days=300), # date de début factice
|
||||
'date_end': date.today() - timedelta(days= date.today().weekday() + 5), # date de fin le mercredi de la semaine dernière
|
||||
'date_start': date.today() - timedelta(days=300), # fake start date
|
||||
'date_end': date.today() - timedelta(days= date.today().weekday() + 5), # end date: wednesday of last week
|
||||
'resource_calendar_id': self.base_calendar.id,
|
||||
'wage': 2000,
|
||||
'state': 'close',
|
||||
@@ -182,16 +191,16 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
|
||||
'name': 'Contract 2',
|
||||
'employee_id': self.employee.id,
|
||||
'state': 'open',
|
||||
'date_start': date.today() - timedelta(days= date.today().weekday() + 4), # date de début le jeudi de la semaine dernière
|
||||
'date_start': date.today() - timedelta(days= date.today().weekday() + 4), # start date: thursday of last week
|
||||
'date_end': False,
|
||||
'resource_calendar_id': self.base_calendar.id,
|
||||
'wage': 1500,
|
||||
})
|
||||
self.employee.resource_calendar_id = part_time_calendar.id
|
||||
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
|
||||
#create a timesheet with period including the change of contract
|
||||
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # monday of last week
|
||||
# create a timesheet with period including the change of contract
|
||||
timesheet_sheet = self._create_timesheet_sheet(start_date)
|
||||
#the create of recovery allocation should raise an error
|
||||
# the creation of the recovery allocation should raise an error
|
||||
with self.assertRaises(UserError):
|
||||
timesheet_sheet.action_generate_recovery_allocation()
|
||||
|
||||
@@ -242,9 +251,33 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
|
||||
recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet_2.id)])
|
||||
self.assertEqual(len(recovery_allocation), 1, "There should be one recovery")
|
||||
|
||||
|
||||
|
||||
|
||||
def test_public_holiday(self):
|
||||
# create a public holiday
|
||||
self.env["resource.calendar.leaves"].create(
|
||||
{
|
||||
"name": "1 mai 2025",
|
||||
"date_from": datetime(2025,4,30,22,0,0),
|
||||
"date_to": datetime(2025,5,1,21,0,0),
|
||||
}
|
||||
)
|
||||
#create 5 stats of 7h each including the public holiday on 1st may
|
||||
stats = self._create_stats(Date.to_date("2025-04-28"), 5, 7)
|
||||
for stat in stats:
|
||||
stat._compute_dayofweek()
|
||||
stat._compute_hours()
|
||||
#create 1 timesheet sheet from monday to friday including the public holiday on 1st may
|
||||
timesheet_sheet = self.env['hr_timesheet.sheet'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'date_start': "2025-04-28",
|
||||
'date_end': "2025-05-04",
|
||||
})
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, 7, "timesheet_sheet_gap_hours should be 7",)
|
||||
self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, 8.75, "timesheet_sheet_recovery_hours should be 8,75",)
|
||||
|
||||
timesheet_sheet.action_generate_recovery_allocation()
|
||||
recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet.id)])
|
||||
self.assertEqual(len(recovery_allocation), 1, "Il doit y avoir une allocation de récupération générée")
|
||||
self.assertEqual(recovery_allocation.number_of_days,1.25, "The recovery allocation should be 1,25 days")
|
||||
|
||||
|
||||
|
||||
|
||||
44
hr_negative_leave/README.rst
Normal file
44
hr_negative_leave/README.rst
Normal file
@@ -0,0 +1,44 @@
|
||||
===================================
|
||||
allow_negative_leave_and_allocation
|
||||
===================================
|
||||
|
||||
allow negative leaves, manage negative leave balances and negative allocations
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Use Odoo normal module installation procedure to install
|
||||
``allow_negative_leave_and_allocation``.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
None yet.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `our issues website <https://git.elabore.coop/Elabore/hr-tools/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.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* `Elabore <mailto:contact@elabore.coop>`
|
||||
|
||||
Funders
|
||||
-------
|
||||
|
||||
The development of this module has been financially supported by:
|
||||
* Elabore (https://elabore.coop)
|
||||
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
This module is maintained by Elabore.
|
||||
1
hr_negative_leave/__init__.py
Normal file
1
hr_negative_leave/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -1,30 +1,28 @@
|
||||
# Copyright 2025 Elabore ()
|
||||
# Copyright 2024 Elabore ()
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "allow_negative_leave_and_allocation_hr_holidays_attendance",
|
||||
"version": "16.0.1.0.0",
|
||||
"name": "hr_negative_leave",
|
||||
"version": "16.0.3.0.0",
|
||||
"author": "Elabore",
|
||||
"website": "https://elabore.coop",
|
||||
"maintainer": "Elabore",
|
||||
"license": "AGPL-3",
|
||||
"category": "HR",
|
||||
"summary": "manage heritance of Duration in TimeOffCard",
|
||||
"category": "hr",
|
||||
"summary": "allow negative leaves, manage negative leave balances and negative allocations",
|
||||
# any module necessary for this one to work correctly
|
||||
"depends": [
|
||||
"base","allow_negative_leave_and_allocation","hr_holidays_attendance",
|
||||
"base","hr_holidays",
|
||||
],
|
||||
"qweb": [],
|
||||
"external_dependencies": {
|
||||
"python": [],
|
||||
},
|
||||
# always loaded
|
||||
"data": [],
|
||||
"assets": {
|
||||
'web.assets_backend': [
|
||||
'allow_negative_leave_and_allocation_hr_holidays_attendance/static/src/xml/time_off_card.xml',
|
||||
"data": [
|
||||
"views/hr_leave_type_views.xml",
|
||||
"views/hr_leave_views.xml",
|
||||
],
|
||||
},
|
||||
# only loaded in demonstration mode
|
||||
"demo": [],
|
||||
"js": [],
|
||||
@@ -32,6 +30,6 @@
|
||||
"installable": True,
|
||||
# Install this module automatically if all dependency have been previously
|
||||
# and independently installed. Used for synergetic or glue modules.
|
||||
"auto_install": True,
|
||||
"auto_install": False,
|
||||
"application": False,
|
||||
}
|
||||
54
hr_negative_leave/i18n/fr.po
Normal file
54
hr_negative_leave/i18n/fr.po
Normal file
@@ -0,0 +1,54 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * hr_negative_leave
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-31 08:25+0000\n"
|
||||
"PO-Revision-Date: 2025-10-31 08:25+0000\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: hr_negative_leave
|
||||
#: model:ir.model.fields,field_description:hr_negative_leave.field_hr_leave_type__allows_negative
|
||||
msgid "Allow Negative Leaves"
|
||||
msgstr "Autoriser les demandes et les soldes de congés négatifs"
|
||||
|
||||
#. module: hr_negative_leave
|
||||
#: model_terms:ir.ui.view,arch_db:hr_negative_leave.hr_leave_type_negative_leave
|
||||
msgid "Allow negative"
|
||||
msgstr "Autoriser les soldes négatifs"
|
||||
|
||||
#. module: hr_negative_leave
|
||||
#: model:ir.model.fields,help:hr_negative_leave.field_hr_leave_type__allows_negative
|
||||
msgid ""
|
||||
"If checked, users request can exceed the allocated days and balance can go "
|
||||
"in negative."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_negative_leave
|
||||
#: model:ir.model.fields,field_description:hr_negative_leave.field_hr_leave_type__remaining_leaves_allowing_negative
|
||||
msgid "Remaining Leaves when Negative Allowed"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_negative_leave
|
||||
#: model:ir.model.fields,field_description:hr_negative_leave.field_hr_leave__smart_search
|
||||
#: model:ir.model.fields,field_description:hr_negative_leave.field_hr_leave_type__smart_search
|
||||
msgid "Smart Search"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_negative_leave
|
||||
#: model:ir.model,name:hr_negative_leave.model_hr_leave
|
||||
msgid "Time Off"
|
||||
msgstr "Congés"
|
||||
|
||||
#. module: hr_negative_leave
|
||||
#: model:ir.model,name:hr_negative_leave.model_hr_leave_type
|
||||
msgid "Time Off Type"
|
||||
msgstr "Type de congés"
|
||||
54
hr_negative_leave/i18n/hr_negative_leave.pot
Normal file
54
hr_negative_leave/i18n/hr_negative_leave.pot
Normal file
@@ -0,0 +1,54 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * hr_negative_leave
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-31 08:23+0000\n"
|
||||
"PO-Revision-Date: 2025-10-31 08:23+0000\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: hr_negative_leave
|
||||
#: model:ir.model.fields,field_description:hr_negative_leave.field_hr_leave_type__allows_negative
|
||||
msgid "Allow Negative Leaves"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_negative_leave
|
||||
#: model_terms:ir.ui.view,arch_db:hr_negative_leave.hr_leave_type_negative_leave
|
||||
msgid "Allow negative"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_negative_leave
|
||||
#: model:ir.model.fields,help:hr_negative_leave.field_hr_leave_type__allows_negative
|
||||
msgid ""
|
||||
"If checked, users request can exceed the allocated days and balance can go "
|
||||
"in negative."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_negative_leave
|
||||
#: model:ir.model.fields,field_description:hr_negative_leave.field_hr_leave_type__remaining_leaves_allowing_negative
|
||||
msgid "Remaining Leaves when Negative Allowed"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_negative_leave
|
||||
#: model:ir.model.fields,field_description:hr_negative_leave.field_hr_leave__smart_search
|
||||
#: model:ir.model.fields,field_description:hr_negative_leave.field_hr_leave_type__smart_search
|
||||
msgid "Smart Search"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_negative_leave
|
||||
#: model:ir.model,name:hr_negative_leave.model_hr_leave
|
||||
msgid "Time Off"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_negative_leave
|
||||
#: model:ir.model,name:hr_negative_leave.model_hr_leave_type
|
||||
msgid "Time Off Type"
|
||||
msgstr ""
|
||||
1
hr_negative_leave/models/__init__.py
Normal file
1
hr_negative_leave/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import hr_leave_type, hr_leave
|
||||
16
hr_negative_leave/models/hr_leave.py
Normal file
16
hr_negative_leave/models/hr_leave.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
class HrLeave(models.Model):
|
||||
_inherit = "hr.leave"
|
||||
|
||||
@api.constrains('state', 'number_of_days', 'holiday_status_id')
|
||||
def _check_holidays(self):
|
||||
# Keep only leaves that do not allow negative balances
|
||||
to_check = self.filtered(lambda h: not h.holiday_status_id.allows_negative)
|
||||
if to_check:
|
||||
super(HrLeave, to_check)._check_holidays()
|
||||
241
hr_negative_leave/models/hr_leave_type.py
Normal file
241
hr_negative_leave/models/hr_leave_type.py
Normal file
@@ -0,0 +1,241 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
from datetime import time, timedelta
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.translate import _
|
||||
from odoo.addons.resource.models.resource import Intervals
|
||||
|
||||
class HolidaysType(models.Model):
|
||||
_inherit = "hr.leave.type"
|
||||
|
||||
# negative time off
|
||||
allows_negative = fields.Boolean(string='Allow Negative Leaves',
|
||||
help="If checked, users request can exceed the allocated days and balance can go in negative.")
|
||||
|
||||
remaining_leaves_allowing_negative = fields.Float(
|
||||
string="Remaining Leaves when Negative Allowed",
|
||||
compute='_compute_remaining_leaves_allowing_negative',
|
||||
)
|
||||
|
||||
def _compute_remaining_leaves_allowing_negative(self):
|
||||
for holiday_type in self:
|
||||
if holiday_type.allows_negative:
|
||||
# if left != usable : remaining_leaves_allowing_negative = left + usable
|
||||
if holiday_type.virtual_remaining_leaves < 0 (holiday_type.max_leaves - holiday_type.virtual_leaves_taken) != holiday_type.virtual_remaining_leaves:
|
||||
holiday_type.remaining_leaves_allowing_negative = holiday_type.max_leaves - holiday_type.virtual_leaves_taken + holiday_type.virtual_remaining_leaves
|
||||
else:
|
||||
# else : remaining_leaves_allowing_negative = left as usual
|
||||
holiday_type.remaining_leaves_allowing_negative = holiday_type.max_leaves - holiday_type.virtual_leaves_taken
|
||||
else:
|
||||
holiday_type.remaining_leaves_allowing_negative = None
|
||||
|
||||
@api.depends('requires_allocation')
|
||||
def _compute_valid(self):
|
||||
res = super()._compute_valid()
|
||||
for holiday_type in res:
|
||||
if not holiday_type.has_valid_allocation:
|
||||
holiday_type.has_valid_allocation = holiday_type.allows_negative
|
||||
|
||||
#overwrite _get_employees_days_per_allocation() from hr_holidays module
|
||||
def _get_employees_days_per_allocation(self, employee_ids, date=None):
|
||||
if not date:
|
||||
date = fields.Date.to_date(self.env.context.get('default_date_from')) or fields.Date.context_today(self)
|
||||
|
||||
leaves_domain = [
|
||||
('employee_id', 'in', employee_ids),
|
||||
('state', 'in', ['confirm', 'validate1', 'validate']),
|
||||
('holiday_status_id', 'in', self.ids)
|
||||
]
|
||||
if self.env.context.get("ignore_future"):
|
||||
leaves_domain.append(('date_from', '<=', date))
|
||||
leaves = self.env['hr.leave'].search(leaves_domain)
|
||||
|
||||
allocations = self.env['hr.leave.allocation'].with_context(active_test=False).search([
|
||||
('employee_id', 'in', employee_ids),
|
||||
('state', 'in', ['validate']),
|
||||
('holiday_status_id', 'in', self.ids),
|
||||
])
|
||||
|
||||
# The allocation_employees dictionary groups the allocations based on the employee and the holiday type
|
||||
# The structure is the following:
|
||||
# - KEYS:
|
||||
# allocation_employees
|
||||
# |--employee_id
|
||||
# |--holiday_status_id
|
||||
# - VALUES:
|
||||
# Intervals with the start and end date of each allocation and associated allocations within this interval
|
||||
allocation_employees = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
### Creation of the allocation intervals ###
|
||||
for holiday_status_id in allocations.holiday_status_id:
|
||||
for employee_id in employee_ids:
|
||||
allocation_intervals = Intervals([(
|
||||
fields.datetime.combine(allocation.date_from, time.min),
|
||||
fields.datetime.combine(allocation.date_to or datetime.date.max, time.max),
|
||||
allocation)
|
||||
for allocation in allocations.filtered(lambda allocation: allocation.employee_id.id == employee_id and allocation.holiday_status_id == holiday_status_id)])
|
||||
|
||||
allocation_employees[employee_id][holiday_status_id] = allocation_intervals
|
||||
|
||||
# The leave_employees dictionary groups the leavess based on the employee and the holiday type
|
||||
# The structure is the following:
|
||||
# - KEYS:
|
||||
# leave_employees
|
||||
# |--employee_id
|
||||
# |--holiday_status_id
|
||||
# - VALUES:
|
||||
# Intervals with the start and end date of each leave and associated leave within this interval
|
||||
leaves_employees = defaultdict(lambda: defaultdict(list))
|
||||
leave_intervals = []
|
||||
|
||||
### Creation of the leave intervals ###
|
||||
if leaves:
|
||||
for holiday_status_id in leaves.holiday_status_id:
|
||||
for employee_id in employee_ids:
|
||||
leave_intervals = Intervals([(
|
||||
fields.datetime.combine(leave.date_from, time.min),
|
||||
fields.datetime.combine(leave.date_to, time.max),
|
||||
leave)
|
||||
for leave in leaves.filtered(lambda leave: leave.employee_id.id == employee_id and leave.holiday_status_id == holiday_status_id)])
|
||||
|
||||
leaves_employees[employee_id][holiday_status_id] = leave_intervals
|
||||
|
||||
# allocation_days_consumed is a dictionary to map the number of days/hours of leaves taken per allocation
|
||||
# The structure is the following:
|
||||
# - KEYS:
|
||||
# allocation_days_consumed
|
||||
# |--employee_id
|
||||
# |--holiday_status_id
|
||||
# |--allocation
|
||||
# |--virtual_leaves_taken
|
||||
# |--leaves_taken
|
||||
# |--virtual_remaining_leaves
|
||||
# |--remaining_leaves
|
||||
# |--max_leaves
|
||||
# |--closest_allocation_to_expire
|
||||
# - VALUES:
|
||||
# Integer representing the number of (virtual) remaining leaves, (virtual) leaves taken or max leaves for each allocation.
|
||||
# The unit is in hour or days depending on the leave type request unit
|
||||
allocations_days_consumed = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0))))
|
||||
|
||||
company_domain = [('company_id', 'in', list(set(self.env.company.ids + self.env.context.get('allowed_company_ids', []))))]
|
||||
|
||||
### Existing leaves assigned to allocations ###
|
||||
if leaves_employees:
|
||||
for employee_id, leaves_interval_by_status in leaves_employees.items():
|
||||
for holiday_status_id in leaves_interval_by_status:
|
||||
days_consumed = allocations_days_consumed[employee_id][holiday_status_id]
|
||||
if allocation_employees[employee_id][holiday_status_id]:
|
||||
allocations = allocation_employees[employee_id][holiday_status_id] & leaves_interval_by_status[holiday_status_id]
|
||||
available_allocations = self.env['hr.leave.allocation']
|
||||
for allocation_interval in allocations._items:
|
||||
available_allocations |= allocation_interval[2]
|
||||
# Consume the allocations that are close to expiration first
|
||||
sorted_available_allocations = available_allocations.filtered('date_to').sorted(key='date_to')
|
||||
sorted_available_allocations += available_allocations.filtered(lambda allocation: not allocation.date_to)
|
||||
leave_intervals = leaves_interval_by_status[holiday_status_id]._items
|
||||
sorted_allocations_with_remaining_leaves = self.env['hr.leave.allocation']
|
||||
for leave_interval in leave_intervals:
|
||||
leaves = leave_interval[2]
|
||||
for leave in leaves:
|
||||
if leave.leave_type_request_unit in ['day', 'half_day']:
|
||||
leave_duration = leave.number_of_days
|
||||
leave_unit = 'days'
|
||||
else:
|
||||
leave_duration = leave.number_of_hours_display
|
||||
leave_unit = 'hours'
|
||||
if holiday_status_id.requires_allocation != 'no':
|
||||
for available_allocation in sorted_available_allocations:
|
||||
# if the allocation is not valid for the leave period, continue
|
||||
if (available_allocation.date_to and available_allocation.date_to < leave.date_from.date()) \
|
||||
or (available_allocation.date_from > leave.date_to.date()):
|
||||
continue
|
||||
# calculate the number of days/hours for this allocation (allocation days/hours - leaves already taken)
|
||||
virtual_remaining_leaves = (available_allocation.number_of_days if leave_unit == 'days' else available_allocation.number_of_hours_display) - allocations_days_consumed[employee_id][holiday_status_id][available_allocation]['virtual_leaves_taken']
|
||||
###########################################
|
||||
# Modification for leaves allowing negative #
|
||||
###########################################
|
||||
# if negative is allowed for this leave type, we can exceed the number of available days in this allocation
|
||||
if holiday_status_id.allows_negative:
|
||||
max_leaves = leave_duration
|
||||
else:
|
||||
# if negative is not allowed for this leave type, then we cannot exceed the allocation amount
|
||||
# the max leaves for this allocation is the minimum between the remaining available days and the leave duration
|
||||
max_leaves = min(virtual_remaining_leaves, leave_duration)
|
||||
# the new calculation of days taken for this allocation is previous taken + max_leaves (which can never exceed the allocation total)
|
||||
days_consumed[available_allocation]['virtual_leaves_taken'] += max_leaves
|
||||
if leave.state == 'validate':
|
||||
days_consumed[available_allocation]['leaves_taken'] += max_leaves
|
||||
leave_duration -= max_leaves
|
||||
# Check valid allocations with still availabe leaves on it
|
||||
if days_consumed[available_allocation]['virtual_remaining_leaves'] > 0 and available_allocation.date_to and available_allocation.date_to > date:
|
||||
sorted_allocations_with_remaining_leaves |= available_allocation
|
||||
if leave_duration > 0:
|
||||
# There are not enough allocation for the number of leaves
|
||||
days_consumed[False]['virtual_remaining_leaves'] -= leave_duration
|
||||
else:
|
||||
days_consumed[False]['virtual_leaves_taken'] += leave_duration
|
||||
if leave.state == 'validate':
|
||||
days_consumed[False]['leaves_taken'] += leave_duration
|
||||
# no need to sort the allocations again
|
||||
allocations_days_consumed[employee_id][holiday_status_id][False]['closest_allocation_to_expire'] = sorted_allocations_with_remaining_leaves[0] if sorted_allocations_with_remaining_leaves else False
|
||||
|
||||
# Future available leaves
|
||||
future_allocations_date_from = fields.datetime.combine(date, time.min)
|
||||
future_allocations_date_to = fields.datetime.combine(date, time.max) + timedelta(days=5*365)
|
||||
for employee_id, allocation_intervals_by_status in allocation_employees.items():
|
||||
employee = self.env['hr.employee'].browse(employee_id)
|
||||
for holiday_status_id, intervals in allocation_intervals_by_status.items():
|
||||
if not intervals:
|
||||
continue
|
||||
future_allocation_intervals = intervals & Intervals([(
|
||||
future_allocations_date_from,
|
||||
future_allocations_date_to,
|
||||
self.env['hr.leave'])])
|
||||
search_date = date
|
||||
closest_allocations = self.env['hr.leave.allocation']
|
||||
for interval in intervals._items:
|
||||
closest_allocations |= interval[2]
|
||||
allocations_with_remaining_leaves = self.env['hr.leave.allocation']
|
||||
for interval_from, interval_to, interval_allocations in future_allocation_intervals._items:
|
||||
if interval_from.date() > search_date:
|
||||
continue
|
||||
interval_allocations = interval_allocations.filtered('active')
|
||||
if not interval_allocations:
|
||||
continue
|
||||
# If no end date to the allocation, consider the number of days remaining as infinite
|
||||
employee_quantity_available = (
|
||||
employee._get_work_days_data_batch(interval_from, interval_to, compute_leaves=False, domain=company_domain)[employee_id]
|
||||
if interval_to != future_allocations_date_to
|
||||
else {'days': float('inf'), 'hours': float('inf')}
|
||||
)
|
||||
reached_remaining_days_limit = False
|
||||
for allocation in interval_allocations:
|
||||
if allocation.date_from > search_date:
|
||||
continue
|
||||
days_consumed = allocations_days_consumed[employee_id][holiday_status_id][allocation]
|
||||
if allocation.type_request_unit in ['day', 'half_day']:
|
||||
quantity_available = employee_quantity_available['days']
|
||||
remaining_days_allocation = (allocation.number_of_days - days_consumed['virtual_leaves_taken'])
|
||||
else:
|
||||
quantity_available = employee_quantity_available['hours']
|
||||
remaining_days_allocation = (allocation.number_of_hours_display - days_consumed['virtual_leaves_taken'])
|
||||
#TODO leave allocation allowing negative not yet handled here
|
||||
if quantity_available <= remaining_days_allocation:
|
||||
search_date = interval_to.date() + timedelta(days=1)
|
||||
days_consumed['max_leaves'] = allocation.number_of_days if allocation.type_request_unit in ['day', 'half_day'] else allocation.number_of_hours_display
|
||||
if not reached_remaining_days_limit:
|
||||
days_consumed['virtual_remaining_leaves'] += min(quantity_available, remaining_days_allocation)
|
||||
days_consumed['remaining_leaves'] = days_consumed['max_leaves'] - days_consumed['leaves_taken']
|
||||
if remaining_days_allocation >= quantity_available:
|
||||
reached_remaining_days_limit = True
|
||||
# Check valid allocations with still availabe leaves on it
|
||||
if days_consumed['virtual_remaining_leaves'] > 0 and allocation.date_to and allocation.date_to > date:
|
||||
allocations_with_remaining_leaves |= allocation
|
||||
allocations_sorted = sorted(allocations_with_remaining_leaves, key=lambda a: a.date_to)
|
||||
allocations_days_consumed[employee_id][holiday_status_id][False]['closest_allocation_to_expire'] = allocations_sorted[0] if allocations_sorted else False
|
||||
return allocations_days_consumed
|
||||
|
||||
18
hr_negative_leave/views/hr_leave_type_views.xml
Normal file
18
hr_negative_leave/views/hr_leave_type_views.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_leave_type_negative_leave" model="ir.ui.view">
|
||||
<field name="name">hr.leave.type.negative.leave</field>
|
||||
<field name="model">hr.leave.type</field>
|
||||
<field name="inherit_id" ref="hr_holidays.edit_holiday_status_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='allocation_validation']" position="after">
|
||||
<group name="negative_leave" id="negative_leave" colspan="4"
|
||||
string="Allow negative"
|
||||
attrs="{'invisible':[('requires_allocation', '=', 'no')]}"
|
||||
>
|
||||
<field name="allows_negative" />
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
22
hr_negative_leave/views/hr_leave_views.xml
Normal file
22
hr_negative_leave/views/hr_leave_views.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<odoo>
|
||||
<record id="hr_leave_view_negative_leave" model="ir.ui.view">
|
||||
<field name="name">hr.leave.view.negative.leave</field>
|
||||
<field name="model">hr.leave</field>
|
||||
<field name="inherit_id" ref="hr_holidays.hr_leave_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='holiday_status_id']" position="attributes">
|
||||
<attribute name="domain">[
|
||||
'|',
|
||||
('requires_allocation', '=', 'no'),
|
||||
'&',
|
||||
('has_valid_allocation', '=', True),
|
||||
'|',
|
||||
('allows_negative', '=', True),
|
||||
'&',
|
||||
('virtual_remaining_leaves', '>', 0),
|
||||
('allows_negative', '=', False),
|
||||
]</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -29,7 +29,7 @@ None yet.
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `our issues website <https://github.com/elabore-coop/allow_negative_leave_and_allocation/issues>`_. In case of
|
||||
Bugs are tracked on `our issues website <https://git.elabore.coop/Elabore/hr-tools/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.
|
||||
|
||||
Reference in New Issue
Block a user