5 Commits

99 changed files with 933 additions and 3871 deletions

View File

@@ -3,7 +3,7 @@ name: pre-commit
on: on:
pull_request: pull_request:
branches: branches:
- "16.0*" - "18.0*"
jobs: jobs:
pre-commit: pre-commit:

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

@@ -10,7 +10,7 @@ manifest-required-authors=Elabore
manifest-required-keys=license manifest-required-keys=license
manifest-deprecated-keys=description,active manifest-deprecated-keys=description,active
license-allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3 license-allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
valid-odoo-versions=16.0 valid-odoo-versions=18.0
[MESSAGES CONTROL] [MESSAGES CONTROL]
disable=all disable=all

View File

@@ -9,7 +9,7 @@ manifest-required-authors=Elabore
manifest-required-keys=license manifest-required-keys=license
manifest-deprecated-keys=description,active manifest-deprecated-keys=description,active
license-allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3 license-allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
valid-odoo-versions=16.0 valid-odoo-versions=18.0
[MESSAGES CONTROL] [MESSAGES CONTROL]
disable=all disable=all

View File

@@ -1,55 +0,0 @@
=======================
hr_employee_stats_sheet
=======================
Summary
=======
Add global sheet for employee stats
Description
===========
This module add a global sheet stats in timesheet sheet
It compare planified working hours (from calendar ressouce) and worked hours form timesheets
Installation
============
Use Odoo normal module installation procedure to install
``hr_employee_stats_sheet``.
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
------------
* `Alusage : Nicolas JEUDY`
* `Elabore <mailto:contact@elabore.coop>`
Funders
-------
The development of this module has been financially supported by:
* Alusage (https://github.com/Alusage)
* Elabore (https://elabore.coop)
Maintainer
----------
This module is maintained by Elabore.

View File

@@ -1,29 +0,0 @@
{
"name": "hr_employee_stats_sheet",
"version": "16.0.3.2.0",
"description": "Add global sheet for employee stats",
"summary": "Add global sheet for employee stats",
"author": "Nicolas JEUDY",
"website": "https://github.com/Alusage/odoo-hr-addons",
"license": "LGPL-3",
"category": "Human Resources",
"depends": [
"hr_negative_leave",
"base",
"hr",
"hr_holidays",
"hr_timesheet",
"hr_timesheet_sheet",
"resource",
"hr_employee_calendar_planning",
"hr_timesheet_sheet_usability_misc",
],
"data": [
"security/ir.model.access.csv",
"views/hr_employee_stats.xml",
"views/hr_timesheet_sheet.xml",
"views/res_config_settings_views.xml",
],
"installable": True,
"application": False,
}

View File

@@ -1,446 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * hr_employee_stats_sheet
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-19 14:31+0000\n"
"PO-Revision-Date: 2025-06-19 14:31+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_employee_stats_sheet
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet.res_config_settings_view_form
msgid ""
"<span class=\"fa fa-lg fa-building\" title=\"Values set here are company-"
"specific.\" aria-label=\"Values set here are company-specific.\" "
"groups=\"base.group_multi_company\" role=\"img\"/>"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__message_needaction
msgid "Action Needed"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__activity_ids
msgid "Activities"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__activity_exception_decoration
msgid "Activity Exception Decoration"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__activity_state
msgid "Activity State"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__activity_type_icon
msgid "Activity Type Icon"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__message_attachment_count
msgid "Attachment Count"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_res_company__auto_validate_recovery_allocation
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_res_config_settings__auto_validate_recovery_allocation
msgid "Auto validate recovery allocation"
msgstr "Auto-validation des allocations de récupération"
#. module: hr_employee_stats_sheet
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet.res_config_settings_view_form
msgid "Choose the coef to apply to recovery hours."
msgstr "Indiquer le coefficient multiplicateur à appliquer sur les heures supplémentaires (en %) pour générer les heures de récupération. Par défaut : 25,00%"
#. module: hr_employee_stats_sheet
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet.res_config_settings_view_form
msgid "Choose the recovery type."
msgstr ""
"Sélectionner le type de congé utilisé pour générer des allocations de "
"récupération. Le type de congé doit accepter les allocations négatives et être pris à l'heure."
#. module: hr_employee_stats_sheet
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet.res_config_settings_view_form
msgid "Choose to auto-validate the recovery allocation or not"
msgstr "Validation automatique des allocations de récupération"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_res_company__coef
msgid "Coef"
msgstr "Coefficient"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_res_config_settings__coef
msgid "Coef (in %)"
msgstr "Coefficient (en %)"
#. module: hr_employee_stats_sheet
#: model:ir.model,name:hr_employee_stats_sheet.model_res_company
msgid "Companies"
msgstr "Sociétés"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__company_id
msgid "Company"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model,name:hr_employee_stats_sheet.model_res_config_settings
msgid "Config Settings"
msgstr "Paramètres de configuration"
#. module: hr_employee_stats_sheet
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet.view_hr_timesheet_sheet_form_inherit
msgid "Create recovery allocation"
msgstr "Générer une récupération"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__create_uid
msgid "Created by"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__create_date
msgid "Created on"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__date
msgid "Date"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__dayofweek
msgid "Day of Week"
msgstr "Jour de la semaine"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__department_id
msgid "Department"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__display_name
msgid "Display Name"
msgstr ""
#. module: hr_employee_stats_sheet
#. odoo-python
#: code:addons/hr_employee_stats_sheet/models/hr_timesheet_sheet.py:0
#, python-format
msgid ""
"Employe not defined for the timesheet sheet or recovery type not defined in "
"settings"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__employee_id
msgid "Employee"
msgstr "Employé·e"
#. module: hr_employee_stats_sheet
#: model:ir.model,name:hr_employee_stats_sheet.model_hr_employee_stats
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_timesheet_sheet__employee_stats_ids
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet.view_hr_timesheet_sheet_form_inherit
msgid "Employee Stats"
msgstr "Statistique des employé·e·s"
#. module: hr_employee_stats_sheet
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet.hr_timesheet_sheet_stats_overview_pivot_view
msgid "Employee time stats pivot"
msgstr "Statistique de l'employé·e"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__failed_message_ids
msgid "Failed Messages"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__message_follower_ids
msgid "Followers"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__message_partner_ids
msgid "Followers (Partners)"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,help:hr_employee_stats_sheet.field_hr_employee_stats__activity_type_icon
msgid "Font awesome icon e.g. fa-tasks"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__gap_hours
msgid "Gap Hours"
msgstr "Heures d'écart"
#. module: hr_employee_stats_sheet
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet.view_hr_timesheet_sheet_form_inherit
msgid "Generated recovery allocations"
msgstr "Allocation(s) de récupération générée(s)"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__has_message
msgid "Has Message"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__id
msgid "ID"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__activity_exception_icon
msgid "Icon"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,help:hr_employee_stats_sheet.field_hr_employee_stats__activity_exception_icon
msgid "Icon to indicate an exception activity."
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,help:hr_employee_stats_sheet.field_hr_employee_stats__message_needaction
msgid "If checked, new messages require your attention."
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,help:hr_employee_stats_sheet.field_hr_employee_stats__message_has_error
msgid "If checked, some messages have a delivery error."
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__message_is_follower
msgid "Is Follower"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats____last_update
msgid "Last Modified on"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__write_uid
msgid "Last Updated by"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__write_date
msgid "Last Updated on"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_res_company__recovery_type_id
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_res_config_settings__recovery_type_id
msgid "Leave recovery type"
msgstr "Type de congé pour les récupérations"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__message_main_attachment_id
msgid "Main Attachment"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__message_has_error
msgid "Message Delivery error"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__message_ids
msgid "Messages"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__my_activity_date_deadline
msgid "My Activity Deadline"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__name
msgid "Name"
msgstr "Nom"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__activity_calendar_event_id
msgid "Next Activity Calendar Event"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__activity_date_deadline
msgid "Next Activity Deadline"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__activity_summary
msgid "Next Activity Summary"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__activity_type_id
msgid "Next Activity Type"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__message_needaction_counter
msgid "Number of Actions"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__message_has_error_counter
msgid "Number of errors"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,help:hr_employee_stats_sheet.field_hr_employee_stats__message_needaction_counter
msgid "Number of messages requiring action"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,help:hr_employee_stats_sheet.field_hr_employee_stats__message_has_error_counter
msgid "Number of messages with delivery error"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__is_public_holiday
msgid "Public Holiday"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_timesheet_sheet__recovery_allocation_ids
msgid "Recovery Allocations"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__activity_user_id
msgid "Responsible User"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,help:hr_employee_stats_sheet.field_hr_employee_stats__activity_state
msgid ""
"Status based on activities\n"
"Overdue: Due date is already passed\n"
"Today: Activity date is today\n"
"Planned: Future activities."
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,help:hr_employee_stats_sheet.field_res_config_settings__coef
msgid ""
"The coef is applied to recovery hoursExample : an employe make 1h overtime, "
"if coef is set to 25%, the recovery hours allocated will be 1,25h"
msgstr ""
#. module: hr_employee_stats_sheet
#. odoo-python
#: code:addons/hr_employee_stats_sheet/models/hr_timesheet_sheet.py:0
#, python-format
msgid ""
"The number of recovery hours exceeds the authorized limit (%s h) by %s hours"
msgstr ""
#. module: hr_employee_stats_sheet
#. odoo-python
#: code:addons/hr_employee_stats_sheet/models/hr_timesheet_sheet.py:0
#, python-format
msgid ""
"There is a contract starting during the timesheet sheet time period for the "
"employee %sPlease create a new timesheet sheet starting from the new "
"contract start date"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model,name:hr_employee_stats_sheet.model_hr_leave_allocation
msgid "Time Off Allocation"
msgstr "Allocation de congés"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__sheet_id
msgid "Timesheet"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model,name:hr_employee_stats_sheet.model_hr_timesheet_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_leave_allocation__timesheet_sheet_id
msgid "Timesheet Sheet"
msgstr "Feuille de temps"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__timesheet_line_ids
msgid "Timesheet lines"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_timesheet_sheet__timesheet_sheet_gap_hours
msgid "Timesheet sheet gap hours"
msgstr "Heures d'écart sur la période de la feuille de temps"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_timesheet_sheet__timesheet_sheet_recovery_hours
msgid "Timesheet sheet recovery hours"
msgstr "Heures de récupération sur la période de la feuille de temps"
#. module: hr_employee_stats_sheet
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet.view_hr_employee_stats_tree
msgid "Total"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__total_hours
msgid "Total Hours"
msgstr "Heures comptabilisées"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__total_leave_hours
msgid "Total Leave Hours"
msgstr "Heures de congés"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__total_planned_hours
msgid "Total Planning Hours"
msgstr "Heures planifiées"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__total_recovery_hours
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_timesheet_sheet__total_recovery_hours
msgid "Total Recovery Hours"
msgstr "Heures de récupération"
#. module: hr_employee_stats_sheet
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet.view_hr_timesheet_sheet_form_inherit
msgid "Total gap hours in this timesheet sheet time range"
msgstr "Total heures d'écart sur cette période de temps"
#. module: hr_employee_stats_sheet
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet.view_hr_timesheet_sheet_form_inherit
msgid "Total recovery hours in this timesheet sheet time range"
msgstr ""
"Total heures de repos compensateur accumulé sur cette période de temps"
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,help:hr_employee_stats_sheet.field_hr_employee_stats__activity_exception_decoration
msgid "Type of the exception activity on record."
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,field_description:hr_employee_stats_sheet.field_hr_employee_stats__website_message_ids
msgid "Website Messages"
msgstr ""
#. module: hr_employee_stats_sheet
#: model:ir.model.fields,help:hr_employee_stats_sheet.field_hr_employee_stats__website_message_ids
msgid "Website communication history"
msgstr ""

View File

@@ -1,6 +0,0 @@
from . import hr_employee_stats
from . import hr_timesheet_sheet
from . import res_config
from . import res_company
from . import hr_leave_allocation
from . import hr_employee

View File

@@ -1,38 +0,0 @@
import pytz
from odoo import models, _
from datetime import timedelta
from pytz import utc
from odoo.exceptions import UserError
class HrEmployee(models.Model):
_inherit = "hr.employee"
def _get_calendar_in_progress_during_a_time_period(self, date_start, date_end):
"""
get the ressource calendar which was used during the timesheet sheet time period
"""
self.ensure_one()
# find calendar(s) running over the duration of the timesheet
calendars = self.env["hr.employee.calendar"].search(
[
("employee_id", "=", self.id),
("date_start", "<=", date_end),
"|",
("date_end", "=", False), # pas de date de fin OU
("date_end", ">=", date_start), # date de fin après le début
],
)
if len(calendars) > 1:
raise UserError(
_("There is a calendar starting during the timesheet sheet time period for the employee %s "
"Please create a new timesheet sheet starting from the new calendar start date")
% self.display_name
)
# if hr_employee_calendar found, use its calendar_id
elif calendars and calendars[0].calendar_id:
return calendars[0].calendar_id
# if resource calendar not found, use the ressource calendar of the company linked to the employee
elif self.company_id.resource_calendar_id:
return self.company_id.resource_calendar_id
return None

View File

@@ -1,239 +0,0 @@
import logging
import pytz
from odoo import api, fields, models, _
from datetime import timedelta
from pytz import utc
from odoo.exceptions import UserError
class HrEmployeeStats(models.Model):
_name = "hr.employee.stats"
_description = "Employee Stats"
_order = "date desc"
_inherit = ["mail.thread", "mail.activity.mixin"]
name = fields.Char("Name", compute="_compute_name", store=True)
dayofweek = fields.Integer("Day of Week", compute="_compute_dayofweek")
is_public_holiday = fields.Boolean("Public Holiday", compute="_compute_dayofweek")
employee_id = fields.Many2one("hr.employee", "Employee", required=True)
department_id = fields.Many2one("hr.department", "Department")
timesheet_line_ids = fields.Many2many(
"account.analytic.line",
compute="_compute_timesheet_line_ids",
string="Timesheet lines",
)
date = fields.Date("Date", required=True)
company_id = fields.Many2one(
"res.company",
"Company",
default=lambda self: self.env.company,
required=True,
)
sheet_id = fields.Many2one("hr_timesheet.sheet", "Timesheet")
total_hours = fields.Float("Total Hours", compute="_compute_hours")
total_planned_hours = fields.Float("Total Planning Hours", compute="_compute_hours")
total_leave_hours = fields.Float("Total Leave Hours", compute="_compute_hours")
total_recovery_hours = fields.Float(
"Total Recovery Hours", compute="_compute_hours"
)
gap_hours = fields.Float("Gap Hours", compute="_compute_hours")
def _get_holiday_status_id(self):
recovery_type_id = self.env.company.recovery_type_id
if recovery_type_id:
return recovery_type_id.id
else:
return False
def _compute_timesheet_line_ids(self):
for stat in self:
stat.timesheet_line_ids = self.env["account.analytic.line"].search(
[
("employee_id", "=", stat.employee_id.id),
("date", "=", stat.date),
]
)
def _get_intersects(
self, datetime1_start, datetime1_end, datetime2_start, datetime2_end
):
latest_start = max(datetime1_start, datetime2_start)
earliest_end = min(datetime1_end, datetime2_end)
delta = (earliest_end - latest_start).total_seconds() / 3600
return max(0, delta)
def get_total_hours_domain(self):
return [
("employee_id", "=", self.employee_id.id),
("date", "=", self.date),
]
@api.depends("timesheet_line_ids")
def _get_total_hours(self):
self.ensure_one()
total_hours = 0
timesheet_line = self.env["account.analytic.line"]
if self.date and self.employee_id:
timesheet_line_ids = timesheet_line.search(
self.get_total_hours_domain()
)
total_hours = sum(timesheet_line_ids.mapped("unit_amount"))
return total_hours
def _get_total_planned_hours(self):
self.ensure_one()
total_planned_hours = 0
if self.employee_id and self.date and not self.is_public_holiday:
dayofweek = int(self.date.strftime("%u")) - 1
calendar_id = self.employee_id._get_calendar_in_progress_during_a_time_period(self.date,self.date)
week_number = self.date.isocalendar()[1] % 2
if calendar_id.two_weeks_calendar:
hours = calendar_id.attendance_ids.search(
[
("dayofweek", "=", dayofweek),
("calendar_id", "=", calendar_id.id),
("week_type", "=", week_number),
]
)
else:
hours = calendar_id.attendance_ids.search(
[
("dayofweek", "=", dayofweek),
("calendar_id", "=", calendar_id.id),
]
)
total_planned_hours = sum(
hours.mapped(lambda r: r.hour_to - r.hour_from)
)
return total_planned_hours
def _get_total_recovery_hours(self):
self.ensure_one()
recovery = self.env["hr.leave"]
total_recovery_hours = 0
if self.date and self.employee_id and self._get_holiday_status_id():
recovery_ids = recovery.search(
[
("employee_id", "=", self.employee_id.id),
("holiday_status_id", "=", self._get_holiday_status_id()),
("request_date_from", "<=", self.date),
("request_date_to", ">=", self.date),
],
)
if recovery_ids:
for recovery_id in recovery_ids:
if recovery_id.request_unit_hours:
recovery_hours = recovery_id.number_of_hours_display
total_recovery_hours += min(recovery_hours,self._get_total_planned_hours())
elif recovery_id.request_unit_half:
total_recovery_hours += self._get_total_planned_hours() / 2
else :
total_recovery_hours += self._get_total_planned_hours()
return total_recovery_hours
def _get_total_leave_hours(self):
self.ensure_one()
leave = self.env["hr.leave"]
total_leave_hours = 0
if self.date and self.employee_id:
leave_ids = leave.search(
[
("employee_id", "=", self.employee_id.id),
("holiday_status_id", "!=", self._get_holiday_status_id()),
("request_date_from", "<=", self.date),
("request_date_to", ">=", self.date),
("state", '=', 'validate'),
("active", '=', True),
],
)
if leave_ids:
for leave_id in leave_ids:
if leave_id.request_unit_hours:
leave_hours = leave_id.number_of_hours_display
total_leave_hours += min(leave_hours,self._get_total_planned_hours())
elif leave_id.request_unit_half:
total_leave_hours += self._get_total_planned_hours() / 2
else :
total_leave_hours += self._get_total_planned_hours()
return total_leave_hours
@api.depends("employee_id", "date")
def _compute_name(self):
for stat in self:
stat.name = "%s - %s" % (stat.employee_id.name, stat.date)
@api.depends("date","employee_id")
def _compute_dayofweek(self):
for stat in self:
if not stat.date:
stat.dayofweek = None
stat.is_public_holiday = False
continue
stat.dayofweek = int(stat.date.strftime("%u")) - 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
if len(public_holidays) > 1:
raise UserError(
_("Several holidays have been found ont he date '%s'. Please correct the anomaly before continuing.") % self.date
)
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()
balance = (
total_hours
+ total_recovery_hours
+ total_leave_hours
- total_planned_hours
)
return balance
@api.depends(
"employee_id",
"date",
"total_hours",
"total_planned_hours",
"timesheet_line_ids",
)
def _compute_hours(self):
for stat in self:
total_hours = stat._get_total_hours()
total_planned_hours = stat._get_total_planned_hours()
total_recovery_hours = stat._get_total_recovery_hours()
total_leave_hours = stat._get_total_leave_hours()
stat.total_hours = total_hours
stat.total_planned_hours = total_planned_hours
stat.gap_hours = stat._get_gap_hours(total_hours, total_recovery_hours, total_leave_hours, total_planned_hours)
stat.total_recovery_hours = total_recovery_hours
stat.total_leave_hours = total_leave_hours

View File

@@ -1,9 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class HrLeaveAllocation(models.Model):
_inherit = "hr.leave.allocation"
timesheet_sheet_id = fields.Many2one("hr_timesheet.sheet", string="Timesheet Sheet", readonly=True)

View File

@@ -1,183 +0,0 @@
from datetime import timedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.addons.resource.models.resource import HOURS_PER_DAY
class HrTimesheetSheet(models.Model):
_inherit = "hr_timesheet.sheet"
employee_stats_ids = fields.One2many(
"hr.employee.stats", "sheet_id", "Employee Stats"
)
timesheet_sheet_gap_hours = fields.Float(
"Timesheet sheet gap hours", compute="_compute_hours"
)
timesheet_sheet_recovery_hours = fields.Float(
"Timesheet sheet recovery hours", compute="_compute_hours"
)
recovery_allocation_ids = fields.One2many(
'hr.leave.allocation',
'timesheet_sheet_id',
string='Recovery Allocations',
)
@api.depends("employee_stats_ids.gap_hours", "employee_id")
def _compute_hours(self):
for sheet in self:
sheet.timesheet_sheet_gap_hours = sheet._get_timesheet_sheet_gap_hours()
sheet.timesheet_sheet_recovery_hours = sheet._get_timesheet_sheet_recovery_hours()
def _get_timesheet_sheet_gap_hours(self):
self.ensure_one()
timesheet_sheet_gap_hours = sum(
self.employee_stats_ids.filtered(
lambda stat: stat.date <= fields.Date.today()
).mapped("gap_hours")
)
return timesheet_sheet_gap_hours
def _get_timesheet_sheet_recovery_hours(self):
self.ensure_one()
coef = self.env.company.coef
# apply coef only if hours to recovery are positive and coef is existing
if self.timesheet_sheet_gap_hours > 0 and coef > 0:
timesheet_sheet_recovery_hours = self.timesheet_sheet_gap_hours + (
self.timesheet_sheet_gap_hours * coef / 100
)
else:
timesheet_sheet_recovery_hours = self.timesheet_sheet_gap_hours
return timesheet_sheet_recovery_hours
def search_and_create_employee_stats(self):
for sheet in self:
if sheet.employee_id:
for day in range((sheet.date_end - sheet.date_start).days + 1):
date = sheet.date_start + timedelta(days=day)
stats = self.env["hr.employee.stats"].search(
[
("employee_id", "=", sheet.employee_id.id),
("date", "=", date),
]
)
if stats and not stats.sheet_id:
stats.write({"sheet_id": sheet.id})
if not stats:
self.env["hr.employee.stats"].create(
{
"employee_id": sheet.employee_id.id,
"date": date,
"sheet_id": sheet.id,
"company_id": sheet.company_id.id,
}
)
return True
@api.model
def create(self, vals):
res = super().create(vals)
res.search_and_create_employee_stats()
return res
def write(self, vals):
res = super().write(vals)
if "date_end" in vals or "date_start" in vals or "employee_id" in vals:
self.search_and_create_employee_stats()
return res
def unlink(self):
for sheet in self:
sheet.employee_stats_ids.unlink()
return super().unlink()
def action_timesheet_draft(self):
res = super().action_timesheet_draft()
for sheet in self:
# set the state of the recovery allocation to refuse if the timesheet sheet is set to draft
if sheet.state == "draft":
sheet.recovery_allocation_ids.write({"state": "refuse"})
return res
def _get_working_hours_per_day(self):
"""
Get the hours per day for the employee according to:
- the employee's work contract,
- or their resource calendar,
- or the company's resource calendar,
- or the default value of 8 hours per day.
:return: hours per day
"""
# get ressource calendar id used during the timesheet sheet time period
ressource_calendar_id = self.employee_id._get_calendar_in_progress_during_a_time_period(self.date_start,self.date_end)
if ressource_calendar_id:
return ressource_calendar_id.hours_per_day
return HOURS_PER_DAY
def action_generate_recovery_allocation(self):
# check if the user has the right to review the timesheet sheet
self.ensure_one()
self._check_can_review()
recovery_type_id = self.env.company.recovery_type_id
employee_id = self.employee_id
if not employee_id or not recovery_type_id:
raise UserError(
_("Employee not defined for the timesheet sheet or recovery type not defined in settings")
)
if recovery_type_id.request_unit != 'hour' or not recovery_type_id.allows_negative:
raise UserError(
_("The recovery type must be set to 'Hours' and allow negative leaves in the settings")
)
# check if allocation already exists for this timesheet sheet, if yes, refuse it
allocations = (
self.env["hr.leave.allocation"]
.sudo()
.search(
[
("timesheet_sheet_id.id", "=", self.id),
("state", "!=", "refuse"),
]
)
)
if allocations:
allocations.write({"state": "refuse"})
# get recovery hours from total gap hours of the timesheet sheet
recovery_hours = self._get_timesheet_sheet_recovery_hours()
# convert recovery hours into days
recovery_days = recovery_hours / self._get_working_hours_per_day()
# create an allocation (positive or negative) according to the total gap hours for the timesheet sheet range time
new_alloc = self.env["hr.leave.allocation"].create(
{
"private_name": "Allocation : %s - %s" % (recovery_type_id.name, employee_id.display_name),
"holiday_status_id": recovery_type_id.id,
"employee_id": employee_id.id,
"number_of_days": recovery_days,
"timesheet_sheet_id": self.id,
"allocation_type": 'accrual',
"date_from": self.date_start,
}
)
# set the allocation to validate or not
if self.env.company.auto_validate_recovery_allocation:
new_alloc.write({"state": "validate"})
return {
"type": "ir.actions.act_window",
"res_model": "hr.leave.allocation",
"res_id": new_alloc.id,
"view_mode": "form",
"target": "current",
"views": [
(
self.env.ref("hr_holidays.hr_leave_allocation_view_form").id,
"form",
)
],
}

View File

@@ -1,12 +0,0 @@
from odoo import fields, models
class ResCompany(models.Model):
_inherit = "res.company"
recovery_type_id = fields.Many2one(
"hr.leave.type", string="Leave recovery type"
)
coef = fields.Float("Coef", default=25)
auto_validate_recovery_allocation = fields.Boolean("Auto validate recovery allocation", default=True)

View File

@@ -1,29 +0,0 @@
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
recovery_type_id = fields.Many2one(
"hr.leave.type",
related="company_id.recovery_type_id",
string="Leave recovery type",
readonly=False,
domain="[('request_unit', '=', 'hour'), ('allows_negative', '=', True)]",
)
coef = fields.Float(
related="company_id.coef",
required=True,
string="Coef (in %)",
domain="[('company_id', '=', company_id)]",
readonly=False,
help="The coef is applied to recovery hours"
"Example : an employe make 1h overtime, if coef is set to 25%, the recovery hours allocated will be 1,25h",
)
auto_validate_recovery_allocation = fields.Boolean(
related="company_id.auto_validate_recovery_allocation",
readonly=False,
domain="[('company_id', '=', company_id)]",
)

View File

@@ -1,3 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_employee_stats_manager,hr.employee.stats,model_hr_employee_stats,hr_timesheet.group_timesheet_manager,1,1,1,1
access_hr_employee_stats_user,hr.employee.stats,model_hr_employee_stats,hr_timesheet.group_hr_timesheet_user,1,0,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_employee_stats_manager hr.employee.stats model_hr_employee_stats hr_timesheet.group_timesheet_manager 1 1 1 1
3 access_hr_employee_stats_user hr.employee.stats model_hr_employee_stats hr_timesheet.group_hr_timesheet_user 1 0 1 0

View File

@@ -1 +0,0 @@
from . import test_employee_stats

View File

@@ -1,404 +0,0 @@
from odoo.tests.common import TransactionCase
from odoo.tests import tagged
from datetime import date, timedelta, datetime
from odoo.exceptions import UserError
from odoo.fields import Date
@tagged("post_install", "-at_install")
class TestHrEmployeeStatsRecovery(TransactionCase):
def setUp(self):
super().setUp()
self.user = self.env['res.users'].create({
'name': 'Camille',
'login': 'camille',
})
self.recovery_type = self.env['hr.leave.type'].create({
'name': 'Recovery',
'request_unit': 'hour',
'allows_negative': True,
})
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',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, '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': 9, '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': 9, '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': 9, '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': 9, 'hour_to': 12, 'day_period': 'morning'}),
(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
analytic_plan = self.env['account.analytic.plan'].create({
'name': 'Test Plan',
})
self.analytic_account = self.env['account.analytic.account'].create({
'name': 'Test Analytic Account',
'plan_id': analytic_plan.id,
})
def _create_timesheet_sheet(self, start_date):
# 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,
'date_end': start_date + timedelta(days=6),
})
def _create_stats(self, start_date, nb_days, unit_amount):
# 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,
'date': start_date + timedelta(days=i),
'unit_amount': unit_amount,
'account_id': self.analytic_account.id,
'name': 'Work Entry',
})
# 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) # monday of last week
self.recovery_type.request_unit = 'day'
self.recovery_type.allows_negative = False
timesheet_sheet = self._create_timesheet_sheet(start_date)
self.env.company.recovery_type_id = self.recovery_type
with self.assertRaises(UserError):
timesheet_sheet.action_generate_recovery_allocation()
def test_no_recovery_hours(self):
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-01-01"),
'date_end': None,
'calendar_id': self.base_calendar.id,
})
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
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):
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-01-01"),
'date_end': None,
'calendar_id': self.base_calendar.id,
})
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
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%
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
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.8928571428571429, "The recovery allocation should be for 6.25h/7h = 0.8928571428571429 day")
def test_negative_recovery_hours(self):
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-01-01"),
'date_end': None,
'calendar_id': self.base_calendar.id,
})
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
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
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.7142857142857143, "The recovery allocation should be for -5/7 hours = 0,714285714")
def test_recovery_hours_part_time_employee(self):
part_time_calendar = self.env['resource.calendar'].create({
'name': 'Part Time Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, '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': 9, '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': 9, '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': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
],
})
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-01-01"),
'date_end': None,
'calendar_id': part_time_calendar.id,
})
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
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)
def test_recovery_hours_change_calendar(self):
employee_full_time_calendar = self.base_calendar # full time calendar (from monday to friday)
employee_part_time_calendar = self.env['resource.calendar'].create({
'name': 'Part Time Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, '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': 9, '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': 9, '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': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
],
})
#create two hr.employee.calendar to change calendar during the timesheet period
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2023-07-01"),
'date_end': Date.to_date("2025-07-31"),
'calendar_id': employee_full_time_calendar.id,
})
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-08-01"),
'date_end': None,
'calendar_id': employee_part_time_calendar.id,
})
self.employee.resource_calendar_id = employee_part_time_calendar.id
#create recovery hours on a period including the change of calendar
timesheet_sheet = self._create_timesheet_sheet(Date.to_date("2025-07-28")) #a week including the change of calendar on 1st august
#the create of recovery allocation should raise an error
with self.assertRaises(UserError):
timesheet_sheet.action_generate_recovery_allocation()
def test_recovery_hours_change_calendar_sucess(self):
employee_full_time_calendar = self.base_calendar # full time calendar (from monday to friday)
employee_part_time_calendar = self.env['resource.calendar'].create({
'name': 'Part Time Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, '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': 9, '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': 9, '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': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
],
})
#create two hr.employee.calendar to change calendar during the timesheet period
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2023-07-01"),
'date_end': Date.to_date("2025-07-31"),
'calendar_id': employee_full_time_calendar.id,
})
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-08-01"),
'date_end': None,
'calendar_id': employee_part_time_calendar.id,
})
self.employee.resource_calendar_id = employee_part_time_calendar.id
#create stats during period of full time calendar for the employee
timesheet_sheet = self.env['hr_timesheet.sheet'].create({
'employee_id': self.employee.id,
'date_start': "2025-07-07",
'date_end': "2025-07-13",
})
stats = self._create_stats(Date.to_date("2025-07-07"), 5, 7)
for stat in stats:
stat._compute_dayofweek()
stat._compute_hours()
timesheet_sheet.action_generate_recovery_allocation()
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",)
#create stats during period of part time calendar for the employee
stats = self._create_stats(Date.to_date("2025-09-08"), 5, 7)
for stat in stats:
stat._compute_dayofweek()
stat._compute_hours()
timesheet_sheet = self.env['hr_timesheet.sheet'].create({
'employee_id': self.employee.id,
'date_start': "2025-09-08",
'date_end': "2025-09-14",
})
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",)
def test_recovery_allocation_doesnt_change_with_of_calendar(self):
employee_full_time_calendar = self.base_calendar # full time calendar (from monday to friday)
employee_full_time_calendar.hours_per_day = 7
employee_part_time_calendar = self.env['resource.calendar'].create({
'name': 'Part Time Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 19, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 19, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 19, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 19, 'day_period': 'afternoon'}),
],
})
employee_part_time_calendar.hours_per_day = 9
#create two hr.employee.calendar to change calendar during the timesheet period
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2023-07-01"),
'date_end': Date.to_date("2025-07-31"),
'calendar_id': employee_full_time_calendar.id,
})
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-08-01"),
'date_end': None,
'calendar_id': employee_part_time_calendar.id,
})
self.employee.resource_calendar_id = employee_part_time_calendar.id
#create stats during period of full time calendar for the employee
timesheet_sheet_1 = self.env['hr_timesheet.sheet'].create({
'employee_id': self.employee.id,
'date_start': "2025-07-07",
'date_end': "2025-07-13",
})
stats = self._create_stats(Date.to_date("2025-07-07"), 5, 8)
for stat in stats:
stat._compute_dayofweek()
stat._compute_hours()
timesheet_sheet_1.action_generate_recovery_allocation()
self.assertEqual(timesheet_sheet_1.timesheet_sheet_gap_hours, 5, "timesheet_sheet_gap_hours should be 0",)
self.assertEqual(timesheet_sheet_1.timesheet_sheet_recovery_hours, 6.25, "timesheet_sheet_recovery_hours should be 6,25",)
allocation_1 = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet_1.id)])
self.assertEqual(len(allocation_1), 1, "There should be one recovery")
self.assertEqual(allocation_1.number_of_days,0.8928571428571429, "The recovery allocation should be for 0.8928571428571429 day")
#create stats during period of part time calendar for the employee
# generation 4 stats of 10h each, the employee is supposed to work 9h per day during 4 days
stats_2 = self._create_stats(Date.to_date("2025-09-08"), 4, 10)
for stat in stats_2:
stat._compute_dayofweek()
stat._compute_hours()
timesheet_sheet_2 = self.env['hr_timesheet.sheet'].create({
'employee_id': self.employee.id,
'date_start': "2025-09-08",
'date_end': "2025-09-14",
})
timesheet_sheet_2.action_generate_recovery_allocation()
self.assertEqual(timesheet_sheet_2.timesheet_sheet_gap_hours, 4, "timesheet_sheet_gap_hours should be 4",)
self.assertEqual(timesheet_sheet_2.timesheet_sheet_recovery_hours, 5, "timesheet_sheet_recovery_hours should be 5",)
allocation_2 = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet_2.id)])
self.assertEqual(len(allocation_2), 1, "There should be one recovery")
self.assertEqual(allocation_2.number_of_days,0.5555555555555556, "The recovery allocation should be for 0,555555556 (5/9) day")
#check that allocation_1 hasn't changed
self.assertEqual(allocation_1.number_of_days,0.8928571428571429, "The recovery allocation should be for 0,892857143 day")
def test_public_holiday(self):
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-01-01"),
'date_end': None,
'calendar_id': self.base_calendar.id,
})
# create a public holiday :
# When you create holidays graphically with a TZ,
# they are saved in the database after conversion to UTC.
# This is why, for a holiday starting on May 1, 2025, at 00:00:00 UTC+2,
# it will be saved in the database as April 30, 2025, at 22:00:00.
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",
})
# the employee has worked 7h on first may (public holiday) instead of 0h
# so the gap hours should be 7h and recovery hours 8,75h with coef 25%
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")

View File

@@ -1,20 +0,0 @@
<odoo>
<record id="view_hr_employee_stats_tree" model="ir.ui.view">
<field name="name">hr.employee.stats.tree</field>
<field name="model">hr.employee.stats</field>
<field name="arch" type="xml">
<tree name="Employee time stats" editable="top" decoration-muted="dayofweek in [5,6] or is_public_holiday">
<field name="name"/>
<field name="date"/>
<field name="employee_id"/>
<field name="total_hours" widget="float_time" sum="Total"/>
<field name="total_leave_hours" widget="float_time" sum="Total"/>
<field name="total_recovery_hours" widget="float_time" sum="Total"/>
<field name="total_planned_hours" widget="float_time" sum="Total"/>
<field name="gap_hours" widget="float_time" sum="Total"/>
<field name="dayofweek" invisible="1"/>
<field name="is_public_holiday" invisible="1"/>
</tree>
</field>
</record>
</odoo>

View File

@@ -1,78 +0,0 @@
<odoo>
<data>
<!-- Ajout d'une vue tree pour les stats dans la timesheet_sheet -->
<record id="view_hr_timesheet_sheet_form_inherit" model="ir.ui.view">
<field name="name">hr.timesheet.sheet.form.inherit</field>
<field name="model">hr_timesheet.sheet</field>
<field name="inherit_id" ref="hr_timesheet_sheet.hr_timesheet_sheet_form"/>
<field name="arch" type="xml">
<group name="dates" position="inside">
<field name="timesheet_sheet_gap_hours" string="Total gap hours in this timesheet sheet time range" widget="float_time"/>
<field name="timesheet_sheet_recovery_hours" string="Total recovery hours in this timesheet sheet time range" widget="float_time" style="font-weight: bold"/>
<button
colspan="2"
class="oe_highlight"
name="action_generate_recovery_allocation"
string='Create recovery allocation'
type='object'
attrs="{'invisible': ['|', ('can_review', '=', False),
('state', '!=', 'done'),
('timesheet_sheet_recovery_hours', '=', 0)]}"
/>
</group>
<xpath expr="//notebook" position="inside">
<page string="Employee Stats">
<field name="employee_stats_ids" no_label="1" readonly="1"/>
</page>
<page string="Generated recovery allocations">
<field name="recovery_allocation_ids" no_label="1" readonly="1"/>
</page>
</xpath>
</field>
</record>
<record id="hr_timesheet_sheet_stats_overview_pivot_view" model="ir.ui.view">
<field name="name">hr.timesheet.sheet.stats.overview.pivot.view</field>
<field name="model">hr_timesheet.sheet</field>
<field name="arch" type="xml">
<pivot string="Employee time stats pivot" sample="1">
<field name="employee_id" type="row"/>
</pivot>
</field>
</record>
<!-- Ajout d'une vue heure d'écart et récup sur la vue tree des hr_timesheet.sheet -->
<record id="view_hr_timesheet_sheet_tree_stats_inherit" model="ir.ui.view">
<field name="name">hr.timesheet.sheet.tree.stats.inherit</field>
<field name="model">hr_timesheet.sheet</field>
<field name="inherit_id" ref="hr_timesheet_sheet.hr_timesheet_sheet_tree" />
<field name="arch" type="xml">
<field name="department_id" position="after">
<field name="timesheet_sheet_gap_hours" string="Heures d'écart" optional="show" widget="float_time" />
<field name="timesheet_sheet_recovery_hours" string="Heures de récupération" optional="hide" widget="float_time" />
</field>
</field>
</record>
<!-- Ajout d'une vue pivot pour hr_timesheet.sheet -->
<!-- <record id="hr_timesheet_sheet_pivot" model="ir.ui.view">
<field name="name">hr.timesheet.sheet.pivot</field>
<field name="model">hr_timesheet.sheet</field>
<field name="arch" type="xml">
<pivot string="Timesheet Sheets Analysis">
<field name="employee_id" type="row"/>
<field name="department_id"/>
<field name="reviewer_id"/>
<field name="state"/>
<field name="date_start" type="row"/>
<field name="date_end"/>
<field name="total_time" type="measure" />
</pivot>
</field>
</record> -->
<!-- Ajout de la vue pivot à l'action "All Timesheet Sheets" -->
<!-- <record id="act_hr_timesheet_sheet_all_timesheets_pivot" model="ir.actions.act_window.view">
<field name="view_mode">pivot</field>
<field name="sequence" eval="10"/>
<field name="view_id" ref="hr_timesheet_sheet_pivot"/>
<field name="act_window_id" ref="hr_timesheet_sheet.act_hr_timesheet_sheet_all_timesheets"/>
</record> -->
</data>
</odoo>

View File

@@ -1,60 +0,0 @@
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.hr.timesheet</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="55" />
<field name="inherit_id" ref="hr_timesheet.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[@name='hr_timesheet_sheet']" position="inside">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_right_pane">
<label for="recovery_type_id"/>
<span
class="fa fa-lg fa-building"
title="Values set here are company-specific."
aria-label="Values set here are company-specific."
groups="base.group_multi_company"
role="img"
/>
<div class="text-muted">
Choose the recovery type.
</div>
<div class="content-group">
<div class="mb16">
<field
name="recovery_type_id"
class="o_light_label"
widget="selection"
required="1"
/>
</div>
</div>
<div class="text-muted">
Choose the coef to apply to recovery hours.
</div >
<div class="content-group">
<div class="mb16">
<field
name="coef"
class="o_light_label"
widget="float"
/> %
</div>
</div>
<div class="text-muted">
Choose to auto-validate the recovery allocation or not
</div>
<div class="content-group">
<div class="mb16">
<field
name="auto_validate_recovery_allocation"
widget="boolean_toggle"
/>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1 +0,0 @@
from . import models

View File

@@ -1,20 +0,0 @@
{
"name": "hr_employee_stats_sheet_lunch_voucher",
"version": "16.0.1.0.0",
"description": "Add global sheet for employee stats",
"summary": "Add global sheet for employee stats",
"author": "Elabore",
"website": "https://elabore.coop",
"license": "LGPL-3",
"category": "Human Resources",
"depends": [
"hr_employee_stats_sheet",
],
"data": [
"views/hr_employee_stats.xml",
"views/hr_timesheet_sheet.xml",
"views/res_config_settings_views.xml",
],
"installable": True,
"application": False,
}

View File

@@ -1,86 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * hr_employee_stats_sheet_lunch_voucher
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-11 13:19+0000\n"
"PO-Revision-Date: 2025-06-11 13:19+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_employee_stats_sheet_lunch_voucher
#: model:ir.model.fields,help:hr_employee_stats_sheet_lunch_voucher.field_res_config_settings__lunch_voucher_min_worked_hours
msgid ""
"5h by default, meaning that if an employee works 5h or more in a day, he "
"will get a lunch voucher"
msgstr "5h par défaut"
#. module: hr_employee_stats_sheet_lunch_voucher
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet_lunch_voucher.res_config_settings_view_form
msgid ""
"<span class=\"fa fa-lg fa-building\" title=\"Minimal number of hours worked "
"in a day to get a lunch voucher.\" groups=\"base.group_multi_company\"/>"
msgstr ""
"<span class=\"fa fa-lg fa-building\" title=\"Nombre minimal d'heures travaillées "
"dans une journée pour cumuler un prime repas.\" groups=\"base.group_multi_company\"/>"
#. module: hr_employee_stats_sheet_lunch_voucher
#: model:ir.model,name:hr_employee_stats_sheet_lunch_voucher.model_res_company
msgid "Companies"
msgstr "Sociétés"
#. module: hr_employee_stats_sheet_lunch_voucher
#: model:ir.model,name:hr_employee_stats_sheet_lunch_voucher.model_res_config_settings
msgid "Config Settings"
msgstr "Paramètres de configuration"
#. module: hr_employee_stats_sheet_lunch_voucher
#: model:ir.model,name:hr_employee_stats_sheet_lunch_voucher.model_hr_employee_stats
msgid "Employee Stats"
msgstr "Statistique de l'employé·e"
#. module: hr_employee_stats_sheet_lunch_voucher
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet_lunch_voucher.res_config_settings_view_form
msgid ""
"Indicate the minimal number of hours worked in a day to get a lunch voucher "
"(by default 5 hours)."
msgstr "Indiquez le nombre d'heures minimal pour cumuler une prime repas/ticket restaurant"
#. module: hr_employee_stats_sheet_lunch_voucher
#: model:ir.model.fields,field_description:hr_employee_stats_sheet_lunch_voucher.field_hr_employee_stats__lunch_voucher
msgid "Lunch Voucher"
msgstr "Ticket restaurant"
#. module: hr_employee_stats_sheet_lunch_voucher
#: model:ir.model.fields,field_description:hr_employee_stats_sheet_lunch_voucher.field_res_company__lunch_voucher_min_worked_hours
msgid "Lunch Voucher Min Worked Hours"
msgstr "Prime repas Nombre d'heures minimal"
#. module: hr_employee_stats_sheet_lunch_voucher
#: model:ir.model.fields,field_description:hr_employee_stats_sheet_lunch_voucher.field_hr_timesheet_sheet__lunch_voucher_count
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet_lunch_voucher.hr_timesheet_sheet_tree_lunch_voucher_inherit
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet_lunch_voucher.timesheet_sheet_form_lunch_voucher_inherit
msgid "Lunch voucher Count"
msgstr "Nombre de ticket restaurant"
#. module: hr_employee_stats_sheet_lunch_voucher
#: model:ir.model.fields,field_description:hr_employee_stats_sheet_lunch_voucher.field_res_config_settings__lunch_voucher_min_worked_hours
msgid "Minimal number of hours worked in a day to get a lunch voucher"
msgstr "Nombre d'heures minimal pour cumuler une prime repas/ticket restaurant"
#. module: hr_employee_stats_sheet_lunch_voucher
#: model:ir.model,name:hr_employee_stats_sheet_lunch_voucher.model_hr_timesheet_sheet
msgid "Timesheet Sheet"
msgstr "Feuille de temps"
#. module: hr_employee_stats_sheet_lunch_voucher
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet_lunch_voucher.hr_employee_stats_tree_lunch_voucher_inherit
msgid "Total"
msgstr ""

View File

@@ -1,42 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * hr_employee_stats_sheet_lunch_voucher
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-05 13:34+0000\n"
"PO-Revision-Date: 2025-05-05 13:34+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_employee_stats_sheet_lunch_voucher
#: model:ir.model,name:hr_employee_stats_sheet_lunch_voucher.model_hr_employee_stats
msgid "Employee Stats"
msgstr ""
#. module: hr_employee_stats_sheet_lunch_voucher
#: model:ir.model.fields,field_description:hr_employee_stats_sheet_lunch_voucher.field_hr_employee_stats__lunch_voucher
msgid "Lunch Voucher"
msgstr ""
#. module: hr_employee_stats_sheet_lunch_voucher
#: model:ir.model.fields,field_description:hr_employee_stats_sheet_lunch_voucher.field_hr_timesheet_sheet__lunch_voucher_count
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet_lunch_voucher.hr_timesheet_sheet_stats_overview_view_sikle_inherit
msgid "Lunch voucher Count"
msgstr ""
#. module: hr_employee_stats_sheet_lunch_voucher
#: model:ir.model,name:hr_employee_stats_sheet_lunch_voucher.model_hr_timesheet_sheet
msgid "Timesheet Sheet"
msgstr ""
#. module: hr_employee_stats_sheet_lunch_voucher
#: model_terms:ir.ui.view,arch_db:hr_employee_stats_sheet_lunch_voucher.view_hr_employee_stats_tree_sikle_inherit
msgid "Total"
msgstr ""

View File

@@ -1 +0,0 @@
from . import hr_employee_stats, hr_timesheet_sheet, res_config, res_company

View File

@@ -1,19 +0,0 @@
from odoo import fields, models, api
class HrEmployeeStats(models.Model):
_inherit = "hr.employee.stats"
lunch_voucher = fields.Integer("Lunch Voucher", compute="_compute_lunch_voucher")
@api.depends("total_hours")
def _compute_lunch_voucher(self):
for stat in self:
stat.lunch_voucher = 0
if stat.date and stat.employee_id:
stat._get_lunch_voucher()
def _get_lunch_voucher(self):
#do not factorize this method with _compute_lunch_voucher to be used in other modules
self.ensure_one()
if self.total_hours >= self.env.company.lunch_voucher_min_worked_hours:
self.lunch_voucher = 1

View File

@@ -1,15 +0,0 @@
from odoo import fields, models, api
class HrTimesheetSheet(models.Model):
_inherit = "hr_timesheet.sheet"
lunch_voucher_count = fields.Integer("Lunch voucher Count", compute="_compute_lunch_voucher_count")
@api.depends("employee_stats_ids.lunch_voucher")
def _compute_lunch_voucher_count(self):
for sheet in self:
sheet.lunch_voucher_count = 0
if sheet.employee_stats_ids:
for stat in sheet.employee_stats_ids:
sheet.lunch_voucher_count += stat.lunch_voucher

View File

@@ -1,7 +0,0 @@
from odoo import fields, models
class ResCompany(models.Model):
_inherit = "res.company"
lunch_voucher_min_worked_hours = fields.Float(default=5)

View File

@@ -1,14 +0,0 @@
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
lunch_voucher_min_worked_hours = fields.Float(
related="company_id.lunch_voucher_min_worked_hours",
required=True,
string="Minimal number of hours worked in a day to get a lunch voucher",
domain="[('company_id', '=', company_id)]",
readonly=False,
help="5h by default, meaning that if an employee works 5h or more in a day, he will get a lunch voucher",
)

View File

@@ -1,12 +0,0 @@
<odoo>
<record id="hr_employee_stats_tree_lunch_voucher_inherit" model="ir.ui.view">
<field name="name">hr.employee.stats.tree.lunch.voucher.inherit</field>
<field name="model">hr.employee.stats</field>
<field name="inherit_id" ref="hr_employee_stats_sheet.view_hr_employee_stats_tree"/>
<field name="arch" type="xml">
<field name="total_hours" position="after">
<field name="lunch_voucher" sum="Total"/>
</field>
</field>
</record>
</odoo>

View File

@@ -1,22 +0,0 @@
<odoo>
<record id="timesheet_sheet_form_lunch_voucher_inherit" model="ir.ui.view">
<field name="name">timesheet.sheet.form.lunch.voucher.inherit</field>
<field name="model">hr_timesheet.sheet</field>
<field name="inherit_id" ref="hr_timesheet_sheet.hr_timesheet_sheet_form" />
<field name="arch" type="xml">
<field name="department_id" position="after">
<field name="lunch_voucher_count" string="Lunch voucher Count" />
</field>
</field>
</record>
<record id="hr_timesheet_sheet_tree_lunch_voucher_inherit" model="ir.ui.view">
<field name="name">hr.timesheet.sheet.tree.lunch.voucher.inherit</field>
<field name="model">hr_timesheet.sheet</field>
<field name="inherit_id" ref="hr_timesheet_sheet.hr_timesheet_sheet_tree" />
<field name="arch" type="xml">
<field name="department_id" position="after">
<field name="lunch_voucher_count" string="Lunch voucher Count" />
</field>
</field>
</record>
</odoo>

View File

@@ -1,35 +0,0 @@
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.hr.timesheet</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="55" />
<field name="inherit_id" ref="hr_timesheet.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[@name='hr_timesheet_sheet']" position="inside">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_right_pane">
<label for="lunch_voucher_min_worked_hours"/>
<span
class="fa fa-lg fa-building"
title="Minimal number of hours worked in a day to get a lunch voucher."
groups="base.group_multi_company"
/>
<div class="text-muted">
Indicate the minimal number of hours worked in a day to get a lunch voucher (by default 5 hours).
</div>
<div class="content-group">
<div class="mb16">
<field
name="lunch_voucher_min_worked_hours"
class="o_light_label"
widget="float"
required="1"
/>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,2 +1,2 @@
*.*~ *.*~
*pyc *pyc

View File

@@ -1,20 +1,14 @@
===================================== ============================
hr_employee_stats_sheet_lunch_voucher hr_holidays_timeoff_analysis
===================================== ============================
Summary
=======
Description
===========
indicate day by day if a date is timeoff or not and generate an analyses pivot by employee
Installation Installation
============ ============
Use Odoo normal module installation procedure to install Use Odoo normal module installation procedure to install
``hr_employee_stats_sheet_lunch_voucher``. ``hr_holidays_timeoff_analysis``.
Known issues / Roadmap Known issues / Roadmap
====================== ======================
@@ -24,7 +18,7 @@ None yet.
Bug Tracker Bug Tracker
=========== ===========
Bugs are tracked on `our issues website <https://github.com/elabore-coop/hr_employee_stats_sheet_lunch_voucher/issues>`_. In case of Bugs are tracked on `our issues website <https://github.com/elabore-coop/hr_holidays_timeoff_analysis/issues>`_. In case of
trouble, please check there if your issue has already been trouble, please check there if your issue has already been
reported. If you spotted it first, help us smashing it by providing a reported. If you spotted it first, help us smashing it by providing a
detailed and welcomed feedback. detailed and welcomed feedback.
@@ -35,7 +29,7 @@ Credits
Contributors Contributors
------------ ------------
* `Elabore <mailto:laetitia.dacosta@elabore.coop>` * `Elabore <mailto:contact@elabore.coop>`
Funders Funders
------- -------

View File

@@ -2,24 +2,30 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{ {
"name": "timesheet_timesheet_analysis_report", "name": "hr_holidays_timeoff_analysis",
"version": "16.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": "add a timesheet_id relation to timesheet.analysis.report model", "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_timesheet", "base",
"hr_holidays",
], ],
"qweb": [], "qweb": [],
"external_dependencies": { "external_dependencies": {
"python": [], "python": [],
}, },
# always loaded # always loaded
"data": [], "data": [
"security/ir.model.access.csv",
"views/hr_leave_timeoff_day_views.xml",
"data/hr_leave_timeoff_day_cron.xml",
],
# only loaded in demonstration mode # only loaded in demonstration mode
"demo": [], "demo": [],
"js": [], "js": [],
@@ -29,4 +35,4 @@
# and independently installed. Used for synergetic or glue modules. # and independently installed. Used for synergetic or glue modules.
"auto_install": False, "auto_install": False,
"application": False, "application": False,
} }

View File

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

View File

@@ -0,0 +1,104 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * hr_holidays_timeoff_analysis
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-08-21 08:09+0000\n"
"PO-Revision-Date: 2025-08-21 08:09+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_holidays_timeoff_analysis
#: model:ir.actions.server,name:hr_holidays_timeoff_analysis.ir_cron_create_timeoff_day_ir_actions_server
#: model:ir.cron,cron_name:hr_holidays_timeoff_analysis.ir_cron_create_timeoff_day
msgid "Create and update Timeoff Days"
msgstr "Générer et mettre à jour la liste des jours en congés pour analyses"
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__create_uid
msgid "Created by"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__create_date
msgid "Created on"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__date
msgid "Date"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__display_name
msgid "Display Name"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__employee_id
msgid "Employee"
msgstr "Employé"
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__hr_leave_id
msgid "Hr Leave"
msgstr "Congés"
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__id
msgid "ID"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day____last_update
msgid "Last Modified on"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__write_uid
msgid "Last Updated by"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__write_date
msgid "Last Updated on"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__leave_duration_by_day
msgid "Leave Duration By Day"
msgstr "Durée d'absence par jour"
#. module: hr_holidays_timeoff_analysis
#: model_terms:ir.ui.view,arch_db:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_view_search
msgid "Period"
msgstr "Période"
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__hr_leave_type
msgid "Time Off Type"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model,name:hr_holidays_timeoff_analysis.model_hr_leave_timeoff_day
msgid "Timeoff Day"
msgstr "Jour d'absence"
#. module: hr_holidays_timeoff_analysis
#: model:ir.actions.act_window,name:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_action
#: model:ir.ui.menu,name:hr_holidays_timeoff_analysis.menu_hr_holidays_timeoff_day_menu
#: model_terms:ir.ui.view,arch_db:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_view_search
msgid "Timeoff Days"
msgstr "Jours d'absence"
#. module: hr_holidays_timeoff_analysis
#: model_terms:ir.ui.view,arch_db:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_view_pivot
msgid "Timeoff Days Analysis"
msgstr "Analyse des jours d'absence"

View File

@@ -0,0 +1,104 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * hr_holidays_timeoff_analysis
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-08-21 08:09+0000\n"
"PO-Revision-Date: 2025-08-21 08:09+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_holidays_timeoff_analysis
#: model:ir.actions.server,name:hr_holidays_timeoff_analysis.ir_cron_create_timeoff_day_ir_actions_server
#: model:ir.cron,cron_name:hr_holidays_timeoff_analysis.ir_cron_create_timeoff_day
msgid "Create and update Timeoff Days"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__create_uid
msgid "Created by"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__create_date
msgid "Created on"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__date
msgid "Date"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__display_name
msgid "Display Name"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__employee_id
msgid "Employee"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__hr_leave_id
msgid "Hr Leave"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__id
msgid "ID"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day____last_update
msgid "Last Modified on"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__write_uid
msgid "Last Updated by"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__write_date
msgid "Last Updated on"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__leave_duration_by_day
msgid "Leave Duration By Day"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model_terms:ir.ui.view,arch_db:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_view_search
msgid "Period"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__hr_leave_type
msgid "Time Off Type"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.model,name:hr_holidays_timeoff_analysis.model_hr_leave_timeoff_day
msgid "Timeoff Day"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model:ir.actions.act_window,name:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_action
#: model:ir.ui.menu,name:hr_holidays_timeoff_analysis.menu_hr_holidays_timeoff_day_menu
#: model_terms:ir.ui.view,arch_db:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_view_search
msgid "Timeoff Days"
msgstr ""
#. module: hr_holidays_timeoff_analysis
#: model_terms:ir.ui.view,arch_db:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_view_pivot
msgid "Timeoff Days Analysis"
msgstr ""

View File

@@ -0,0 +1 @@
from . import hr_leave_timeoff_day

View File

@@ -0,0 +1,130 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import api, fields, models
class TimeOffDay(models.Model):
_name = "hr.leave.timeoff.day"
_description = "Timeoff Day"
_order = "date desc"
date = fields.Date()
employee_id = fields.Many2one("hr.employee")
hr_leave_id = fields.Many2one("hr.leave")
hr_leave_type = fields.Many2one(related="hr_leave_id.holiday_status_id", store=True)
leave_duration_by_day = fields.Float()
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.
"""
calendar = employee.resource_calendar_id
if not calendar or not calendar.attendance_ids:
return False
day_of_week = str(date.weekday())
attendances = calendar.attendance_ids.filtered(
lambda att: att.dayofweek == day_of_week
)
return bool(attendances)
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))
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:
return True
return False
def compute_leave_duration_by_day(self, leave):
"""
Compute the leave duration by day based on the leave type.
"""
leave_duration_by_day = 0.0
# Full day case
if leave.request_unit_half:
leave_duration_by_day = 0.5
elif leave.request_unit_hours:
leave_duration_by_day = leave.number_of_days_display
else:
leave_duration_by_day = 1.0
return leave_duration_by_day
@api.model
def cron_manage_timeoff_days(self):
self.cron_create_timeoff_days()
self.cron_delete_timeoff_days()
def cron_create_timeoff_days(self):
# Browse all validated leaves
leaves = self.env["hr.leave"].search(
[
("state", "=", "validate"),
("request_date_from", "!=", False),
("request_date_to", "!=", False),
("employee_id", "!=", False),
]
)
for leave in leaves:
current_date = leave.request_date_from
employee = leave.employee_id
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):
# 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
if not self.search(
[
("date", "=", current_date),
("employee_id", "=", employee.id),
("hr_leave_id", "=", leave.id),
],
limit=1,
):
self.create(
{
"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)
def cron_delete_timeoff_days(self):
# Browse all unvalidated leaves
leaves = self.env["hr.leave"].search(
[
("state", "!=", "validate"),
("request_date_from", "!=", False),
("request_date_to", "!=", False),
("employee_id", "!=", False),
]
)
# Delete timeoff days for leaves that are no longer validated
for leave in leaves:
self.search(
[
("hr_leave_id", "=", leave.id),
]
).unlink()
# Delete timeoff days that are not linked to any leave
self.search(
[
("hr_leave_id", "=", False),
]
).unlink()

View File

@@ -0,0 +1,2 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_hr_leave_timeoff_day_manager,access_hr_holidays_timeoff_day_manager,model_hr_leave_timeoff_day,hr_holidays.group_hr_holidays_manager,1,0,0,0
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_hr_leave_timeoff_day_manager access_hr_holidays_timeoff_day_manager model_hr_leave_timeoff_day hr_holidays.group_hr_holidays_manager 1 0 0 0

View File

@@ -0,0 +1 @@
from . import test_hr_leave_timeoff_day

View File

@@ -0,0 +1,491 @@
from odoo.fields import Date
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged("post_install", "-at_install")
class TestHrLeaveTimeoffDay(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.employee = cls.env["hr.employee"].create(
{
"name": "Camille",
}
)
cls.base_calendar = cls.env["resource.calendar"].create(
{
"name": "default calendar",
}
)
cls.time_off_type = cls.env["hr.leave.type"].create(
{
"name": "Time Off",
"requires_allocation": "no",
"request_unit": "half_day",
}
)
cls.time_off_hour_type = cls.env["hr.leave.type"].create(
{
"name": "Recovery",
"requires_allocation": "no",
"request_unit": "hour",
}
)
def test_leave_between_2_months_nb_of_days(self):
leave = self.env["hr.leave"].create(
{
"employee_id": self.employee.id,
"request_date_from": Date.to_date("2025-06-30"),
"request_date_to": Date.to_date("2025-07-06"),
"holiday_status_id": self.time_off_type.id,
}
)
leave.state = "validate" # Simulate the leave being validated
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
# Vérifie qu'il existe bien 5 hr.leave.timeoff.day pour ce leave
timeoff_days = self.env["hr.leave.timeoff.day"].search(
[
("employee_id", "=", self.employee.id),
("hr_leave_id", "=", leave.id),
]
)
self.assertEqual(
len(timeoff_days), 5, "There should be 5 timeoff days for this leave"
)
# Vérifie que la somme des leave_duration_by_day fait bien 5
total_duration = sum(timeoff_days.mapped("leave_duration_by_day"))
self.assertEqual(
total_duration,
5.0,
"The sum of leave_duration_by_day should be 5.0 for this leave",
)
def test_leave_duration_by_day_half_day(self):
leave = self.env["hr.leave"].create(
{
"employee_id": self.employee.id,
"request_date_from": Date.to_date("2025-07-02"),
"request_date_to": Date.to_date("2025-07-02"),
"holiday_status_id": self.time_off_type.id,
"request_unit_half": True,
}
)
leave.state = "validate" # Simulate the leave being validated
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
timeoff_days = self.env["hr.leave.timeoff.day"].search(
[
("employee_id", "=", self.employee.id),
("hr_leave_id", "=", leave.id),
]
)
self.assertEqual(
len(timeoff_days), 1, "There should be 1 timeoff day for this leave"
)
total_duration = sum(timeoff_days.mapped("leave_duration_by_day"))
self.assertEqual(
total_duration,
0.5,
"leave_duration_by_day should be 0.5 for a half day leave",
)
def test_leave_duration_by_day_hour_compare_to_employee_calendar(self):
employee_calendar = self.env["resource.calendar"].create(
{
"name": "Employee Calendar",
"attendance_ids": [
(
0,
0,
{
"name": "Monday Morning",
"dayofweek": "0",
"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",
},
),
(
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
leave = self.env["hr.leave"].create(
{
"employee_id": self.employee.id,
"request_date_from": Date.to_date("2025-07-03"),
"request_date_to": Date.to_date("2025-07-03"),
"request_unit_hours": True,
"request_hour_from": "8",
"request_hour_to": "12",
"holiday_status_id": self.time_off_hour_type.id,
}
)
leave._compute_date_from_to()
leave._compute_number_of_hours_display()
leave._compute_number_of_days_display()
leave.state = "validate" # Simulate the leave being validated
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
timeoff_days = self.env["hr.leave.timeoff.day"].search(
[
("employee_id", "=", self.employee.id),
("hr_leave_id", "=", leave.id),
]
)
self.assertEqual(
len(timeoff_days), 1, "There should be 1 timeoff day for this leave"
)
total_duration = sum(timeoff_days.mapped("leave_duration_by_day"))
self.assertEqual(
total_duration,
0.5,
"leave_duration_by_day should be 0.5 for 4 hours on an 8h day",
)
def test_leave_duration_by_day_compare_to_time_part_employee_calendar(self):
employee_calendar = self.env["resource.calendar"].create(
{
"name": "Employee Calendar",
"attendance_ids": [
(
0,
0,
{
"name": "Monday Morning",
"dayofweek": "0",
"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
leave = self.env["hr.leave"].create(
{
"employee_id": self.employee.id,
"request_date_from": Date.to_date("2025-07-21"),
"request_date_to": Date.to_date("2025-07-27"),
"holiday_status_id": self.time_off_type.id,
}
)
leave.state = "validate" # Simulate the leave being validated
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
timeoff_days = self.env["hr.leave.timeoff.day"].search(
[
("employee_id", "=", self.employee.id),
("hr_leave_id", "=", leave.id),
]
)
self.assertEqual(
len(timeoff_days), 4, "There should be 4 timeoff day for this leave"
)
total_duration = sum(timeoff_days.mapped("leave_duration_by_day"))
self.assertEqual(
total_duration,
4,
"leave_duration_by_day should be 4 day",
)
def test_public_holidays_between_a_leave(self):
# Leaves the code below commented because
# 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"),
# "date_to": Date.to_date("2025-05-08 23:00:00"),
# }
# )
leave = self.env["hr.leave"].create(
{
"employee_id": self.employee.id,
"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)
"holiday_status_id": self.time_off_type.id,
}
)
leave.state = "validate" # Simulate the leave being validated
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
# Vérifie qu'il existe bien 4 hr.leave.timeoff.day pour ce leave
timeoff_days = self.env["hr.leave.timeoff.day"].search(
[
("employee_id", "=", self.employee.id),
("hr_leave_id", "=", leave.id),
]
)
self.assertEqual(
len(timeoff_days), 4, "There should be 4 timeoff days for this leave"
)
# Vérifie que la somme des leave_duration_by_day fait bien 4
total_duration = sum(timeoff_days.mapped("leave_duration_by_day"))
self.assertEqual(
total_duration,
4.0,
"The sum of leave_duration_by_day should be 4.0 for this leave",
)
def test_unvalidated_leave(self):
leave = self.env["hr.leave"].create(
{
"employee_id": self.employee.id,
"request_date_from": Date.to_date("2025-08-04"),
"request_date_to": Date.to_date("2025-08-10"),
"holiday_status_id": self.time_off_type.id,
}
)
leave.state = "validate" # Simulate the leave being validated
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
# Vérifie qu'il existe bien 5 hr.leave.timeoff.day pour ce leave
timeoff_days = self.env["hr.leave.timeoff.day"].search(
[
("employee_id", "=", self.employee.id),
("hr_leave_id", "=", leave.id),
]
)
self.assertEqual(
len(timeoff_days), 5, "There should be 5 timeoff days for this leave"
)
leave.state = "draft"
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
timeoff_days = self.env["hr.leave.timeoff.day"].search(
[
("employee_id", "=", self.employee.id),
("hr_leave_id", "=", leave.id),
]
)
self.assertEqual(
len(timeoff_days), 0, "There should be no timeoff days for this leave"
)
def test_deleted_leave(self):
leave = self.env["hr.leave"].create(
{
"employee_id": self.employee.id,
"request_date_from": Date.to_date("2025-08-18"),
"request_date_to": Date.to_date("2025-08-24"),
"holiday_status_id": self.time_off_type.id,
}
)
leave.state = "validate" # Simulate the leave being validated
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
# Vérifie qu'il existe bien 5 hr.leave.timeoff.day pour ce leave
timeoff_days = self.env["hr.leave.timeoff.day"].search(
[
("employee_id", "=", self.employee.id),
("hr_leave_id", "=", leave.id),
]
)
self.assertEqual(
len(timeoff_days), 5, "There should be 5 timeoff days for this leave"
)
leave.state = "draft"
leave.unlink()
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
timeoff_days = self.env["hr.leave.timeoff.day"].search(
[
("employee_id", "=", self.employee.id),
("hr_leave_id", "=", leave.id),
]
)
self.assertEqual(
len(timeoff_days), 0, "There should be no timeoff days for this leave"
)

View File

@@ -0,0 +1,62 @@
<?xml version='1.0' encoding='UTF-8' ?>
<odoo>
<record id="hr_leave_timeoff_day_view_list" model="ir.ui.view">
<field name="model">hr.leave.timeoff.day</field>
<field name="arch" type="xml">
<tree editable="bottom">
<field name="date" />
<field name="employee_id" groups="base.group_user" />
<field name="hr_leave_id" />
<field name="hr_leave_type" />
<field name="leave_duration_by_day" />
</tree>
</field>
</record>
<record id="hr_leave_timeoff_day_view_pivot" model="ir.ui.view">
<field name="model">hr.leave.timeoff.day</field>
<field name="arch" type="xml">
<pivot string="Timeoff Days Analysis">
<field name="date" type="col" interval="month" />
<field name="employee_id" type="row" />
<field name="hr_leave_type" type="row" />
<field name="leave_duration_by_day" type="measure" />
</pivot>
</field>
</record>
<record id="hr_leave_timeoff_day_view_search" model="ir.ui.view">
<field name="model">hr.leave.timeoff.day</field>
<field name="arch" type="xml">
<search string="Timeoff Days">
<field name="date" />
<field name="employee_id" groups="base.group_user" />
<separator />
<filter
name="filter_date"
date="date"
default_period="this_year"
string="Period"
/>
</search>
</field>
</record>
<record id="hr_leave_timeoff_day_action" model="ir.actions.act_window">
<field name="name">Timeoff Days</field>
<field name="res_model">hr.leave.timeoff.day</field>
<field name="view_mode">tree,form,pivot</field>
<field name="search_view_id" ref="hr_leave_timeoff_day_view_search" />
<field name="context">{'search_default_filter_date': True}</field>
</record>
<menuitem
id="menu_hr_holidays_timeoff_day_menu"
name="Timeoff Days"
parent="hr_holidays.menu_hr_holidays_configuration"
action="hr_leave_timeoff_day_action"
groups="hr_holidays.group_hr_holidays_manager"
sequence="6"
/>
</odoo>

View File

@@ -1,2 +0,0 @@
*.*~
*pyc

View File

@@ -1,44 +0,0 @@
===================================
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.

View File

@@ -1 +0,0 @@
from . import models

View File

@@ -1,35 +0,0 @@
# Copyright 2024 Elabore ()
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "hr_negative_leave",
"version": "16.0.3.0.1",
"author": "Elabore",
"website": "https://elabore.coop",
"maintainer": "Elabore",
"license": "AGPL-3",
"category": "hr",
"summary": "allow negative leaves, manage negative leave balances and negative allocations",
# any module necessary for this one to work correctly
"depends": [
"base","hr_holidays",
],
"qweb": [],
"external_dependencies": {
"python": [],
},
# always loaded
"data": [
"views/hr_leave_type_views.xml",
"views/hr_leave_views.xml",
],
# only loaded in demonstration mode
"demo": [],
"js": [],
"css": [],
"installable": True,
# Install this module automatically if all dependency have been previously
# and independently installed. Used for synergetic or glue modules.
"auto_install": False,
"application": False,
}

View File

@@ -1,54 +0,0 @@
# 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"

View File

@@ -1,54 +0,0 @@
# 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 ""

View File

@@ -1 +0,0 @@
from . import hr_leave_type, hr_leave

View File

@@ -1,16 +0,0 @@
# -*- 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()

View File

@@ -1,228 +0,0 @@
# -*- 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.")
@api.depends('requires_allocation')
def _compute_valid(self):
res = super()._compute_valid()
for holiday_type in res:
#if negative is allowed, then the holiday type is valid in any case
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)
####################################################
# END OF Modification for leaves allowing negative #
####################################################
# 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

View File

@@ -1 +0,0 @@
from . import test_hr_negative_leave

View File

@@ -1,91 +0,0 @@
from datetime import date, timedelta
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
@tagged('post_install', '-at_install')
class TestHrNegativeLeave(TransactionCase):
def setUp(self):
super().setUp()
# create a simple employee
self.employee = self.env['hr.employee'].create({
'name': 'NegTest Employee',
})
# create a user
self.user = self.env['res.users'].create({
'name': 'Test user',
'login': 'test user',
'employee_id': self.employee.id,
})
# prepare a leave type and an allocation with 1 day available
self.leave_type = self.env['hr.leave.type'].create({
'name': 'NegTest Type',
'request_unit': 'day',
'requires_allocation': 'yes',
'allows_negative': False,
})
self.allocation = self.env['hr.leave.allocation'].create({
'name': 'Alloc 1d',
'employee_id': self.employee.id,
'holiday_status_id': self.leave_type.id,
'number_of_days': 1.0,
'date_from': date.today() - timedelta(days=1),
'allocation_type': 'regular',
})
def test_negative_not_allowed_raises(self):
self.allocation.action_validate()
#self.leave_type._compute_leaves()
"""If the leave type does NOT allow negative, trying to confirm a leave
that exceeds allocations should raise a UserError."""
leave = self.env['hr.leave'].create({
'name': 'Too many days',
'employee_id': self.employee.id,
'holiday_status_id': self.leave_type.id,
'request_date_from': date.today(),
'request_date_to': date.today() + timedelta(days=1),
'number_of_days': 2.0,
'state': 'draft',
})
# self.leave_type._compute_leaves()
with self.assertRaises(UserError):
leave.write({'state': 'validate'})
def test_negative_allowed_allows_excess(self):
self.env.user = self.user
self.env.user.employee_id = self.employee
self.allocation.action_validate()
"""If the leave type allows negative, confirming a leave that exceeds
allocations must NOT raise an error."""
# flip the flag on the leave type
self.leave_type.allows_negative = True
leave = self.env['hr.leave'].create({
'name': 'Too many days',
'employee_id': self.employee.id,
'holiday_status_id': self.leave_type.id,
'date_from': date.today(),
'date_to': date.today() + timedelta(days=1),
'number_of_days': 2.0,
'state': 'draft',
})
# should not raise
leave.write({'state': 'confirm'})
# check remaining leaves is negative
self.leave_type._compute_leaves()
# Allocated in time off popup (Alloué)
self.assertEqual(self.leave_type.max_leaves, 1, "max_leaves should be 1",)
# Approuved in time off popup (Approuvé)
self.assertEqual(self.leave_type.virtual_leaves_taken, 2, "virtual_leaves_taken should be 2",)
# Remaining in time off popup (Restants)
self.assertEqual(self.leave_type.max_leaves - self.leave_type.virtual_leaves_taken, -1, "remaining leaves should display in timeoff popup -1",)

View File

@@ -1,18 +0,0 @@
<?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>

View File

@@ -1,31 +0,0 @@
<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">
<!-- Pseudo-code for the domain:
if requires_allocation == 'no':
ok
elif has_valid_allocation and virtual_remaining_leaves > 0 and max_leaves > 0:
ok
elif allows_negative == True:
ok
else:
refuse
-->
<attribute name="domain">[
'|', '|',
('requires_allocation', '=', 'no'),
'&amp;',
('has_valid_allocation', '=', True),
'&amp;',
('virtual_remaining_leaves', '&gt;', 0),
('max_leaves', '&gt;', 0),
('allows_negative', '=', True)
]</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,2 +0,0 @@
from . import models
from . import wizard

View File

@@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': "hr_timesheet_import_from_calendar",
'version': '16.0.1.0.1',
'depends': ['hr_timesheet'],
'author': "Élabore",
'category': 'Human Resources/Employees',
'summary' : "Import HR timesheet from calendar events",
'description': """
In HR timesheet list view, you can import directly from your calendar.
""",
'data': [
'views/hr_timesheet_views.xml',
'wizard/hr_timesheet_import_from_calendar_wizard_views.xml',
'security/ir.model.access.csv'
],
'demo': [
],
'application': False,
'license': 'LGPL-3',
}

View File

@@ -1 +0,0 @@
from . import hr_timesheet

View File

@@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
from collections import defaultdict
from lxml import etree
import re
from odoo import api, Command, fields, models, _, _lt
from odoo.exceptions import UserError, AccessError, ValidationError
from odoo.osv import expression
class AccountAnalyticLine(models.Model):
_inherit = 'account.analytic.line'
calendar_event_origin_id = fields.Many2one('calendar.event')

View File

@@ -1,3 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_timesheet_import_from_calendar_wizard,access.hr.timesheet.import.from.calendar.wizard,model_hr_timesheet_import_from_calendar_wizard,hr_timesheet.group_hr_timesheet_user,1,1,1,1
access_hr_timesheet_import_from_calendar_wizard_line,access.hr.timesheet.import.from.calendar.wizard.line,model_hr_timesheet_import_from_calendar_wizard_line,hr_timesheet.group_hr_timesheet_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_timesheet_import_from_calendar_wizard access.hr.timesheet.import.from.calendar.wizard model_hr_timesheet_import_from_calendar_wizard hr_timesheet.group_hr_timesheet_user 1 1 1 1
3 access_hr_timesheet_import_from_calendar_wizard_line access.hr.timesheet.import.from.calendar.wizard.line model_hr_timesheet_import_from_calendar_wizard_line hr_timesheet.group_hr_timesheet_user 1 1 1 1

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
</data>
</odoo>

View File

@@ -1 +0,0 @@
from . import hr_timesheet_import_from_calendar_wizard

View File

@@ -1,106 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
from datetime import date, timedelta
import re
from odoo.exceptions import UserError
class HrTimesheetImportFromCalendarWizard(models.TransientModel):
_name = 'hr.timesheet.import.from.calendar.wizard'
imported = fields.Boolean()
date_from = fields.Date('Du', default=lambda self: self._default_date_from(), required=True)
date_to = fields.Date('Au', default=lambda self: self._default_date_to(), required=True)
lines = fields.One2many('hr.timesheet.import.from.calendar.wizard.line', 'wizard_id', string="Lines")
warning = fields.Boolean()
def _default_date_from(self):
return date.today()-timedelta(days=date.today().weekday())-timedelta(days=7)
def _default_date_to(self):
return date.today()-timedelta(days=date.today().weekday())-timedelta(days=1)
def action_import(self):
self.lines.unlink()
calendar_events = self.env['calendar.event'].search([
('user_id','=',self.env.user.id),
('start','>=',self.date_from),
('start','<=',self.date_to),
]).sorted('start')
for event in calendar_events:
project_id = None
if '[' in event.name and ']' in event.name:
analytic_account_codes = re.findall(r'\[(.*?)\]', event.name)
if analytic_account_codes:
analytic_account_code = analytic_account_codes[0]
analytic_account = self.env['account.analytic.account'].search([('code','=',analytic_account_code)], limit=1)
if analytic_account:
project = self.env['project.project'].search([('analytic_account_id','=',analytic_account.id)], limit=1)
project_id = project.id
# Check if analytic account line already created by this event
existing_lines = self.env['account.analytic.line'].search([('calendar_event_origin_id','=',event.id)])
warning = False
if existing_lines:
warning = True
self.warning = True
self.env['hr.timesheet.import.from.calendar.wizard.line'].create({
'wizard_id':self.id,
'date': event.start,
'name': event.name,
'project_id':project_id,
'unit_amount':event.duration,
'calendar_event_origin_id': event.id,
'warning':warning
})
self.imported = True
return {
'type': 'ir.actions.act_window',
'name': 'Importer depuis le calendrier',
'res_model': 'hr.timesheet.import.from.calendar.wizard',
'res_id': self.id, # Garde le même enregistrement ouvert
'view_mode': 'form',
'target': 'new', # Wizard modal
}
@api.onchange('lines')
def check_warning(self):
for line in self.lines:
if line.warning:
self.warning = True
return
self.warning = False
def action_confirm(self):
if not all([l.project_id for l in self.lines]):
raise UserError("Vous devez indiquer un projet sur chaque ligne. Si une ligne n'est pas concernée, vous devez la supprimer.")
for line in self.lines:
self.env['account.analytic.line'].create({
'date':line.date,
'project_id':line.project_id.id,
'name':line.name,
'unit_amount':line.unit_amount,
'calendar_event_origin_id':line.calendar_event_origin_id.id
})
action = self.env["ir.actions.act_window"]._for_xml_id("hr_timesheet.act_hr_timesheet_line")
action['target'] = 'main' # clear breadcrump
return action
class HrTimesheetImportFromCalendarWizardLine(models.TransientModel):
_name = 'hr.timesheet.import.from.calendar.wizard.line'
wizard_id = fields.Many2one('hr.timesheet.import.from.calendar.wizard')
date = fields.Date()
project_id = fields.Many2one('project.project')
name = fields.Char('Description')
unit_amount = fields.Float('Heures passées')
calendar_event_origin_id = fields.Many2one('calendar.event')
warning = fields.Boolean()

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_timesheet_import_from_calendar_wizard_view" model="ir.ui.view">
<field name="name">hr.timesheet.import.from.calendar.wizard.view</field>
<field name="model">hr.timesheet.import.from.calendar.wizard</field>
<field name="arch" type="xml">
<form>
<field name="imported" invisible="True" />
Du <field name="date_from" />
Au <field name="date_to" />
<button name="action_import" type="object" string="Récupérer la liste" />
<field name="warning" invisible="True" />
<div attrs="{'invisible': [('warning', '=', False)]}">
<p class="oe_field_label" style="color: red;">Attention: Certaines lignes ont déjà été importées !</p>
</div>
<field name="lines" attrs="{'invisible': [('imported', '=', False)]}">
<tree editable="bottom" decoration-warning="warning != False">
<field name="date" />
<field name="project_id" />
<field name="name" />
<field name="unit_amount" widget="float_time" />
<field name="warning" invisible="True" />
</tree>
</field>
<footer>
<button class="oe_highlight" name="action_confirm" type="object" string="Créer les feuilles de temps" attrs="{'invisible': [('imported', '=', False)]}" />
<button special="cancel" string="Cancel" class="oe_link" />
</footer>
</form>
</field>
</record>
<record id="action_hr_timesheet_import_from_calendar_wizard" model="ir.actions.act_window">
<field name="name">Importer depuis le calendrier</field>
<field name="res_model">hr.timesheet.import.from.calendar.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem
id="timesheet_menu_import_from_calendar"
action="action_hr_timesheet_import_from_calendar_wizard"
sequence="11"
parent="hr_timesheet.timesheet_menu_root"
groups="hr_timesheet.group_hr_timesheet_user"
/>
</odoo>

View File

@@ -1,55 +0,0 @@
============================
hr_timesheet_sheet_usability
============================
Summary
=======
Various changes to improve the usability of hr_timesheet_sheet application
Description
===========
This module changes :
- menu, menu order, menu access right
- french translation
of hr_timesheet_sheet module
Installation
============
Use Odoo normal module installation procedure to install
``hr_timesheet_sheet_usability``.
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.

View File

@@ -1,19 +0,0 @@
{
"name": "hr_timesheet_sheet_usability_misc",
"version": "16.0.1.1.0",
"description": "Various changes to improve the usability of hr_timesheet_sheet application",
"summary": "",
"author": "Elabore",
"website": "https://github.com/Alusage/odoo-hr-addons",
"license": "LGPL-3",
"category": "Human Resources",
"depends": [
"base","hr_timesheet","hr_timesheet_sheet",
],
"data": [
"views/hr_timesheet_sheet_menu.xml",
"views/account_analytic_line_views.xml",
],
"installable": True,
"application": False,
}

View File

@@ -1,80 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-19 09:03+0000\n"
"PO-Revision-Date: 2025-06-19 09:03+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: hr_timesheet
#: model:ir.actions.act_window,name:hr_timesheet.timesheet_action_all
#: model:ir.ui.menu,name:hr_timesheet.timesheet_menu_activity_all
msgid "All Timesheets"
msgstr "Tous les temps"
#. module: hr_timesheet
#: model:ir.actions.act_window,name:hr_timesheet.act_hr_timesheet_line
#: model:ir.ui.menu,name:hr_timesheet.timesheet_menu_activity_mine
#: model:ir.ui.menu,name:hr_timesheet.timesheet_menu_activity_user
#: model_terms:ir.ui.view,arch_db:hr_timesheet.hr_timesheet_line_search
#: model_terms:ir.ui.view,arch_db:hr_timesheet.hr_timesheet_report_search
msgid "My Timesheets"
msgstr "Mes temps"
#. module: hr_timesheet
#. odoo-python
#: code:addons/hr_timesheet/models/hr_timesheet.py:0
#: code:addons/hr_timesheet/models/project.py:0
#: model:ir.actions.act_window,name:hr_timesheet.act_hr_timesheet_line_by_project
#: model:ir.actions.act_window,name:hr_timesheet.timesheet_action_from_employee
#: model:ir.actions.report,name:hr_timesheet.timesheet_report
#: model:ir.actions.report,name:hr_timesheet.timesheet_report_project
#: model:ir.actions.report,name:hr_timesheet.timesheet_report_task
#: model:ir.actions.report,name:hr_timesheet.timesheet_report_task_timesheets
#: model:ir.model.fields,field_description:hr_timesheet.field_project_project__allow_timesheets
#: model:ir.model.fields,field_description:hr_timesheet.field_project_task__timesheet_ids
#: model:ir.ui.menu,name:hr_timesheet.menu_hr_time_tracking
#: model:ir.ui.menu,name:hr_timesheet.menu_timesheets_reports_timesheet
#: model:ir.ui.menu,name:hr_timesheet.timesheet_menu_root
#: model_terms:ir.ui.view,arch_db:hr_timesheet.hr_department_view_kanban
#: model_terms:ir.ui.view,arch_db:hr_timesheet.portal_layout
#: model_terms:ir.ui.view,arch_db:hr_timesheet.portal_my_home_timesheet
#: model_terms:ir.ui.view,arch_db:hr_timesheet.portal_my_task
#: model_terms:ir.ui.view,arch_db:hr_timesheet.portal_my_timesheets
#: model_terms:ir.ui.view,arch_db:hr_timesheet.project_invoice_form
#: model_terms:ir.ui.view,arch_db:hr_timesheet.project_project_view_form_simplified_inherit_timesheet
#: model_terms:ir.ui.view,arch_db:hr_timesheet.project_sharing_inherit_project_task_view_form
#: model_terms:ir.ui.view,arch_db:hr_timesheet.report_timesheet
#: model_terms:ir.ui.view,arch_db:hr_timesheet.report_timesheet_task
#: model_terms:ir.ui.view,arch_db:hr_timesheet.res_config_settings_view_form
#: model_terms:ir.ui.view,arch_db:hr_timesheet.timesheet_project_task_page
#: model_terms:ir.ui.view,arch_db:hr_timesheet.timesheets_analysis_report_graph_employee
#: model_terms:ir.ui.view,arch_db:hr_timesheet.timesheets_analysis_report_graph_project
#: model_terms:ir.ui.view,arch_db:hr_timesheet.timesheets_analysis_report_graph_task
#: model_terms:ir.ui.view,arch_db:hr_timesheet.view_hr_timesheet_line_graph
#: model_terms:ir.ui.view,arch_db:hr_timesheet.view_hr_timesheet_line_pivot
#: model_terms:ir.ui.view,arch_db:hr_timesheet.view_project_kanban_inherited
#: model_terms:ir.ui.view,arch_db:hr_timesheet.view_task_form2_inherited
#, python-format
msgid "Timesheets"
msgstr "Temps"
#. module: hr_timesheet_sheet
#: model:ir.actions.act_window,name:hr_timesheet_sheet.act_hr_timesheet_sheet_all_timesheets
#: model:ir.ui.menu,name:hr_timesheet_sheet.menu_act_hr_timesheet_sheet_all_timesheets
msgid "All Timesheet Sheets"
msgstr "Toutes les feuilles de temps"
#. module: hr_timesheet_sheet
#: model:ir.ui.menu,name:hr_timesheet_sheet.menu_hr_my_timesheets
msgid "My Timesheets"
msgstr "Feuilles de temps"

View File

@@ -1,26 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * hr_timesheet_sheet_usability
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-15 08:39+0000\n"
"PO-Revision-Date: 2025-09-15 08:39+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_timesheet_sheet_usability
#: model_terms:ir.ui.view,arch_db:hr_timesheet_sheet_usability.view_hr_timesheet_sheet_group_by_inherit
msgid "Date de départ"
msgstr ""
#. module: hr_timesheet_sheet_usability
#: model_terms:ir.ui.view,arch_db:hr_timesheet_sheet_usability.view_hr_timesheet_sheet_group_by_inherit
msgid "Timesheet by Date"
msgstr ""

View File

@@ -1,11 +0,0 @@
<odoo>
<!-- Hide menu "Timesheets to Submit" -->
<record id="hr_timesheet_sheet.menu_act_hr_timesheet_line_to_submit" model="ir.ui.menu">
<field name="active">False</field>
</record>
<!-- Hide menu "My Timesheets to Submit" -->
<record id="hr_timesheet_sheet.menu_act_hr_timesheet_line_to_submit_my" model="ir.ui.menu">
<field name="active">False</field>
</record>
</odoo>

View File

@@ -1,47 +0,0 @@
<?xml version="1.0"?>
<odoo>
<!-- add date group by date in timesheet sheet view -->
<record id="view_hr_timesheet_sheet_group_by_inherit" model="ir.ui.view">
<field name="name">hr_timesheet.sheet.filter</field>
<field name="model">hr_timesheet.sheet</field>
<field
name="inherit_id"
ref="hr_timesheet_sheet.view_hr_timesheet_sheet_filter"
/>
<field name="arch" type="xml">
<group position="inside">
<filter string="Date de départ" name="groupby_date" domain="[]" context="{'group_by': 'date_start'}" help="Timesheet by Date"/>
</group>
</field>
</record>
<!-- Make menu "My Timesheets to Submit" visible only for managers -->
<menuitem
id="hr_timesheet_sheet.menu_hr_to_review"
groups="hr_timesheet.group_timesheet_manager"
name="To Review"
parent="hr_timesheet.timesheet_menu_root"
sequence="7"
/>
<menuitem
id="hr_timesheet_sheet.menu_act_hr_timesheet_sheet_to_review"
groups="hr_timesheet.group_timesheet_manager"
action="hr_timesheet_sheet.act_hr_timesheet_sheet_to_review"
parent="hr_timesheet_sheet.menu_hr_to_review"
sequence="11"
/>
<!-- Move All timesheets menu under Timesheet menu -->
<menuitem
id="hr_timesheet_sheet.menu_act_hr_timesheet_sheet_all_timesheets"
parent="hr_timesheet_sheet.menu_hr_my_timesheets"
/>
<!-- Cancel timesheet line menus move and restore their original placement from hr_timesheet module -->
<record model="ir.ui.menu" id="hr_timesheet.timesheet_menu_activity_mine">
<field name="parent_id" ref="hr_timesheet.menu_hr_time_tracking" />
</record>
<record model="ir.ui.menu" id="hr_timesheet.timesheet_menu_activity_user">
<field name="parent_id" ref="hr_timesheet.timesheet_menu_root" />
</record>
</odoo>

View File

@@ -1,30 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
"name": "hr_usability Elabore",
"version": "16.0.1.0.0",
"depends": [
"base",
"hr",
"hr_holidays",
],
"author": "Élabore",
"category": "Human Resources/Employees",
"summary": "In times off type form view, add 'create_calendar_meeting' field",
"description": """
Go to Times off app > "Setings" > "Type time off"
Select a type
The 'create_calendar_meeting' is check by default
Uncheck it if do not want to display times off in the calendar
One the checkbox is unchecked for a time off type, the next approuved times off (of that type) won't be created as a meeting and won't appear in the calendar
""",
"data": [
"views/hr_leave_type_views.xml",
],
"demo": [],
"application": False,
"license": "LGPL-3",
}

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_leave_type_form_inherit" model="ir.ui.view">
<field name="name">hr.leave.type.form.inherit</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="//field[@name='color']" position="before">
<field name="create_calendar_meeting"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,2 +0,0 @@
*.*~
*pyc

View File

@@ -1,41 +0,0 @@
===================
l10n_fr_hr_holidays
===================
Manages French specificities for leaves and holidays, specialy for part-time employees.
Installation
============
Use Odoo normal module installation procedure to install
``l10n_fr_hr_holidays``.
Configuration
=============
In settings, select the leaves type on wich you want to manage the french specificities.
Known issues / Roadmap
======================
None yet.
Credits
=======
Contributors
------------
* `Elabore <mailto:contacnt@elabore.coop>`
Funders
-------
The development of this module has been financially supported by:
* Elabore (https://elabore.coop)
Maintainer
----------
This module is maintained by Elabore.

View File

@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
"name": "France - Time Off",
"version": "16.0.1.0.1",
"category": "Human Resources/Time Off",
"countries": ["fr"],
"summary": "Management of leaves for part-time workers in France",
"depends": ["hr_holidays", "l10n_fr","resource"],
"auto_install": True,
"license": "LGPL-3",
"data": [
"views/res_config_settings_views.xml",
],
"demo": [
"data/l10n_fr_hr_holidays_demo.xml",
],
}

View File

@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="l10n_fr_part_time_calendar" model="resource.calendar">
<field name="name">Part time</field>
<field name="company_id" eval="False"/>
<field name="hours_per_day">9</field>
<field name="attendance_ids"
eval="[(5, 0, 0),
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 18.0, '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': 18.0, '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': 18.0, 'day_period': 'afternoon'}),
]"
/>
</record>
<record id="l10n_fr_part_time_employee" model="hr.employee">
<field name="company_id" ref="l10n_fr.demo_company_fr"/>
<field name="active" eval="1"/>
<field name="name">Mitchell Admin</field>
<field name="user_id" ref="base.user_admin"/>
<field name="resource_calendar_id" ref="l10n_fr_part_time_calendar"/>
<field name="image_1920" eval="obj(ref('base.partner_admin')).image_1920" model="res.partner"/>
</record>
<record id="l10n_fr_holiday_status_cl" model="hr.leave.type">
<field name="name">Paid Time Off</field>
<field name="company_id" ref="l10n_fr.demo_company_fr"/>
<field name="requires_allocation">yes</field>
<field name="employee_requests">no</field>
<field name="leave_validation_type">both</field>
<field name="allocation_validation_type">officer</field>
<field name="responsible_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="icon_id" ref="hr_holidays.icon_14"/>
<field name="color">2</field>
<field name="has_valid_allocation">True</field>
</record>
<record id="l10n_fr.demo_company_fr" model="res.company">
<field name="l10n_fr_reference_leave_type" ref="l10n_fr_holiday_status_cl"/>
</record>
<record id="l10n_fr_hr_holidays_allocation" model="hr.leave.allocation">
<field name="name">Paid Time Off allocation</field>
<field name="state">confirm</field>
<field name="holiday_status_id" ref="l10n_fr_holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="date_from" eval="time.strftime('%Y-01-01')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
<field name="employee_id" ref="l10n_fr_part_time_employee"/>
<field name="employee_ids" eval="[(4, ref('l10n_fr_part_time_employee'))]"/>
</record>
<function model="hr.leave.allocation" name="action_validate">
<value eval="[ref('l10n_fr_hr_holidays_allocation')]"/>
</function>
</odoo>

View File

@@ -1,7 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import hr_leave
from . import res_company
from . import resource_calendar
from . import res_config_settings

View File

@@ -1,201 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import fields, models, _, api
from odoo.exceptions import UserError
class HrLeave(models.Model):
_inherit = 'hr.leave'
resource_calendar_id = fields.Many2one('resource.calendar', compute='_compute_resource_calendar_id', store=True, readonly=False, copy=False)
l10n_fr_date_to_changed = fields.Boolean()
@api.depends(
'holiday_type',
'employee_id',
'employee_id.resource_calendar_id',
'request_date_from',
'request_date_to',
)
def _compute_resource_calendar_id(self):
for leave in self:
calendar = False
if leave.holiday_type == 'employee':
calendar = leave.employee_id.resource_calendar_id
# YTI: Crappy hack: Move this to a new dedicated hr_holidays_contract module
# We use the request dates to find the contracts, because date_from
# and date_to are not set yet at this point. Since these dates are
# used to get the contracts for which these leaves apply and
# contract start- and end-dates are just dates (and not datetimes)
# these dates are comparable.
if 'hr.contract' in self.env and leave.employee_id:
contracts = self.env['hr.contract'].search([
'|', ('state', 'in', ['open', 'close']),
'&', ('state', '=', 'draft'),
('kanban_state', '=', 'done'),
('employee_id', '=', leave.employee_id.id),
('date_start', '<=', leave.request_date_to),
'|', ('date_end', '=', False),
('date_end', '>=', leave.request_date_from),
])
if contracts:
# If there are more than one contract they should all have the
# same calendar, otherwise a constraint is violated.
calendar = contracts[:1].resource_calendar_id
# Crappy hack again : if hr_employee_calendar_planning is installed
# use planning calendar instead of contract calendar
if 'hr.employee.calendar' in self.env:
calendar_planning = self.env["hr.employee.calendar"].search(
[
("employee_id", "=", leave.employee_id.id),
("date_start", "<=", leave.request_date_to),
"|", ("date_end", "=", False), # pas de date de fin OU
("date_end", ">=", leave.request_date_from), # date de fin après le début
],
limit=1,
)
if calendar_planning:
calendar = calendar_planning.calendar_id
elif leave.holiday_type == 'department':
calendar = leave.department_id.company_id.resource_calendar_id
elif leave.holiday_type == 'company':
calendar = leave.mode_company_id.resource_calendar_id
leave.resource_calendar_id = calendar or self.env.company.resource_calendar_id
def _l10n_fr_leave_applies(self):
# The french l10n is meant to be computed only in very specific cases:
# - there is only one employee affected by the leave
# - the company is french
# - the leave_type is the reference leave_type of that company
self.ensure_one()
return self.employee_id and \
self.employee_company_id.country_id.code == 'FR' and \
self.resource_calendar_id != self.employee_company_id.resource_calendar_id and \
self.holiday_status_id == self.employee_company_id._get_fr_reference_leave_type()
def _get_fr_date_from_to(self, date_from, date_to):
self.ensure_one()
# What we need to compute is how much we will need to push date_to in order to account for the lost days
# This gets even more complicated in two_weeks_calendars
# The following computation doesn't work for resource calendars in
# which the employee works zero hours.
if not (self.resource_calendar_id.attendance_ids):
raise UserError(_("An employee cannot take a paid time off in a period they work no hours."))
if self.request_unit_half and self.request_date_from_period == 'am':
# In normal workflows request_unit_half implies that date_from and date_to are the same
# request_unit_half allows us to choose between `am` and `pm`
# In a case where we work from mon-wed and request a half day in the morning
# we do not want to push date_to since the next work attendance is actually in the afternoon
date_from_weektype = str(self.env['resource.calendar.attendance'].get_week_type(date_from))
date_from_dayofweek = str(date_from.weekday())
# Get morning and afternoon attendances for that day
attendances_am = self.resource_calendar_id.attendance_ids.filtered(lambda a:
a.dayofweek == date_from_dayofweek
and a.day_period == 'morning'
and (not self.resource_calendar_id.two_weeks_calendar or a.week_type == date_from_weektype))
attendances_pm = self.resource_calendar_id.attendance_ids.filtered(lambda a:
a.dayofweek == date_from_dayofweek
and a.day_period == 'afternoon'
and (not self.resource_calendar_id.two_weeks_calendar or a.week_type == date_from_weektype))
if attendances_am and not attendances_pm:
# If the employee does not work in the afternoon, postpone date_to to the next working day
next_date = date_from + relativedelta(days=1)
while not self.resource_calendar_id._works_on_date(next_date):
next_date += relativedelta(days=1)
return (date_from, next_date)
elif attendances_am and attendances_pm:
# The employee also works in the afternoon, no postponement
return (date_from, date_to)
# Special handling for two-weeks calendars
if self.resource_calendar_id.two_weeks_calendar:
# Count the number of days actually worked by the employee between date_from and date_to
current_date = date_from
days_count = 0
while current_date <= date_to:
if self.resource_calendar_id._works_on_date(current_date):
days_count += 1
current_date += relativedelta(days=1)
# Adjust date_to so it matches the expected number of days
# If the expected number of days is less than the period, reduce date_to
if days_count > 0:
# Find the date_to that gives the right number of worked days
current_date = date_from
counted = 0
while counted < days_count:
if self.resource_calendar_id._works_on_date(current_date):
counted += 1
if counted == days_count:
break
current_date += relativedelta(days=1)
return (date_from, current_date)
# Check calendars for working days until we find the right target, start at date_to + 1 day
# Postpone date_target until the next working day
date_start = date_from
date_target = date_to
# It is necessary to move the start date up to the first work day of
# the employee calendar as otherwise days worked on by the company
# calendar before the actual start of the leave would be taken into
# account.
while not self.resource_calendar_id._works_on_date(date_start):
date_start += relativedelta(days=1)
while not self.resource_calendar_id._works_on_date(date_target + relativedelta(days=1)):
date_target += relativedelta(days=1)
# Undo the last day increment
return (date_start, date_target)
def _compute_date_from_to(self):
super()._compute_date_from_to()
for leave in self:
if leave._l10n_fr_leave_applies():
new_date_from, new_date_to = leave._get_fr_date_from_to(leave.date_from, leave.date_to)
if new_date_from != leave.date_from:
leave.date_from = new_date_from
if new_date_to != leave.date_to:
leave.date_to = new_date_to
leave.l10n_fr_date_to_changed = True
else:
leave.l10n_fr_date_to_changed = False
#@overwrite
def _get_calendar(self):
"""
In France, paid time off for part-time employees is counted on the company's working days (not the employee's own schedule).
The company's calendar must be used for the legal leave day count.
"""
self.ensure_one()
if self._l10n_fr_leave_applies():
return self.employee_company_id.resource_calendar_id or self.env.company.resource_calendar_id
return super()._get_calendar()
#@overwrite
def _get_number_of_days_batch(self, date_from, date_to, employee_ids):
"""
Returns a dict with the number of legal leave days for each employee,
based on the company's calendar. In France, part-time employees accrue and take leave on company working days,
not only on their own working days. Handles half-day requests and rounds according to French rules.
"""
employee = self.env['hr.employee'].browse(employee_ids)
# Force the company in the domain, as we are likely in a compute_sudo context
domain = [
('time_type', '=', 'leave'),
('company_id', 'in', self.env.company.ids + self.env.context.get('allowed_company_ids', []))
]
calendar = self._get_calendar()
result = employee._get_work_days_data_batch(date_from, date_to, calendar=calendar, domain=domain)
for employee_id in result:
# For non-French context: a half-day leave always counts as 0.5 day
if self.request_unit_half and result[employee_id]['hours'] > 0 and not self._l10n_fr_leave_applies():
result[employee_id]['days'] = 0.5
# For French context: round the number of days to the nearest half-day (legal rule)
elif self.request_unit_half and result[employee_id]['hours'] > 0 and self._l10n_fr_leave_applies():
result[employee_id]['days'] = self._round_to_nearest_half(result[employee_id]['days'])
return result
def _round_to_nearest_half(self, x):
"""Round a float to the nearest 0.5."""
return round(x * 2) / 2

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo.exceptions import ValidationError
class ResCompany(models.Model):
_inherit = 'res.company'
l10n_fr_reference_leave_type = fields.Many2one(
'hr.leave.type',
string='Company Paid Time Off Type')
def _get_fr_reference_leave_type(self):
self.ensure_one()
if not self.l10n_fr_reference_leave_type:
raise ValidationError(_("You must first define a reference time off type for the company."))
return self.l10n_fr_reference_leave_type

View File

@@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
l10n_fr_reference_leave_type = fields.Many2one(
'hr.leave.type',
related='company_id.l10n_fr_reference_leave_type',
readonly=False)
# backport from V170
company_country_code = fields.Char(related="company_id.country_id.code", string="Company Country Code", readonly=True)

View File

@@ -1,26 +0,0 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from collections import defaultdict
class ResourceCalendar(models.Model):
_inherit = 'resource.calendar'
def _works_on_date(self, date):
self.ensure_one()
working_days = self._get_working_hours()
dayofweek = str(date.weekday())
if self.two_weeks_calendar:
weektype = str(self.env['resource.calendar.attendance'].get_week_type(date))
return working_days[weektype][dayofweek]
return working_days[False][dayofweek]
def _get_working_hours(self):
self.ensure_one()
working_days = defaultdict(lambda: defaultdict(lambda: False))
for attendance in self.attendance_ids:
working_days[attendance.week_type][attendance.dayofweek] = True
return working_days

View File

@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_french_leaves

View File

@@ -1,359 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase, tagged
@tagged('post_install_l10n', 'post_install', '-at_install', 'french_leaves')
class TestFrenchLeaves(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
country_fr = cls.env.ref('base.fr')
cls.company = cls.env['res.company'].create({
'name': 'French Company',
'country_id': country_fr.id,
})
cls.employee = cls.env['hr.employee'].create({
'name': 'Camille',
'gender': 'other',
'birthday': '1973-03-29',
'country_id': country_fr.id,
'company_id': cls.company.id,
})
cls.time_off_type = cls.env['hr.leave.type'].create({
'name': 'Time Off',
'requires_allocation': 'no',
'request_unit': 'half_day',
})
cls.company.write({
'l10n_fr_reference_leave_type': cls.time_off_type.id,
})
cls.base_calendar = cls.env['resource.calendar'].create({
'name': 'default calendar',
})
def test_no_differences(self):
# Base case that should not have a different behaviour
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = self.base_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-06',
'request_date_to': '2021-09-10',
'employee_company_id': self.company.id,
'resource_calendar_id': self.employee.resource_calendar_id.id,
})
leave._compute_date_from_to()
self.assertEqual(leave.number_of_days, 5, 'The number of days should be equal to 5.')
leave.unlink()
def test_end_of_week(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', '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'}),
],
})
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-06', #monday
'request_date_to': '2021-09-08', #wednesday
'employee_company_id': self.company.id,
'resource_calendar_id': self.employee.resource_calendar_id.id,
})
leave._compute_date_from_to()
self.assertEqual(leave.number_of_days, 5, 'The number of days should be equal to 5.')
leave.unlink()
def test_start_of_week(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'attendance_ids': [
(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.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-08',
'request_date_to': '2021-09-10',
'employee_company_id': self.company.id,
'resource_calendar_id': self.employee.resource_calendar_id.id,
})
leave._compute_date_from_to()
self.assertEqual(leave.number_of_days, 5, 'The number of days should be equal to 5.')
leave.unlink()
def test_last_day_half(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'attendance_ids': [
(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.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-10', #friday
'request_date_to': '2021-09-10',
'request_unit_half': True,
'request_date_from_period': 'am',
'employee_company_id': self.company.id,
'resource_calendar_id': self.employee.resource_calendar_id.id,
})
leave._compute_date_from_to()
# Since the employee works on the afternoon, the date_to is not post-poned
self.assertEqual(leave.number_of_days, 0.5, 'The number of days should be equal to 0.5.')
leave.request_date_from_period = 'pm'
# This however should push the date_to
self.assertEqual(leave.number_of_days, 2.5, 'The number of days should be equal to 2.5.')
leave.unlink()
def test_full_time_am_day_half(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', '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'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
],
})
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-10', #friday
'request_date_to': '2021-09-10',
'request_unit_half': True,
'request_date_from_period': 'am',
'employee_company_id': self.company.id,
'resource_calendar_id': self.employee.resource_calendar_id.id,
})
leave._compute_date_from_to()
# Since the employee works doesnt work the afternoon, the date_to is post-poned
self.assertEqual(leave.number_of_days, 1, 'The number of days should be equal to 1.')
leave.unlink()
def test_am_day_half(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'attendance_ids': [
(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'}),
],
})
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-24', #friday
'request_date_to': '2021-09-24',
'request_unit_half': True,
'request_date_from_period': 'am',
'employee_company_id': self.company.id,
'resource_calendar_id': self.employee.resource_calendar_id.id,
})
leave._compute_date_from_to()
# Since the employee works doesnt work the afternoon, the date_to is post-poned
self.assertEqual(leave.number_of_days, 3, 'The number of days should be equal to 3.')
leave.unlink()
def test_calendar_with_holes(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', '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': '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': '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.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-06',
'request_date_to': '2021-09-10',
'employee_company_id': self.company.id,
'resource_calendar_id': self.employee.resource_calendar_id.id,
})
leave._compute_date_from_to()
self.assertEqual(leave.number_of_days, 5, 'The number of days should be equal to 5.')
leave.unlink()
def test_calendar_end_week_hole(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', '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': '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'}),
],
})
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-06',
'request_date_to': '2021-09-08',
'employee_company_id': self.company.id,
'resource_calendar_id': self.employee.resource_calendar_id.id,
})
leave._compute_date_from_to()
self.assertEqual(leave.number_of_days, 5, 'The number of days should be equal to 5.')
leave.unlink()
def test_2_weeks_calendar(self):
company_calendar = self.env['resource.calendar'].create({
'name': 'Company Calendar',
'two_weeks_calendar': True,
'attendance_ids': [
(0, 0, {'week_type': '0', 'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '0', 'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '0', 'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '0', 'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '0', 'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '0', 'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '0', 'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '0', 'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '0', 'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '0', 'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '1', 'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '1', 'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '1', 'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '1', 'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '1', 'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '1', 'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
],
})
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', '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'}),
],
})
self.company.resource_calendar_id = company_calendar
self.employee.resource_calendar_id = employee_calendar
# Week type 0
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-06',
'request_date_to': '2021-09-08',
'employee_company_id': self.company.id,
'resource_calendar_id': self.employee.resource_calendar_id.id,
})
leave._compute_date_from_to()
self.assertEqual(leave.number_of_days, 5, 'The number of days should be equal to 5.')
leave.unlink()
# Week type 1
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-13',
'request_date_to': '2021-09-15',
'employee_company_id': self.company.id,
'resource_calendar_id': self.employee.resource_calendar_id.id,
})
leave._compute_date_from_to()
self.assertEqual(leave.number_of_days, 3, 'The number of days should be equal to 3.')
leave.unlink()
# Both ending with week type 1
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-06',
'request_date_to': '2021-09-15',
'employee_company_id': self.company.id,
'resource_calendar_id': self.employee.resource_calendar_id.id,
})
leave._compute_date_from_to()
self.assertEqual(leave.number_of_days, 8, 'The number of days should be equal to 3.')
leave.unlink()
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-13',
'request_date_to': '2021-09-22',
'company_id': self.company.id,
'resource_calendar_id': self.employee.resource_calendar_id.id,
})
leave._compute_date_from_to()
self.assertEqual(leave.number_of_days, 8, 'The number of days should be equal to 3.')
leave.unlink()

View File

@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.hr</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="70"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<div name="work_organization_setting_container" position="after">
<field name="company_country_code" invisible="1"/>
<h2>French Time Off Localization</h2>
<div class="row mt16 o_settings_container" name="work_organization_setting_container">
<div class="col-12 col-lg-6 o_setting_box" id="default_company_work_organization_setting">
<div class="o_setting_right_pane">
<label for="resource_calendar_id"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." role="img" aria-label="Values set here are company-specific." groups="base.group_multi_company"/>
<div class="row">
<div class="text-muted col-lg-8">
Set the time off type used as the company Paid Time Off to compute part-timers leave duration
</div>
</div>
<div class="content-group">
<div class="mt16">
<field name="l10n_fr_reference_leave_type" required="1"
class="o_light_label"
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]"
context="{'default_company_id': company_id}"/>
</div>
</div>
</div>
</div>
</div>
</div>
</field>
</record>
<menuitem id="hr_holidays_menu_configuration"
name="Settings"
parent="hr_holidays.menu_hr_holidays_configuration"
sequence="10"
action="hr.hr_config_settings_action"
groups="base.group_system"/>
</odoo>

View File

@@ -1 +0,0 @@
from . import models

View File

@@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': "project timesheet holidays type",
'version': '16.0.1.0.0',
'depends': ['project_timesheet_holidays'],
'author': "Élabore",
'category': 'Human Resources/Employees',
'summary' : "add holidays type in project timesheet holidays type description",
'description': """
In project timesheet holidays, all holidays types are name 'Times off (d/d)' in description.
This module changes 'Times off' by the holiday type for better description.
""",
'data': [
],
'demo': [
],
'application': False,
'license': 'LGPL-3',
}

View File

@@ -1 +0,0 @@
from . import hr_holidays

View File

@@ -1,12 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
class Holidays(models.Model):
_inherit = "hr.leave"
def _timesheet_prepare_line_values(self, index, work_hours_data, day_date, work_hours_count):
res = super()._timesheet_prepare_line_values(index, work_hours_data, day_date, work_hours_count)
res['name'] = _("%s",self.holiday_status_id.name)
return res

View File

@@ -1,45 +0,0 @@
===================================
timesheet_timesheet_analysis_report
===================================
add a timesheet_id relation many2one to timesheet.analysis.report model
in order to allow filter by timesheet
Installation
============
Use Odoo normal module installation procedure to install
``timesheet_timesheet_analysis_report``.
Known issues / Roadmap
======================
None yet.
Bug Tracker
===========
Bugs are tracked on `our issues website <https://github.com/elabore-coop/timesheet_timesheet_analysis_report/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.

View File

@@ -1 +0,0 @@
from . import models

View File

@@ -1 +0,0 @@
from . import timesheet_analysis_report

View File

@@ -1,12 +0,0 @@
from odoo import fields, models, api
class TimesheetsAnalysisReport(models.Model):
_inherit = "timesheets.analysis.report"
timesheet_id = fields.Many2one("account.analytic.line", string="Timesheet", readonly=True, help="Feuille de temps")
@api.model
def _select(self):
return super()._select() + """,
A.id AS timesheet_id
"""