2 Commits

Author SHA1 Message Date
bc64030847 [IMP] hr_holidays_timeoff_analysis : migrate from version 16 to 18
All checks were successful
pre-commit / pre-commit (pull_request) Successful in 1m26s
2025-10-10 12:18:40 +02:00
517a59b59d [IMP] hr_holidays_timeoff_analysis : apply changes proposed by pre-commit 2025-10-10 11:34:54 +02:00
11 changed files with 344 additions and 140 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.ruff_cache/
*.*~ *.*~
*.pyc *.pyc

View File

@@ -49,12 +49,9 @@ repos:
$(git rev-parse --show-toplevel))"' $(git rev-parse --show-toplevel))"'
- id: oca-gen-addon-readme - id: oca-gen-addon-readme
entry: entry:
bash -c 'oca-gen-addon-readme bash -c 'oca-gen-addon-readme --addons-dir=. --branch=$(git symbolic-ref
--addons-dir=.
--branch=$(git symbolic-ref
refs/remotes/origin/HEAD | sed "s@^refs/remotes/origin/@@") refs/remotes/origin/HEAD | sed "s@^refs/remotes/origin/@@")
--repo-name=$(basename $(git rev-parse --show-toplevel)) --repo-name=$(basename $(git rev-parse --show-toplevel)) --org-name="Elabore"
--org-name="Elabore"
--if-source-changed --keep-source-digest' --if-source-changed --keep-source-digest'
- repo: https://github.com/OCA/odoo-pre-commit-hooks - repo: https://github.com/OCA/odoo-pre-commit-hooks

View File

@@ -1,4 +1 @@
from . import models from . import models
from . import tests

View File

@@ -5,14 +5,16 @@
"name": "hr_holidays_timeoff_analysis", "name": "hr_holidays_timeoff_analysis",
"version": "18.0.1.0.0", "version": "18.0.1.0.0",
"author": "Elabore", "author": "Elabore",
"website": "https://elabore.coop", "website": "https://git.elabore.coop/elabore/hr-tools",
"maintainer": "Elabore", "maintainer": "Elabore",
"license": "AGPL-3", "license": "AGPL-3",
"category": "HR", "category": "HR",
"summary": "indicate day by day if a date is timeoff or not and generate an analyses pivot by employee", "summary": "indicate day by day if a date is timeoff or not and "
"generate an analyses pivot by employee",
# any module necessary for this one to work correctly # any module necessary for this one to work correctly
"depends": [ "depends": [
"base","hr_holidays", "base",
"hr_holidays",
], ],
"qweb": [], "qweb": [],
"external_dependencies": { "external_dependencies": {

View File

@@ -1,5 +1,4 @@
<odoo> <odoo noupdate="1">
<data noupdate="1">
<record id="ir_cron_create_timeoff_day" model="ir.cron"> <record id="ir_cron_create_timeoff_day" model="ir.cron">
<field name="name">Create and update Timeoff Days</field> <field name="name">Create and update Timeoff Days</field>
<field name="model_id" ref="model_hr_leave_timeoff_day" /> <field name="model_id" ref="model_hr_leave_timeoff_day" />
@@ -7,8 +6,6 @@
<field name="code">model.cron_manage_timeoff_days()</field> <field name="code">model.cron_manage_timeoff_days()</field>
<field name="interval_number">1</field> <field name="interval_number">1</field>
<field name="interval_type">days</field> <field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active">True</field> <field name="active">True</field>
</record> </record>
</data>
</odoo> </odoo>

View File

@@ -1,20 +1,25 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api
from datetime import timedelta from datetime import timedelta
from odoo import api, fields, models
class TimeOffDay(models.Model): class TimeOffDay(models.Model):
_name = 'hr.leave.timeoff.day' _name = "hr.leave.timeoff.day"
_description = 'Timeoff Day' _description = "Timeoff Day"
_order = 'date desc' _order = "date desc"
date = fields.Date() date = fields.Date()
employee_id = fields.Many2one('hr.employee') employee_id = fields.Many2one("hr.employee")
hr_leave_id = fields.Many2one('hr.leave') hr_leave_id = fields.Many2one("hr.leave")
hr_leave_type = fields.Many2one(related='hr_leave_id.holiday_status_id', store=True) hr_leave_type = fields.Many2one(related="hr_leave_id.holiday_status_id", store=True)
leave_duration_by_day = fields.Float() leave_duration_by_day = fields.Float()
def employee_is_scheduled_to_work_this_day(self, date, employee): def employee_is_scheduled_to_work_this_day(self, date, employee):
""" Check if the employee is scheduled to work on this day according to his calendar """ """
Check if the employee is scheduled to work on this day according to his
calendar.
"""
calendar = employee.resource_calendar_id calendar = employee.resource_calendar_id
if not calendar or not calendar.attendance_ids: if not calendar or not calendar.attendance_ids:
return False return False
@@ -25,19 +30,26 @@ class TimeOffDay(models.Model):
return bool(attendances) return bool(attendances)
def is_a_public_holiday(self, date, employee): def is_a_public_holiday(self, date, employee):
""" Check if the day is a public holiday """ """
# public holidays start the day before in database (ex: date_from : 7mai 22:00 and date_to : 8mai 23:59 for 8mai public holiday) ) Check if the day is a public holiday.
public_holidays = self.env['resource.calendar.leaves'].search([ """
('date_from', '<=', date-timedelta(days=1)), # public holidays start the day before in database
('date_to', '>=', date), # (ex: date_from : 7mai 22:00 and date_to : 8mai 23:59 for 8mai public holiday))
('resource_id', '=', False) # resource_id is null for public holiday public_holidays = self.env["resource.calendar.leaves"].search(
]) [
("date_from", "<=", date - timedelta(days=1)),
("date_to", ">=", date),
("resource_id", "=", False), # resource_id is null for public holiday
]
)
if public_holidays: if public_holidays:
return True return True
return False return False
def compute_leave_duration_by_day(self, leave): def compute_leave_duration_by_day(self, leave):
""" Compute the leave duration by day based on the leave type """ """
Compute the leave duration by day based on the leave type.
"""
leave_duration_by_day = 0.0 leave_duration_by_day = 0.0
# Full day case # Full day case
if leave.request_unit_half: if leave.request_unit_half:
@@ -55,46 +67,64 @@ class TimeOffDay(models.Model):
def cron_create_timeoff_days(self): def cron_create_timeoff_days(self):
# Browse all validated leaves # Browse all validated leaves
leaves = self.env['hr.leave'].search([ leaves = self.env["hr.leave"].search(
('state', '=', 'validate'), [
('request_date_from', '!=', False), ("state", "=", "validate"),
('request_date_to', '!=', False), ("request_date_from", "!=", False),
('employee_id', '!=', False), ("request_date_to", "!=", False),
]) ("employee_id", "!=", False),
]
)
for leave in leaves: for leave in leaves:
current_date = leave.request_date_from current_date = leave.request_date_from
employee = leave.employee_id employee = leave.employee_id
while current_date <= leave.request_date_to: while current_date <= leave.request_date_to:
if self.employee_is_scheduled_to_work_this_day(current_date, employee) and not self.is_a_public_holiday(current_date, employee): if self.employee_is_scheduled_to_work_this_day(
# The employee is scheluded to work this day according his calendar and it's not a public holiday, current_date, employee
) and not self.is_a_public_holiday(current_date, employee):
# The employee is scheluded to work this day according his calendar
# and it's not a public holiday,
# so create a timeoff day record if it does not already exist # so create a timeoff day record if it does not already exist
if not self.search([ if not self.search(
('date', '=', current_date), [
('employee_id', '=', employee.id), ("date", "=", current_date),
('hr_leave_id', '=', leave.id), ("employee_id", "=", employee.id),
], limit=1): ("hr_leave_id", "=", leave.id),
self.create({ ],
'date': current_date, limit=1,
'employee_id': employee.id, ):
'hr_leave_id': leave.id, self.create(
'leave_duration_by_day': self.compute_leave_duration_by_day(leave), {
}) "date": current_date,
"employee_id": employee.id,
"hr_leave_id": leave.id,
"leave_duration_by_day": self.compute_leave_duration_by_day( # noqa: E501
leave
),
}
)
current_date += timedelta(days=1) current_date += timedelta(days=1)
def cron_delete_timeoff_days(self): def cron_delete_timeoff_days(self):
# Browse all unvalidated leaves # Browse all unvalidated leaves
leaves = self.env['hr.leave'].search([ leaves = self.env["hr.leave"].search(
('state', '!=', 'validate'), [
('request_date_from', '!=', False), ("state", "!=", "validate"),
('request_date_to', '!=', False), ("request_date_from", "!=", False),
('employee_id', '!=', False), ("request_date_to", "!=", False),
]) ("employee_id", "!=", False),
]
)
# Delete timeoff days for leaves that are no longer validated # Delete timeoff days for leaves that are no longer validated
for leave in leaves: for leave in leaves:
self.search([ self.search(
('hr_leave_id', '=', leave.id), [
]).unlink() ("hr_leave_id", "=", leave.id),
]
).unlink()
# Delete timeoff days that are not linked to any leave # Delete timeoff days that are not linked to any leave
self.search([ self.search(
('hr_leave_id', '=', False), [
]).unlink() ("hr_leave_id", "=", False),
]
).unlink()

View File

@@ -1,3 +1 @@
# -*- coding: utf-8 -*-
from . import test_hr_leave_timeoff_day from . import test_hr_leave_timeoff_day

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from odoo.fields import Date from odoo.fields import Date
from odoo.tests import tagged from odoo.tests import tagged
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
@@ -96,21 +95,123 @@ class TestHrLeaveTimeoffDay(TransactionCase):
) )
def test_leave_duration_by_day_hour_compare_to_employee_calendar(self): def test_leave_duration_by_day_hour_compare_to_employee_calendar(self):
employee_calendar = self.env['resource.calendar'].create({ employee_calendar = self.env["resource.calendar"].create(
'name': 'Employee Calendar', {
'attendance_ids': [ "name": "Employee Calendar",
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), "attendance_ids": [
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), (
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), 0,
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), 0,
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), {
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), "name": "Monday Morning",
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), "dayofweek": "0",
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), "hour_from": 8,
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), "hour_to": 12,
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), "day_period": "morning",
},
),
(
0,
0,
{
"name": "Monday Afternoon",
"dayofweek": "0",
"hour_from": 13,
"hour_to": 17,
"day_period": "afternoon",
},
),
(
0,
0,
{
"name": "Tuesday Morning",
"dayofweek": "1",
"hour_from": 8,
"hour_to": 12,
"day_period": "morning",
},
),
(
0,
0,
{
"name": "Tuesday Afternoon",
"dayofweek": "1",
"hour_from": 13,
"hour_to": 17,
"day_period": "afternoon",
},
),
(
0,
0,
{
"name": "Wednesday Morning",
"dayofweek": "2",
"hour_from": 8,
"hour_to": 12,
"day_period": "morning",
},
),
(
0,
0,
{
"name": "Wednesday Afternoon",
"dayofweek": "2",
"hour_from": 13,
"hour_to": 17,
"day_period": "afternoon",
},
),
(
0,
0,
{
"name": "Thursday Morning",
"dayofweek": "3",
"hour_from": 8,
"hour_to": 12,
"day_period": "morning",
},
),
(
0,
0,
{
"name": "Thursday Afternoon",
"dayofweek": "3",
"hour_from": 13,
"hour_to": 17,
"day_period": "afternoon",
},
),
(
0,
0,
{
"name": "Friday Morning",
"dayofweek": "4",
"hour_from": 8,
"hour_to": 12,
"day_period": "morning",
},
),
(
0,
0,
{
"name": "Friday Afternoon",
"dayofweek": "4",
"hour_from": 13,
"hour_to": 17,
"day_period": "afternoon",
},
),
], ],
}) }
)
self.employee.resource_calendar_id = employee_calendar self.employee.resource_calendar_id = employee_calendar
leave = self.env["hr.leave"].create( leave = self.env["hr.leave"].create(
@@ -129,8 +230,6 @@ class TestHrLeaveTimeoffDay(TransactionCase):
leave._compute_number_of_days_display() leave._compute_number_of_days_display()
leave.state = "validate" # Simulate the leave being validated leave.state = "validate" # Simulate the leave being validated
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days() self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
timeoff_days = self.env["hr.leave.timeoff.day"].search( timeoff_days = self.env["hr.leave.timeoff.day"].search(
[ [
@@ -150,19 +249,101 @@ class TestHrLeaveTimeoffDay(TransactionCase):
) )
def test_leave_duration_by_day_compare_to_time_part_employee_calendar(self): def test_leave_duration_by_day_compare_to_time_part_employee_calendar(self):
employee_calendar = self.env['resource.calendar'].create({ employee_calendar = self.env["resource.calendar"].create(
'name': 'Employee Calendar', {
'attendance_ids': [ "name": "Employee Calendar",
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), "attendance_ids": [
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), (
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), 0,
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), 0,
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), {
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), "name": "Monday Morning",
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), "dayofweek": "0",
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), "hour_from": 8,
"hour_to": 12,
"day_period": "morning",
},
),
(
0,
0,
{
"name": "Monday Afternoon",
"dayofweek": "0",
"hour_from": 13,
"hour_to": 17,
"day_period": "afternoon",
},
),
(
0,
0,
{
"name": "Tuesday Morning",
"dayofweek": "1",
"hour_from": 8,
"hour_to": 12,
"day_period": "morning",
},
),
(
0,
0,
{
"name": "Tuesday Afternoon",
"dayofweek": "1",
"hour_from": 13,
"hour_to": 17,
"day_period": "afternoon",
},
),
(
0,
0,
{
"name": "Wednesday Morning",
"dayofweek": "2",
"hour_from": 8,
"hour_to": 12,
"day_period": "morning",
},
),
(
0,
0,
{
"name": "Wednesday Afternoon",
"dayofweek": "2",
"hour_from": 13,
"hour_to": 17,
"day_period": "afternoon",
},
),
(
0,
0,
{
"name": "Thursday Morning",
"dayofweek": "3",
"hour_from": 8,
"hour_to": 12,
"day_period": "morning",
},
),
(
0,
0,
{
"name": "Thursday Afternoon",
"dayofweek": "3",
"hour_from": 13,
"hour_to": 17,
"day_period": "afternoon",
},
),
], ],
}) }
)
self.employee.resource_calendar_id = employee_calendar self.employee.resource_calendar_id = employee_calendar
leave = self.env["hr.leave"].create( leave = self.env["hr.leave"].create(
@@ -194,19 +375,22 @@ class TestHrLeaveTimeoffDay(TransactionCase):
) )
def test_public_holidays_between_a_leave(self): def test_public_holidays_between_a_leave(self):
# Leaves the code below commented because default database already has a public holiday on 8th May 2025 # Leaves the code below commented because
# self.env["resource.calendar.leaves"].create( # the default database already has a public holiday on 8th May 2025
# { self.env["resource.calendar.leaves"].create(
# "name": "8 mai 2025", {
# "date_from": Date.to_date("2025-05-07 22:00:00"), "name": "8 mai 2025",
# "date_to": Date.to_date("2025-05-08 23:00:00"), "date_from": Date.to_date("2025-05-07 22:00:00"),
# } "date_to": Date.to_date("2025-05-08 23:00:00"),
# ) }
)
leave = self.env["hr.leave"].create( leave = self.env["hr.leave"].create(
{ {
"employee_id": self.employee.id, "employee_id": self.employee.id,
"request_date_from": Date.to_date("2025-05-05"), "request_date_from": Date.to_date("2025-05-05"),
"request_date_to": Date.to_date("2025-05-11"), #a public holiday is in between (8 may) "request_date_to": Date.to_date(
"2025-05-11"
), # a public holiday is in between (8 may)
"holiday_status_id": self.time_off_type.id, "holiday_status_id": self.time_off_type.id,
} }
) )
@@ -254,7 +438,7 @@ class TestHrLeaveTimeoffDay(TransactionCase):
len(timeoff_days), 5, "There should be 5 timeoff days for this leave" len(timeoff_days), 5, "There should be 5 timeoff days for this leave"
) )
leave.state = "draft" leave.state = "confirm"
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days() self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
# Vérifie qu'il n'existe plus de hr.leave.timeoff.day pour ce leave # Vérifie qu'il n'existe plus de hr.leave.timeoff.day pour ce leave
@@ -291,7 +475,7 @@ class TestHrLeaveTimeoffDay(TransactionCase):
len(timeoff_days), 5, "There should be 5 timeoff days for this leave" len(timeoff_days), 5, "There should be 5 timeoff days for this leave"
) )
leave.state = "draft" leave.state = "confirm"
leave.unlink() leave.unlink()
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days() self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
@@ -305,4 +489,3 @@ class TestHrLeaveTimeoffDay(TransactionCase):
self.assertEqual( self.assertEqual(
len(timeoff_days), 0, "There should be no timeoff days for this leave" len(timeoff_days), 0, "There should be no timeoff days for this leave"
) )

View File

@@ -3,13 +3,13 @@
<record id="hr_leave_timeoff_day_view_list" model="ir.ui.view"> <record id="hr_leave_timeoff_day_view_list" model="ir.ui.view">
<field name="model">hr.leave.timeoff.day</field> <field name="model">hr.leave.timeoff.day</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree editable="bottom"> <list editable="bottom">
<field name="date" /> <field name="date" />
<field name="employee_id" groups="base.group_user" /> <field name="employee_id" groups="base.group_user" />
<field name="hr_leave_id" /> <field name="hr_leave_id" />
<field name="hr_leave_type" /> <field name="hr_leave_type" />
<field name="leave_duration_by_day" /> <field name="leave_duration_by_day" />
</tree> </list>
</field> </field>
</record> </record>
@@ -32,8 +32,7 @@
<field name="date" /> <field name="date" />
<field name="employee_id" groups="base.group_user" /> <field name="employee_id" groups="base.group_user" />
<separator /> <separator />
<filter name="filter_date" date="date" default_period="this_year" <filter name="filter_date" date="date" string="Period" />
string="Period" />
</search> </search>
</field> </field>
</record> </record>
@@ -41,9 +40,8 @@
<record id="hr_leave_timeoff_day_action" model="ir.actions.act_window"> <record id="hr_leave_timeoff_day_action" model="ir.actions.act_window">
<field name="name">Timeoff Days</field> <field name="name">Timeoff Days</field>
<field name="res_model">hr.leave.timeoff.day</field> <field name="res_model">hr.leave.timeoff.day</field>
<field name="view_mode">tree,form,pivot</field> <field name="view_mode">list,form,pivot</field>
<field name="search_view_id" ref="hr_leave_timeoff_day_view_search" /> <field name="search_view_id" ref="hr_leave_timeoff_day_view_search" />
<field name="context">{'search_default_filter_date': True}</field>
</record> </record>
<menuitem <menuitem
@@ -52,6 +50,7 @@
parent="hr_holidays.menu_hr_holidays_configuration" parent="hr_holidays.menu_hr_holidays_configuration"
action="hr_leave_timeoff_day_action" action="hr_leave_timeoff_day_action"
groups="hr_holidays.group_hr_holidays_manager" groups="hr_holidays.group_hr_holidays_manager"
sequence="6"/> sequence="6"
/>
</odoo> </odoo>