From 0718e859bd4be419fccbaa85c206e2fd810daec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=9C=D0=B0=D0=BB=D1=8C=D1=86=D0=B5=D0=B2?= Date: Fri, 17 Jan 2025 19:52:44 +0300 Subject: [PATCH 1/2] create new branch --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 0ccb447..f9a16d4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ # hr_holidays_ru - From ad845e7ad87ea507a58f04ef5f5c75e7a2cfc396 Mon Sep 17 00:00:00 2001 From: odoo-robot Date: Fri, 17 Jan 2025 21:10:28 +0000 Subject: [PATCH 2/2] automatic update repo with commit - 'Update .gitlab-ci.yml' from project hr_holidays_ru --- README.md | 1 + __init__.py | 6 + __manifest__.py | 36 ++ data/holidays_cron.xml | 14 + data/holidays_notification_template.xml | 15 + data/holidays_template.xml | 63 +++ i18n/ru.po | 507 ++++++++++++++++++ models/__init__.py | 7 + models/holidays.py | 197 +++++++ models/hr_leave.py | 259 +++++++++ models/hr_leave_allocation.py | 31 ++ models/hr_leave_type.py | 63 +++ models/resource.py | 390 ++++++++++++++ security/ir.model.access.csv | 5 + static/src/css/calendar_renderer.css | 63 +++ .../common/calendar_common_renderer.js | 71 +++ .../filter_panel/calendar_filter_panel.js | 122 +++++ .../filter_panel/calendar_filter_panel.xml | 38 ++ .../calendar/year/calendar_year_renderer.js | 66 +++ views/hr_holidays_menus_view.xml | 12 + views/hr_holidays_views.xml | 170 ++++++ views/hr_leave_allocation_views.xml | 7 + views/hr_leave_report_calendar_view.xml | 7 + views/hr_leave_views.xml | 50 ++ wizard/__init__.py | 3 + wizard/hr_holidays_summary_department.py | 180 +++++++ 26 files changed, 2383 insertions(+) create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 data/holidays_cron.xml create mode 100644 data/holidays_notification_template.xml create mode 100644 data/holidays_template.xml create mode 100644 i18n/ru.po create mode 100644 models/__init__.py create mode 100644 models/holidays.py create mode 100644 models/hr_leave.py create mode 100644 models/hr_leave_allocation.py create mode 100644 models/hr_leave_type.py create mode 100644 models/resource.py create mode 100644 security/ir.model.access.csv create mode 100644 static/src/css/calendar_renderer.css create mode 100644 static/src/views/calendar/common/calendar_common_renderer.js create mode 100644 static/src/views/calendar/filter_panel/calendar_filter_panel.js create mode 100644 static/src/views/calendar/filter_panel/calendar_filter_panel.xml create mode 100644 static/src/views/calendar/year/calendar_year_renderer.js create mode 100644 views/hr_holidays_menus_view.xml create mode 100644 views/hr_holidays_views.xml create mode 100644 views/hr_leave_allocation_views.xml create mode 100644 views/hr_leave_report_calendar_view.xml create mode 100644 views/hr_leave_views.xml create mode 100644 wizard/__init__.py create mode 100644 wizard/hr_holidays_summary_department.py diff --git a/README.md b/README.md index f9a16d4..0ccb447 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ # hr_holidays_ru + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..b3cac0a --- /dev/null +++ b/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +from odoo import api, SUPERUSER_ID + +from . import models +from . import wizard diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..19dd962 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +{ + "name": "hr_holidays_ru", + "summary": """ + Adds possibility to count leaves in calendar days as is customary in Russian Federation. + """, + "description": """ + In Russian Federation leaves are counted in calendar days instead of working days. + Also there are not working days and public holidays which are taken into account in different ways. + This module implements these features to correctly count leaves in RF. + """, + "author": "RYDLAB", + "website": "http://rydlab.ru", + "license": "Other proprietary", + "category": "Localization/Payroll", + "version": "17.0.1.3", + "depends": ["base", "hr_holidays", "resource", "calendar", "web"], + "data": [ + "security/ir.model.access.csv", + "data/holidays_template.xml", + "data/holidays_cron.xml", + "data/holidays_notification_template.xml", + "views/hr_holidays_views.xml", + "views/hr_leave_views.xml", + "views/hr_holidays_menus_view.xml", + "views/hr_leave_report_calendar_view.xml", + "views/hr_leave_allocation_views.xml", + ], + "assets": { + "web.assets_backend": [ + "hr_holidays_ru/static/src/css/*.css", + "hr_holidays_ru/static/src/views/calendar/**/*.xml", + "hr_holidays_ru/static/src/views/calendar/**/*.js", + ], + }, +} diff --git a/data/holidays_cron.xml b/data/holidays_cron.xml new file mode 100644 index 0000000..d95ebda --- /dev/null +++ b/data/holidays_cron.xml @@ -0,0 +1,14 @@ + + + + + HR Holidays: Notify about employee holiday + + code + model._check_users_holidays() + 1 + days + -1 + True + + \ No newline at end of file diff --git a/data/holidays_notification_template.xml b/data/holidays_notification_template.xml new file mode 100644 index 0000000..c12fda6 --- /dev/null +++ b/data/holidays_notification_template.xml @@ -0,0 +1,15 @@ + + + + HR holidays: Holidays start notification + {{ object.holiday_status_id.responsible_employee_to_notify_id.work_email }} + Reminder of the holidays start + + {{ object.holiday_status_id.responsible_employee_to_notify_id.lang }} + +

Dear ,

+

A reminder that the employee begins his vacation in days.

+

A vacation from to . Duration of holidays: day(s).

+
+
+
\ No newline at end of file diff --git a/data/holidays_template.xml b/data/holidays_template.xml new file mode 100644 index 0000000..18f37d6 --- /dev/null +++ b/data/holidays_template.xml @@ -0,0 +1,63 @@ + + + + Weekends and holidays 2022 + 2022 + + + + New Year + 2022-01-01 + 2022-01-08 + is_holiday + + + + + 23 February + 2022-02-23 + 2022-02-23 + is_holiday + + + + + 8 March + 2022-03-08 + 2022-03-08 + is_holiday + + + + + 1 May + 2022-05-01 + 2022-05-01 + is_holiday + + + + + 9 May + 2022-05-09 + 2022-05-09 + is_holiday + + + + + 12 June + 2022-06-12 + 2022-06-12 + is_holiday + + + + + 4 November + 2022-11-04 + 2022-11-04 + is_holiday + + + diff --git a/i18n/ru.po b/i18n/ru.po new file mode 100644 index 0000000..8a75882 --- /dev/null +++ b/i18n/ru.po @@ -0,0 +1,507 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_holidays_ru +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0-20241029\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-11-20 10:45+0000\n" +"PO-Revision-Date: 2024-11-20 10:45+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_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/hr_leave_type.py:0 +#, python-format +msgid " days" +msgstr "дней" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/hr_leave_type.py:0 +#, python-format +msgid " hours" +msgstr "часов" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/hr_leave_type.py:0 +#, python-format +msgid "%g remaining " +msgstr "%g осталось " + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/holidays.py:0 +#, python-format +msgid "%s (Copy)" +msgstr "%s (Копия)" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/hr_leave.py:0 +#, python-format +msgid "%s : %.2f Calendar day(s)" +msgstr "%s : %.2f Календарных дней" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/hr_leave.py:0 +#, python-format +msgid "%s : %.2f Working day(s)" +msgstr "%s : %.2f Рабочих дней" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/hr_leave.py:0 +#, python-format +msgid "%s : %.2f hour(s)" +msgstr "%s : %.2f часов" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/hr_leave.py:0 +#, python-format +msgid "%s on %s : %.2f Calendar day(s)" +msgstr "%s из %s : %.2f Календарных дней" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/hr_leave.py:0 +#, python-format +msgid "%s on %s : %.2f Working day(s)" +msgstr "%s из %s : %.2f Рабочих дней" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/hr_leave.py:0 +#, python-format +msgid "%s on %s : %.2f hour(s)" +msgstr "%s из %s : %.2f часов" + +#. module: hr_holidays_ru +#: model:mail.template,body_html:hr_holidays_ru.holidays_notification_template +msgid "" +"

Dear ,

\n" +"

A reminder that the employee begins his vacation in days.

\n" +"

A vacation from to . Duration of holidays: day(s).

\n" +" " +msgstr "" +"

Уважаемый(ая) ,

Напоминаем вам о том, что сотрудник уходит в отпуск через " +"дня/дней.

Отпуск с по . Продолжительность " +"отпуска: дня/дней.

" + +#. module: hr_holidays_ru +#: model_terms:ir.ui.view,arch_db:hr_holidays_ru.hr_leave_allocation_view_form_inherit +msgid "" +"Calendar days\n" +" Working days\n" +" Hours" +msgstr "" + +#. module: hr_holidays_ru +#: model_terms:ir.ui.view,arch_db:hr_holidays_ru.hr_leave_view_form_inherit +msgid "Calendar days" +msgstr "Календарных дней" + +#. module: hr_holidays_ru +#: model_terms:ir.ui.view,arch_db:hr_holidays_ru.hr_leave_view_form_inherit +msgid "Hours" +msgstr "Часов" + +#. module: hr_holidays_ru +#: model_terms:ir.ui.view,arch_db:hr_holidays_ru.hr_leave_view_form_inherit +msgid "Working days" +msgstr "Рабочих дней" + +#. module: hr_holidays_ru +#: model_terms:ir.ui.view,arch_db:hr_holidays_ru.hr_leave_view_form_inh +msgid "Days" +msgstr "Дней" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar_leaves__calendar_id +msgid "Calendar" +msgstr "Календарь" + +#. module: hr_holidays_ru +#: model:ir.model.fields.selection,name:hr_holidays_ru.selection__hr_leave_allocation__type_request_unit__c_day +msgid "Calendar Days" +msgstr "" + +#. module: hr_holidays_ru +#: model:ir.model.fields.selection,name:hr_holidays_ru.selection__hr_leave_type__request_unit__c_day +#: model_terms:ir.ui.view,arch_db:hr_holidays_ru.hr_leave_view_kanban_inherit +msgid "Calendar days" +msgstr "Календарные дни" + +#. module: hr_holidays_ru +#: model:ir.model.fields,help:hr_holidays_ru.field_hr_leave_type__exclude_holidays +msgid "Check this box to exclude holidays from calendar days amount." +msgstr "" +"Установите этот флажок, чтобы исключить праздники из количества календарных " +"дней." + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar__create_uid +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar_leaves__create_uid +msgid "Created by" +msgstr "Кем создано" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar__create_date +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar_leaves__create_date +msgid "Created on" +msgstr "Когда создано" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_hr_leave_type__days_before_holidays +msgid "Days before holidays" +msgstr "Дней до отпуска" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar__display_name +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar_leaves__display_name +msgid "Display Name" +msgstr "Отображаемое имя" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_hr_leave_type__responsible_employee_to_notify_id +msgid "Employee to notify" +msgstr "Сотрудник, которого необходимо уведомить" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar_leaves__date_to +msgid "End Date" +msgstr "Дата окончания" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_hr_leave_type__exclude_holidays +msgid "Exclude holidays" +msgstr "Исключить праздники" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/wizard/hr_holidays_summary_department.py:0 +#, python-format +msgid "Form content is missing, this report cannot be printed." +msgstr "Содержимое формы отсутствует, этот отчет не может быть распечатан." + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar__global_leave_ids +#: model_terms:ir.ui.view,arch_db:hr_holidays_ru.open_view_holiday_settings_form +msgid "Global Leaves" +msgstr "Глобальные отпуска" + +#. module: hr_holidays_ru +#: model:ir.actions.server,name:hr_holidays_ru.ir_cron_notification_about_paid_holidays_ir_actions_server +msgid "HR Holidays: Notify about employee holiday" +msgstr "HR holidays: Уведомить об отпуске сотрудника" + +#. module: hr_holidays_ru +#: model:mail.template,name:hr_holidays_ru.holidays_notification_template +msgid "HR holidays: Holidays start notification" +msgstr "HR holidays: уведомление о начале каникул" + +#. module: hr_holidays_ru +#: model:ir.model.fields.selection,name:hr_holidays_ru.selection__holidays_calendar_leaves__type_transfer_day__is_holiday +msgid "Holiday" +msgstr "Праздник" + +#. module: hr_holidays_ru +#: model:ir.model,name:hr_holidays_ru.model_report_hr_holidays_report_holidayssummary +msgid "Holidays Summary Report" +msgstr "Сводный отчет о праздниках" + +#. module: hr_holidays_ru +#: model:ir.actions.act_window,name:hr_holidays_ru.open_view_holiday_settings +#: model:ir.ui.menu,name:hr_holidays_ru.hr_holidays_status_menu_configuration +msgid "Holidays and non-working days" +msgstr "Праздничные и нерабочие дни" + +#. module: hr_holidays_ru +#: model:ir.model.fields.selection,name:hr_holidays_ru.selection__hr_leave_type__request_unit__hour +msgid "Hours" +msgstr "Часов" + +#. module: hr_holidays_ru +#: model:ir.model.fields,help:hr_holidays_ru.field_hr_leave_type__days_before_holidays +msgid "" +"How many days before it is necessary to notify the responsible employee " +"about the start of the holidays." +msgstr "" +"За сколько дней до начала отпуска необходимо уведомить ответственного " +"сотрудника?" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar__id +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar_leaves__id +msgid "ID" +msgstr "" + +#. module: hr_holidays_ru +#: model:ir.model.fields,help:hr_holidays_ru.field_hr_leave_type__notify_before_start_holidays +msgid "" +"Is it necessary to notify the responsible employee before the start of the " +"holidays?" +msgstr "Нужно ли уведомлять ответственного сотрудника о начале отпуска?" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar__write_uid +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar_leaves__write_uid +msgid "Last Updated by" +msgstr "Кем обновлено" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar__write_date +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar_leaves__write_date +msgid "Last Updated on" +msgstr "Когда обновлено" + +#. module: hr_holidays_ru +#: model:ir.model.fields.selection,name:hr_holidays_ru.selection__holidays_calendar_leaves__time_type__leave +msgid "Leave" +msgstr "Отгул" + +#. module: hr_holidays_ru +#. odoo-javascript +#: code:addons/hr_holidays_ru/static/src/views/calendar/filter_panel/calendar_filter_panel.xml:0 +#: model:ir.model.fields.selection,name:hr_holidays_ru.selection__holidays_calendar_leaves__type_transfer_day__is_day_off +#, python-format +msgid "Non-working day" +msgstr "Нерабочий день" + +#. module: hr_holidays_ru +#: model_terms:ir.ui.view,arch_db:hr_holidays_ru.edit_holiday_status_form_inherit +msgid "Notification about holidays" +msgstr "Уведомления о начале отпуска" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_hr_leave_type__notify_before_start_holidays +msgid "Notify about start holidays" +msgstr "Уведомлять о начале отпуска?" + +#. module: hr_holidays_ru +#. odoo-javascript +#: code:addons/hr_holidays_ru/static/src/views/calendar/filter_panel/calendar_filter_panel.xml:0 +#, python-format +msgid "Number of days" +msgstr "Количество дней" + +#. module: hr_holidays_ru +#: model:ir.model.fields.selection,name:hr_holidays_ru.selection__holidays_calendar_leaves__time_type__other +msgid "Other" +msgstr "Другое" + +#. module: hr_holidays_ru +#. odoo-javascript +#: code:addons/hr_holidays_ru/static/src/views/calendar/filter_panel/calendar_filter_panel.xml:0 +#, python-format +msgid "Public Holiday" +msgstr "Праздничный день" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar_leaves__name +msgid "Reason" +msgstr "Причина" + +#. module: hr_holidays_ru +#: model:mail.template,subject:hr_holidays_ru.holidays_notification_template +msgid "Reminder of the holidays start" +msgstr "Напоминание о начале отпуска" + +#. module: hr_holidays_ru +#: model:ir.model.fields.selection,name:hr_holidays_ru.selection__holidays_calendar_leaves__type_transfer_day__is_workday +msgid "Rescheduled work day" +msgstr "Перенесенный рабочий день" + +#. module: hr_holidays_ru +#: model:ir.model,name:hr_holidays_ru.model_resource_mixin +msgid "Resource Mixin" +msgstr "Смешанный ресурс" + +#. module: hr_holidays_ru +#: model:ir.model,name:hr_holidays_ru.model_resource_calendar +msgid "Resource Working Time" +msgstr "Рабочее время" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar_leaves__date_from +msgid "Start Date" +msgstr "Дата начала" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_hr_leave_type__request_unit +msgid "Take Leaves in" +msgstr "Отпуск берется в" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar__name +msgid "Template name" +msgstr "Имя шаблона" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/holidays.py:0 +#, python-format +msgid "The date's year %s mismatch the template year" +msgstr "Год даты %s не соответствует году шаблона" + +#. module: hr_holidays_ru +#: model:ir.model.fields,help:hr_holidays_ru.field_hr_leave_type__responsible_employee_to_notify_id +msgid "The employee who needs to be notified" +msgstr "Сотрудник, который должен быть уведомлен" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/holidays.py:0 +#, python-format +msgid "The start date of the leave must be not later than end date." +msgstr "Дата начала отпуска должна быть не позднее даты окончания." + +#. module: hr_holidays_ru +#: model:ir.model,name:hr_holidays_ru.model_hr_leave +msgid "Time Off" +msgstr "Отпуск" + +#. module: hr_holidays_ru +#: model:ir.model,name:hr_holidays_ru.model_hr_leave_allocation +msgid "Time Off Allocation" +msgstr "Распределение отпусков" + +#. module: hr_holidays_ru +#: model:ir.model,name:hr_holidays_ru.model_hr_leave_type +msgid "Time Off Type" +msgstr "Тип отсутсвия" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar_leaves__time_type +msgid "Time Type" +msgstr "Тип времени" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_hr_leave_allocation__type_request_unit +msgid "Type Request Unit" +msgstr "Тип Запрос Единица измерения" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar_leaves__type_transfer_day +msgid "Type of day" +msgstr "Тип дня" + +#. module: hr_holidays_ru +#: model:ir.model.fields,help:hr_holidays_ru.field_holidays_calendar_leaves__type_transfer_day +msgid "" +"Type of day for right calculate count of working hours and vacation days" +msgstr "Тип дня для правильного подсчета рабочих часов и дней отпуска" + +#. module: hr_holidays_ru +#: model_terms:ir.ui.view,arch_db:hr_holidays_ru.open_view_holiday_settings_tree +msgid "Weekends" +msgstr "Выходные" + +#. module: hr_holidays_ru +#: model:ir.model.fields,help:hr_holidays_ru.field_holidays_calendar_leaves__time_type +msgid "" +"Whether this should be computed as a holiday or as work time (eg: formation)" +msgstr "Должен ли он рассчитываться как выходной или как рабочее время" + +#. module: hr_holidays_ru +#: model_terms:ir.ui.view,arch_db:hr_holidays_ru.open_view_holiday_settings_form +#: model_terms:ir.ui.view,arch_db:hr_holidays_ru.open_view_holiday_settings_tree +msgid "Working Time" +msgstr "Рабочее время" + +#. module: hr_holidays_ru +#: model:ir.model.fields.selection,name:hr_holidays_ru.selection__hr_leave_type__request_unit__day +#: model_terms:ir.ui.view,arch_db:hr_holidays_ru.hr_leave_view_kanban_inherit +msgid "Working days" +msgstr "День" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_hr_leave__working_hours_display +msgid "Working hours" +msgstr "Кол-во рабочих часов" + +#. module: hr_holidays_ru +#: model:ir.model.fields,help:hr_holidays_ru.field_hr_leave__working_hours_display +msgid "Working hours amount in selected period" +msgstr "Сумма рабочих часов за выбранный период" + +#. module: hr_holidays_ru +#: model:ir.model.fields,field_description:hr_holidays_ru.field_holidays_calendar__year +msgid "Year" +msgstr "Год" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/hr_leave.py:0 +#: code:addons/hr_holidays_ru/models/hr_leave_allocation.py:0 +#, python-format +msgid "calendar day(s)" +msgstr "кол-во календарных дней" + +#. module: hr_holidays_ru +#. odoo-javascript +#: code:addons/hr_holidays_ru/static/src/views/calendar/filter_panel/calendar_filter_panel.xml:0 +#, python-format +msgid "continuous days:" +msgstr "календарных:" + +#. module: hr_holidays_ru +#: model:ir.model,name:hr_holidays_ru.model_holidays_calendar +msgid "holidays.calendar" +msgstr "" + +#. module: hr_holidays_ru +#: model:ir.model,name:hr_holidays_ru.model_holidays_calendar_leaves +msgid "holidays.calendar.leaves" +msgstr "" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/hr_leave.py:0 +#: code:addons/hr_holidays_ru/models/hr_leave_allocation.py:0 +#, python-format +msgid "hour(s)" +msgstr "кол-во часов" + +#. module: hr_holidays_ru +#. odoo-javascript +#: code:addons/hr_holidays_ru/static/src/views/calendar/filter_panel/calendar_filter_panel.xml:0 +#, python-format +msgid "non-working days and holidays:" +msgstr "вых./праздн.:" + +#. module: hr_holidays_ru +#. odoo-python +#: code:addons/hr_holidays_ru/models/hr_leave.py:0 +#: code:addons/hr_holidays_ru/models/hr_leave_allocation.py:0 +#, python-format +msgid "working day(s)" +msgstr "кол-во рабочих дней" + +#. module: hr_holidays_ru +#. odoo-javascript +#: code:addons/hr_holidays_ru/static/src/views/calendar/filter_panel/calendar_filter_panel.xml:0 +#, python-format +msgid "working days:" +msgstr "кол-во рабочих дней" + +#. module: hr_holidays +#: model:ir.ui.menu,name:hr_holidays.hr_holidays_menu_manager_approve_allocations +msgid "Allocations" +msgstr "Начисления дней отпуска" diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..2ddb478 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from . import hr_leave +from . import hr_leave_allocation +from . import hr_leave_type +from . import holidays +from . import resource diff --git a/models/holidays.py b/models/holidays.py new file mode 100644 index 0000000..ed5eb52 --- /dev/null +++ b/models/holidays.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +import logging +from datetime import datetime, date, timedelta + +from odoo import _, api, models, fields +from odoo.exceptions import ValidationError + +logger = logging.getLogger(__name__) + + +class HolidaysCalendar(models.Model): + _name = "holidays.calendar" + + name = fields.Char(string="Template name", required=True) + year = fields.Selection( + string="Year", + selection="_compute_years_selection", + default=lambda self: str(datetime.today().year), + ) + + global_leave_ids = fields.One2many( + "holidays.calendar.leaves", + "calendar_id", + "Global Leaves", + copy="true", + ) + + @api.returns("self", lambda value: value.id) + def copy(self, default=None): + default = dict(default or {}) + if "name" not in default: + default["name"] = _("%s (Copy)", self.name) + return super().copy(default=default) + + @api.model + def create(self, values): + if values: + self._validate_years(values) + return super(HolidaysCalendar, self).create(values) + + @api.model + def get_all_holidays(self, range_start, range_end): + """The function gets all the holiday dates that are specified in the production calendars for the current displayed year""" + range_start_date = datetime.strptime(range_start, "%Y-%m-%d").date() + range_end_date = datetime.strptime(range_end, "%Y-%m-%d").date() + production_calendars = self.env["holidays.calendar"].search( + [ + ("year", "in", [range_start_date.year, range_end_date.year]), + ] + ) + holidays = [] + non_working_days = [] + rescheduled_work_days = [] + for calendar in production_calendars: + self._separate_holidays( + calendar, + holidays, + non_working_days, + rescheduled_work_days, + range_start_date, + range_end_date, + ) + return holidays, non_working_days, rescheduled_work_days + + def write(self, values=None): + if values: + self._validate_years(values) + return super(HolidaysCalendar, self).write(values) + + @staticmethod + def _check_year(user_date, year): + user_year = ( + datetime.strptime(user_date, "%Y-%m-%d").year + if isinstance(user_date, str) + else user_date.year + ) + if int(year) != user_year: + raise ValidationError( + _("The date's year %s mismatch the template year") % user_year + ) + + @api.onchange("year") + def _change_years(self): + for event in self.global_leave_ids: + event.date_to = date( + year=int(self.year), month=event.date_to.month, day=event.date_to.day + ) + event.date_from = date( + year=int(self.year), + month=event.date_from.month, + day=event.date_from.day, + ) + + @staticmethod + def _compute_years_selection(): + current_year = datetime.today().year + result = [ + ("{}".format(year), "{}".format(year)) + for year in range(current_year - 5, current_year + 10) + ] + return result + + def _fill_day_gaps(self, start_date, end_date): + """The function fills in the date gaps between the beginning and the end of the period""" + current_date = start_date + dates_list = [] + while current_date <= end_date: + dates_list.append(current_date.strftime("%Y-%m-%d")) + current_date += timedelta(days=1) + return dates_list + + def _get_days_from_holidays(self, day, range_start_date, range_end_date): + """The function gets the value of the start date and the end date of the period returns a list of dates in a string representation with filled intervals""" + start_date = day.date_from + end_date = day.date_to + if range_start_date <= start_date and end_date <= range_end_date: + days_list = self._fill_day_gaps(start_date, end_date) + elif range_start_date >= start_date and end_date <= range_end_date: + days_list = self._fill_day_gaps(range_start_date, end_date) + elif range_start_date <= start_date and end_date >= range_end_date: + days_list = self._fill_day_gaps(start_date, range_end_date) + elif range_start_date >= start_date and end_date >= range_end_date: + days_list = self._fill_day_gaps(range_start_date, range_end_date) + else: + days_list = [] + return days_list + + def _separate_holidays( + self, + calendar, + holidays, + non_working_days, + rescheduled_work_days, + range_start_date, + range_end_date, + ): + """The function sorts the dates in the production calendar according to the category of the day""" + days = calendar.global_leave_ids + for day in days: + days_list = self._get_days_from_holidays( + day, range_start_date, range_end_date + ) + if day.type_transfer_day == "is_holiday": + holidays.extend(days_list) + elif day.type_transfer_day == "is_day_off": + non_working_days.extend(days_list) + elif day.type_transfer_day == "is_workday": + rescheduled_work_days.extend(days_list) + + def _validate_years(self, values): + if type(values) != list: + values = [values] + for value in values: + year = value.get("year", self.year) + events = value.get("global_leave_ids", []) + + for event in events: + changes = event[2] + if changes: + if changes.get("date_from"): + self._check_year(changes.get("date_from"), year) + if changes.get("date_to"): + self._check_year(changes.get("date_to"), year) + + +class HolidaysCalendarLeaves(models.Model): + _name = "holidays.calendar.leaves" + _order = "date_from asc" + + name = fields.Char("Reason") + + type_transfer_day = fields.Selection( + [ + ("is_holiday", "Holiday"), + ("is_day_off", "Non-working day"), + ("is_workday", "Rescheduled work day"), + ], + string="Type of day", + required=True, + help="Type of day for right calculate count of working hours and vacation days", + ) + + calendar_id = fields.Many2one("holidays.calendar") + date_from = fields.Date("Start Date", required=True) + date_to = fields.Date("End Date", required=True) + time_type = fields.Selection( + [("leave", "Leave"), ("other", "Other")], + default="leave", + help="Whether this should be computed as a holiday or as work time (eg: formation)", + ) + + @api.constrains("date_from", "date_to") + def check_dates(self): + if self.filtered(lambda leave: leave.date_from > leave.date_to): + raise ValidationError( + _("The start date of the leave must be not later than end date.") + ) diff --git a/models/hr_leave.py b/models/hr_leave.py new file mode 100644 index 0000000..13efeeb --- /dev/null +++ b/models/hr_leave.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- +import logging +from datetime import datetime, timedelta, date + +from odoo import models, fields, api +from odoo.addons.resource.models.utils import HOURS_PER_DAY +from odoo.tools.float_utils import float_round +from odoo.tools.translate import _ + +logger = logging.getLogger(__name__) + +DEFAULT_DAYS = 7 + + +class HolidayRequest(models.Model): + _inherit = "hr.leave" + + def _get_duration(self, check_leave_type=True, resource_calendar=None): + if self.leave_type_request_unit == "c_day": + return self.employee_id.get_all_days_data( + self.date_from, self.date_to, self.holiday_status_id.exclude_holidays + ) + else: + return super()._get_duration(check_leave_type, resource_calendar) + + working_hours_display = fields.Float( + "Working hours", + help="Working hours amount in selected period", + compute="_compute_working_hours_display", + copy=False, + readonly=True, + ) + + @staticmethod + def compute_work_weekends(date_from, date_to, collection): + days = set() + + for event in collection: + if ( + date_from + and event.date_to < date_from.date() + or date_to + and event.date_from > date_to.date() + ): + continue + + start = max(event.date_from, date_from.date()) + until = min(event.date_to, date_to.date()) + + if event.type_transfer_day == "is_workday": + for i in range((until - start + timedelta(1)).days): + today = start + timedelta(i) + days.add(today) + else: + for i in range((until - start + timedelta(1)).days): + today = start + timedelta(i) + # weekends check + if today.weekday() != 5 and today.weekday() != 6: + days.add(today) + return timedelta(len(days)).days + + @api.depends("number_of_days", "date_from", "date_to") + def _compute_working_hours_display(self): + for holiday in self: + calendar = ( + holiday.employee_id.resource_calendar_id + or self.env.user.company_id.resource_calendar_id + ) + count_work_weekends = holiday.compute_work_weekends( + holiday.date_from, + holiday.date_to, + self.env["holidays.calendar.leaves"].search( + [ + ("type_transfer_day", "=", "is_workday"), + ] + ), + ) + count_non_working_days = holiday.compute_work_weekends( + holiday.date_from, + holiday.date_to, + self.env["holidays.calendar.leaves"].search( + [ + ("type_transfer_day", "=", "is_day_off"), + ] + ), + ) + count_holidays = holiday.compute_work_weekends( + holiday.date_from, + holiday.date_to, + self.env["holidays.calendar.leaves"].search( + [ + ("type_transfer_day", "=", "is_holiday"), + ] + ), + ) + + if holiday.date_from and holiday.date_to: + date_start = datetime( + year=holiday.date_from.year, + month=holiday.date_from.month, + day=holiday.date_from.day, + hour=0, + minute=0, + second=0, + ) + date_end = datetime( + year=holiday.date_to.year, + month=holiday.date_to.month, + day=holiday.date_to.day, + hour=23, + minute=59, + second=59, + ) + number_of_hours = calendar.get_work_hours_count( + date_start, date_end, compute_leaves=False + ) + holiday.working_hours_display = number_of_hours + HOURS_PER_DAY * ( + count_work_weekends - count_non_working_days - count_holidays + ) + else: + holiday.working_hours_display = 0 + + @api.depends("number_of_hours_display", "number_of_days_display") + def _compute_duration_display(self): + for leave in self: + amount = ( + float_round(leave.number_of_hours_display, precision_digits=2) + if leave.leave_type_request_unit == "hour" + else float_round(leave.number_of_days_display, precision_digits=2) + ) + if leave.leave_type_request_unit == "c_day": + units = _("calendar day(s)") + elif leave.leave_type_request_unit == "hour": + units = _("hour(s)") + else: + units = _("working day(s)") + leave.duration_display = "%g %s" % (amount, units) + + @staticmethod + def notify_before(leave): + """Checks whether days_before_holidays is set, if it's not, use DEFAULT_DAYS""" + return leave.holiday_status_id.days_before_holidays or DEFAULT_DAYS + + def _check_users_holidays(self): + """ + Method calls every day and checks the planned holidays. + If needed, an email notification to the responsible employee will be sent. + """ + leaves = self.env["hr.leave"].search( + [ + ("holiday_status_id.notify_before_start_holidays", "=", True), + ("holiday_status_id.responsible_employee_to_notify_id", "!=", None), + ("date_from", ">=", date.today()), + ] + ) + + for leave in leaves: + days_before_holidays = self.notify_before(leave) + holidays_start_date = leave.date_from + + if ( + holidays_start_date - timedelta(days=days_before_holidays) + ).date() == date.today(): + template_name = "hr_holidays_ru.holidays_notification_template" + template = self.env.ref(template_name) + template.send_mail(leave.id) + + @staticmethod + def append_number_by_type(leave, res): + if leave.leave_type_request_unit == "c_day": + res.append( + ( + leave.id, + _("%s : %.2f Calendar day(s)") + % ( + leave.name or leave.holiday_status_id.name, + leave.number_of_days, + ), + ) + ) + elif leave.leave_type_request_unit == "hour": + res.append( + ( + leave.id, + _("%s : %.2f hour(s)") + % ( + leave.name or leave.holiday_status_id.name, + leave.number_of_hours_display, + ), + ) + ) + else: + res.append( + ( + leave.id, + _("%s : %.2f Working day(s)") + % ( + leave.name or leave.holiday_status_id.name, + leave.number_of_days, + ), + ) + ) + + @staticmethod + def append_number_by_type_without_short_name(leave, res): + if leave.holiday_type == "company": + target = leave.mode_company_id.name + elif leave.holiday_type == "department": + target = leave.department_id.name + elif leave.holiday_type == "category": + target = leave.category_id.name + else: + target = leave.employee_id.name + + if leave.leave_type_request_unit == "c_day": + res.append( + ( + leave.id, + _("%s on %s : %.2f Calendar day(s)") + % ( + target, + leave.holiday_status_id.name, + leave.number_of_days, + ), + ) + ) + elif leave.leave_type_request_unit == "hour": + res.append( + ( + leave.id, + _("%s on %s : %.2f hour(s)") + % ( + target, + leave.holiday_status_id.name, + leave.number_of_hours_display, + ), + ) + ) + else: + res.append( + ( + leave.id, + _("%s on %s : %.2f Working day(s)") + % ( + target, + leave.holiday_status_id.name, + leave.number_of_days, + ), + ) + ) + + def name_get(self): + res = [] + for leave in self: + if self.env.context.get("short_name"): + self.append_number_by_type(leave, res) + else: + self.append_number_by_type_without_short_name(leave, res) + return res diff --git a/models/hr_leave_allocation.py b/models/hr_leave_allocation.py new file mode 100644 index 0000000..9c8969f --- /dev/null +++ b/models/hr_leave_allocation.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +from odoo import models, api, fields +from odoo.tools.float_utils import float_round +from odoo.tools.translate import _ + + +class HolidaysAllocation(models.Model): + _inherit = "hr.leave.allocation" + + type_request_unit = fields.Selection( + selection_add=[ + ("c_day", "Calendar Days"), + ] + ) + + @api.depends("number_of_hours_display", "number_of_days_display") + def _compute_duration_display(self): + for allocation in self: + amount = ( + float_round(allocation.number_of_hours_display, precision_digits=2) + if allocation.type_request_unit == "hour" + else float_round(allocation.number_of_days_display, precision_digits=2) + ) + if allocation.type_request_unit == "c_day": + units = _("calendar day(s)") + elif allocation.type_request_unit == "hour": + units = _("hour(s)") + else: + units = _("working day(s)") + allocation.duration_display = "%g %s" % (amount, units) diff --git a/models/hr_leave_type.py b/models/hr_leave_type.py new file mode 100644 index 0000000..77f6853 --- /dev/null +++ b/models/hr_leave_type.py @@ -0,0 +1,63 @@ +import logging +from collections import defaultdict +from datetime import time, timedelta +import datetime + +from odoo import api, fields, models +from odoo.tools.translate import _ +from odoo.tools.float_utils import float_round +from odoo.addons.resource.models.utils import Intervals + +_logger = logging.getLogger(__name__) + + +class HolidaysType(models.Model): + _inherit = "hr.leave.type" + + request_unit = fields.Selection( + [ + ("c_day", "Calendar days"), + ("day", "Working days"), + ("hour", "Hours"), + ], + default="c_day", + string="Take Leaves in", + required=True, + ) + exclude_holidays = fields.Boolean( + "Exclude holidays", + help="Check this box to exclude holidays from calendar days amount.", + ) + + notify_before_start_holidays = fields.Boolean( + "Notify about start holidays", + help="Is it necessary to notify the responsible employee before the start of the holidays?", + ) + responsible_employee_to_notify_id = fields.Many2one( + "res.users", + string="Employee to notify", + help="The employee who needs to be notified", + ) + days_before_holidays = fields.Integer( + string="Days before holidays", + help="How many days before it is necessary to notify the responsible employee about the start of the holidays.", + ) + + def name_get(self): + if not self._context.get("employee_id"): + # leave counts is based on employee_id, would be inaccurate if not based on correct employee + return super(HolidaysType, self).name_get() + res = [] + for record in self: + name = record.name + name = "%(name)s (%(count)s)" % { + "name": name, + "count": _("%g remaining ") + % ( + float_round(record.virtual_remaining_leaves, precision_digits=2) + or 0.0, + ) + + (_(" hours") if record.request_unit == "hour" else _(" days")), + } + res.append((record.id, name)) + return res diff --git a/models/resource.py b/models/resource.py new file mode 100644 index 0000000..8bb1ee0 --- /dev/null +++ b/models/resource.py @@ -0,0 +1,390 @@ +# -*- coding: utf-8 -*- +import logging +from datetime import datetime +from datetime import timedelta +from pytz import timezone, utc +from datetime import datetime, time +from collections import defaultdict +from dateutil.relativedelta import relativedelta +import pytz + +from odoo import models, fields +from odoo.addons.resource.models.utils import HOURS_PER_DAY + +_logger = logging.getLogger(__name__) + + +class Employee(models.Model): + _inherit = "hr.employee" + + def _get_consumed_leaves(self, leave_types, target_date=False, ignore_future=False): + employees = self or self._get_contextual_employee() + leaves_domain = [ + ("holiday_status_id", "in", leave_types.ids), + ("employee_id", "in", employees.ids), + ("state", "in", ["confirm", "validate1", "validate"]), + ] + if self.env.context.get("ignored_leave_ids"): + leaves_domain.append( + ("id", "not in", self.env.context.get("ignored_leave_ids")) + ) + + if not target_date: + target_date = fields.Date.today() + if ignore_future: + leaves_domain.append(("date_from", "<=", target_date)) + leaves = self.env["hr.leave"].search(leaves_domain) + leaves_per_employee_type = defaultdict( + lambda: defaultdict(lambda: self.env["hr.leave"]) + ) + for leave in leaves: + leaves_per_employee_type[leave.employee_id][ + leave.holiday_status_id + ] |= leave + + allocations = ( + self.env["hr.leave.allocation"] + .with_context(active_test=False) + .search( + [ + ("employee_id", "in", employees.ids), + ("holiday_status_id", "in", leave_types.ids), + ("state", "=", "validate"), + ] + ) + .filtered(lambda al: al.active or not al.employee_id.active) + ) + allocations_per_employee_type = defaultdict( + lambda: defaultdict(lambda: self.env["hr.leave.allocation"]) + ) + for allocation in allocations: + allocations_per_employee_type[allocation.employee_id][ + allocation.holiday_status_id + ] |= allocation + + # _get_consumed_leaves returns a tuple of two dictionnaries. + # 1) The first is a dictionary to map the number of days/hours of leaves taken per allocation + # The structure is the following: + # - KEYS: + # allocation_leaves_consumed + # |--employee_id + # |--holiday_status_id + # |--allocation + # |--virtual_leaves_taken + # |--leaves_taken + # |--virtual_remaining_leaves + # |--remaining_leaves + # |--max_leaves + # |--accrual_bonus + # - VALUES: + # Integer representing the number of (virtual) remaining leaves, (virtual) leaves taken or max leaves + # for each allocation. + # leaves_taken and remaining_leaves only take into account validated leaves, while the "virtual" equivalent are + # also based on leaves in "confirm" or "validate1" state. + # Accrual bonus gives the amount of additional leaves that will have been granted at the given + # target_date in comparison to today. + # The unit is in hour or days depending on the leave type request unit + # 2) The second is a dictionary mapping the remaining days per employee and per leave type that are either + # not taken into account by the allocations, mainly because accruals don't take future leaves into account. + # This is used to warn the user if the leaves they takes bring them above their available limit. + # - KEYS: + # allocation_leaves_consumed + # |--employee_id + # |--holiday_status_id + # |--to_recheck_leaves + # |--excess_days + # |--exceeding_duration + # - VALUES: + # "to_recheck_leaves" stores every leave that is not yet taken into account by the "allocation_leaves_consumed" dictionary. + # "excess_days" represents the excess amount that somehow isn't taken into account by the first dictionary. + # "exceeding_duration" sum up the to_recheck_leaves duration and compares it to the maximum allocated for that time period. + allocations_leaves_consumed = defaultdict( + lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0))) + ) + + to_recheck_leaves_per_leave_type = defaultdict( + lambda: defaultdict( + lambda: { + "excess_days": defaultdict( + lambda: { + "amount": 0, + "is_virtual": True, + } + ), + "exceeding_duration": 0, + "to_recheck_leaves": self.env["hr.leave"], + } + ) + ) + for allocation in allocations: + allocation_data = allocations_leaves_consumed[allocation.employee_id][ + allocation.holiday_status_id + ][allocation] + future_leaves = 0 + if allocation.allocation_type == "accrual": + future_leaves = allocation._get_future_leaves_on(target_date) + max_leaves = ( + allocation.number_of_hours_display + if allocation.type_request_unit in ["hour"] + else allocation.number_of_days_display + ) + max_leaves += future_leaves + allocation_data.update( + { + "max_leaves": max_leaves, + "accrual_bonus": future_leaves, + "virtual_remaining_leaves": max_leaves, + "remaining_leaves": max_leaves, + "leaves_taken": 0, + "virtual_leaves_taken": 0, + } + ) + + for employee in employees: + for leave_type in leave_types: + allocations_with_date_to = self.env["hr.leave.allocation"] + allocations_without_date_to = self.env["hr.leave.allocation"] + for leave_allocation in allocations_per_employee_type[employee][ + leave_type + ]: + if leave_allocation.date_to: + allocations_with_date_to |= leave_allocation + else: + allocations_without_date_to |= leave_allocation + sorted_leave_allocations = ( + allocations_with_date_to.sorted(key="date_to") + + allocations_without_date_to + ) + + if leave_type.request_unit in ["c_day", "day", "half_day"]: + leave_duration_field = "number_of_days" + leave_unit = "days" + else: + leave_duration_field = "number_of_hours_display" + leave_unit = "hours" + + leave_type_data = allocations_leaves_consumed[employee][leave_type] + for leave in leaves_per_employee_type[employee][leave_type].sorted( + "date_from" + ): + leave_duration = leave[leave_duration_field] + skip_excess = False + + if ( + sorted_leave_allocations.filtered( + lambda alloc: alloc.allocation_type == "accrual" + ) + and leave.date_from.date() > target_date + ): + to_recheck_leaves_per_leave_type[employee][leave_type][ + "to_recheck_leaves" + ] |= leave + skip_excess = True + continue + + if leave_type.requires_allocation == "yes": + for allocation in sorted_leave_allocations: + # We don't want to include future leaves linked to accruals into the total count of available leaves. + # However, we'll need to check if those leaves take more than what will be accrued in total of those days + # to give a warning if the total exceeds what will be accrued. + if allocation.date_from > leave.date_to.date() or ( + allocation.date_to + and allocation.date_to < leave.date_from.date() + ): + continue + interval_start = max( + leave.date_from, + datetime.combine(allocation.date_from, time.min), + ) + interval_end = min( + leave.date_to, + datetime.combine(allocation.date_to, time.max) + if allocation.date_to + else leave.date_to, + ) + duration = leave[leave_duration_field] + if ( + leave.date_from != interval_start + or leave.date_to != interval_end + ): + duration_info = employee._get_calendar_attendances( + interval_start.replace(tzinfo=pytz.UTC), + interval_end.replace(tzinfo=pytz.UTC), + ) + duration = duration_info[ + "hours" if leave_unit == "hours" else "days" + ] + max_allowed_duration = min( + duration, + leave_type_data[allocation]["virtual_remaining_leaves"], + ) + + if not max_allowed_duration: + continue + + allocated_time = min(max_allowed_duration, leave_duration) + leave_type_data[allocation][ + "virtual_leaves_taken" + ] += allocated_time + leave_type_data[allocation][ + "virtual_remaining_leaves" + ] -= allocated_time + if leave.state == "validate": + leave_type_data[allocation][ + "leaves_taken" + ] += allocated_time + leave_type_data[allocation][ + "remaining_leaves" + ] -= allocated_time + + leave_duration -= allocated_time + if not leave_duration: + break + if round(leave_duration, 2) > 0 and not skip_excess: + to_recheck_leaves_per_leave_type[employee][leave_type][ + "excess_days" + ][leave.date_to.date()] = { + "amount": leave_duration, + "is_virtual": leave.state != "validate", + "leave_id": leave.id, + } + else: + if leave_unit == "hours": + allocated_time = leave.number_of_hours_display + else: + allocated_time = leave.number_of_days_display + leave_type_data[False]["virtual_leaves_taken"] += allocated_time + leave_type_data[False]["virtual_remaining_leaves"] = 0 + leave_type_data[False]["remaining_leaves"] = 0 + if leave.state == "validate": + leave_type_data[False]["leaves_taken"] += allocated_time + + for employee in to_recheck_leaves_per_leave_type: + for leave_type in to_recheck_leaves_per_leave_type[employee]: + content = to_recheck_leaves_per_leave_type[employee][leave_type] + consumed_content = allocations_leaves_consumed[employee][leave_type] + if content["to_recheck_leaves"]: + date_to_simulate = max( + content["to_recheck_leaves"].mapped("date_from") + ).date() + latest_accrual_bonus = 0 + date_accrual_bonus = 0 + virtual_remaining = 0 + additional_leaves_duration = 0 + for allocation in consumed_content: + latest_accrual_bonus += ( + allocation + and allocation._get_future_leaves_on(date_to_simulate) + ) + date_accrual_bonus += consumed_content[allocation][ + "accrual_bonus" + ] + virtual_remaining += consumed_content[allocation][ + "virtual_remaining_leaves" + ] + for leave in content["to_recheck_leaves"]: + additional_leaves_duration += ( + leave.number_of_hours + if leave_type.request_unit == "hours" + else leave.number_of_days + ) + latest_remaining = ( + virtual_remaining - date_accrual_bonus + latest_accrual_bonus + ) + content["exceeding_duration"] = round( + min(0, latest_remaining - additional_leaves_duration), 2 + ) + + return (allocations_leaves_consumed, to_recheck_leaves_per_leave_type) + + +class ResourceMixin(models.AbstractModel): + _inherit = "resource.mixin" + + def get_all_days_data(self, date_from, date_to, exclude_holidays): + """ + Returns days amount as float, like 'get_work_days_data()' + """ + # Set timezone if no timezone is explicitly given + user_tz = timezone(self.env.context.get("tz") or self.tz) + if not user_tz: + user_tz = utc + if not date_from.tzinfo: + date_from = user_tz.localize(date_from) + if not date_to.tzinfo: + date_to = user_tz.localize(date_to) + # Bring booth dates to the same timezone + if date_from.tzinfo != date_to.tzinfo: + date_to = date_to.astimezone(date_from.tzinfo) + # Handle holidays in selected period + holidays_dur = timedelta(0) + if exclude_holidays: + holidays_dur = ( + self.env.user.company_id.resource_calendar_id.get_holidays_duration( + date_from, date_to + ) + ) + duration = date_to.date() - date_from.date() + timedelta(1) - holidays_dur + days = float(duration.days) + hours = days * HOURS_PER_DAY + return (days, hours) + + +class ResourceCalendar(models.Model): + _inherit = "resource.calendar" + + def get_holidays_duration(self, start_dt, end_dt): + """ + Returns timedelta object which is duration of all holidays included into + specified period. + """ + days = set() + holidays_ids = self.env["holidays.calendar.leaves"].search( + [("type_transfer_day", "=", "is_holiday")] + ) + + for holiday in holidays_ids: + # Transformation date to datetime + date_from = datetime( + year=holiday.date_from.year, + month=holiday.date_from.month, + day=holiday.date_from.day, + hour=0, + minute=0, + second=0, + ) + date_to = datetime( + year=holiday.date_to.year, + month=holiday.date_to.month, + day=holiday.date_to.day, + hour=23, + minute=59, + second=59, + ) + # Dates in database are without timezones, and time in fact for UTC timezone, + # so it should be added explicitly. + holiday_date_from = utc.localize(date_from) if date_from else None + holiday_date_to = utc.localize(date_to) if date_to else None + # Bring booth dates to the holiday timezone + if holiday_date_from and start_dt.tzinfo != holiday_date_from.tzinfo: + start_dt = start_dt.astimezone(holiday_date_from.tzinfo) + if holiday_date_to and end_dt.tzinfo != holiday_date_to.tzinfo: + end_dt = end_dt.astimezone(holiday_date_to.tzinfo) + # Check periods intersection + if ( + holiday_date_from + and end_dt < holiday_date_from + or holiday_date_to + and start_dt > holiday_date_to + ): + continue + # Handle this holiday + start = start_dt.date() + if holiday_date_from: + start = max(start, holiday_date_from.date()) + until = end_dt.date() + if holiday_date_to: + until = min(until, holiday_date_to.date()) + for i in range((until - start + timedelta(1)).days): + days.add(start + timedelta(i)) + return timedelta(len(days)) diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..c54e9c2 --- /dev/null +++ b/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_holidays_calendar_manager,access.holidays.calendar.manager,model_holidays_calendar,hr_holidays.group_hr_holidays_manager,1,1,1,1 +access_holidays_calendar_leaves_manager,access.holidays.calendar.leaves.manager,model_holidays_calendar_leaves,hr_holidays.group_hr_holidays_manager,1,1,1,1 +access_holidays_calendar_user,access.holidays.calendar.user,model_holidays_calendar,base.group_user,1,0,0,0 +access_holidays_calendar_leaves_user,access.holidays.calendar.leaves.user,model_holidays_calendar_leaves,base.group_user,1,0,0,0 \ No newline at end of file diff --git a/static/src/css/calendar_renderer.css b/static/src/css/calendar_renderer.css new file mode 100644 index 0000000..a4be27d --- /dev/null +++ b/static/src/css/calendar_renderer.css @@ -0,0 +1,63 @@ +.fc-today-day-number-year { + position: relative; + color: black !important; +} +.fc-today-day-number-year::before { + content: "\f1db"; + position: absolute; + font-family: FontAwesome; + font-size: 2.1em; + color: red; + line-height: 0.75em; + left: 50%; + transform: translateX(-50%); +} + +.fc-today-day-number-month { + position: relative; + color: black !important; +} + +.fc-today-day-number-month::before { + content: "\f1db"; + position: absolute; + font-family: FontAwesome; + font-size: 2em; + color: red; + line-height: 0.8em; + left: 50%; + transform: translateX(-50%); +} + +.o_holidays_legend { + vertical-align: middle; + background-color: #e9ecef; + font-weight: 600; + display: inline-block; + width: 24px; + height: 30px; + margin: 0 3px; + padding: 3px 0; + text-align: center; + color: red; +} + +.o_non_working_legend { + vertical-align: middle; + background-color: #e9ecef; + display: inline-block; + width: 24px; + height: 30px; + margin: 0 3px; + padding: 3px 0; + text-align: center; + color: black; +} + +.o_holidays_week_days { + color: red; +} +.hr_holiday_calendar { + font-weight: bold; + color: red !important; +} diff --git a/static/src/views/calendar/common/calendar_common_renderer.js b/static/src/views/calendar/common/calendar_common_renderer.js new file mode 100644 index 0000000..ea2ec47 --- /dev/null +++ b/static/src/views/calendar/common/calendar_common_renderer.js @@ -0,0 +1,71 @@ +/** @odoo-module **/ + +import { patch } from '@web/core/utils/patch' + +import { TimeOffCalendarCommonRenderer } from '@hr_holidays/views/calendar/common/calendar_common_renderer' +import { onWillStart } from '@odoo/owl' +import { useService } from '@web/core/utils/hooks' +import { serializeDate } from '@web/core/l10n/dates' + +patch( + TimeOffCalendarCommonRenderer.prototype, + { + /** + * Adding onWillStart to load data from the holidays.calendar model to get holidays, non-working days and rescheduled working days. + */ + setup() { + super.setup(...arguments) + this.orm = useService('orm') + onWillStart(async () => { + const rangeStart = serializeDate( + this.props.model.rangeStart, + 'datetime' + ) + const rangeEnd = serializeDate(this.props.model.rangeEnd, 'datetime') + const [holidaysList, nonWorkingDaysList, rescheduledWorkDaysList] = + await this.orm.call('holidays.calendar', 'get_all_holidays', [ + rangeStart, + rangeEnd + ]) + this.holidays = holidaysList + this.nonWorkingDays = nonWorkingDaysList + this.rescheduledWorkDays = rescheduledWorkDaysList + }) + }, + + /** + * Redefining onDayRender, adding additional functionality to it to display holidays, non-working days and postponed working days. + */ + onDayRender(info) { + const date = luxon.DateTime.fromJSDate(info.date).toISODate(); + if (this.props.model.unusualDays.includes(date)) { + info.el.classList.add("o_calendar_disabled"); + } + // New code + if (this.nonWorkingDays.includes(date)) { + info.el.classList.add('o_calendar_disabled') + } + const element = document.querySelector( + `.fc-content-skeleton [data-date=${CSS.escape(date)}]` + ) + if (element && element.classList.contains('fc-today')) { + element.classList.remove('fc-today') + element.classList.add('fc-today-day-number-month') + } + if (this.holidays.includes(date)) { + if (element) { + const day_number = element.querySelector('.fc-day-number') + day_number.classList.add('hr_holiday_calendar') + info.el.classList.add("o_calendar_disabled"); + } else { + const days_in_week_element = document.querySelector(`.fc-head-container [data-date=${CSS.escape(date)}]`) + info.el.classList.add("o_calendar_disabled"); + days_in_week_element.classList.add('o_holidays_week_days') + } + } + if (this.rescheduledWorkDays.includes(date)) { + info.el.classList.remove('o_calendar_disabled') + } + } + } +) diff --git a/static/src/views/calendar/filter_panel/calendar_filter_panel.js b/static/src/views/calendar/filter_panel/calendar_filter_panel.js new file mode 100644 index 0000000..0a70ca7 --- /dev/null +++ b/static/src/views/calendar/filter_panel/calendar_filter_panel.js @@ -0,0 +1,122 @@ +/** @odoo-module **/ + +import { patch } from '@web/core/utils/patch' + +import { TimeOffCalendarFilterPanel } from '@hr_holidays/views/calendar/filter_panel/calendar_filter_panel' +import { serializeDate } from '@web/core/l10n/dates' +import { onWillStart, onWillUpdateProps } from '@odoo/owl' + +patch(TimeOffCalendarFilterPanel.prototype, { + /** + * Add onWillStart to load data from the holidays.calendar model to the dates displayed in the calendar, add onWillUpdateProps to update data when switching month, week or year + */ + setup() { + super.setup(...arguments) + onWillStart(this.countHolidays) + onWillUpdateProps(this.countHolidays) + }, + + /** + * The function counts the number of working days and days off + */ + async countHolidays() { + const rangeStart = this.props.model.rangeStart + const rangeEnd = this.props.model.rangeEnd + const firstDay = await this.getFirstMonthDay(rangeStart, rangeEnd) + if (!this.weekScale) { + const lastDay = await this.getLastMonthDay(firstDay, rangeStart, rangeEnd) + this.daysTotalAmount = + (new Date(lastDay) - new Date(firstDay)) / (1000 * 60 * 60 * 24) + 1 + const unusualDays = this.getUnusualDaysList(firstDay, lastDay) + await this.getAllHolidays(firstDay, lastDay) + const nonWorkingDaysAndHolidaysSet = new Set([ + ...unusualDays, + ...this.nonWorkingDays, + ...this.holidays + ]) + const nonWorkingDaysAndHolidaysSetWithoutRescheduled = new Set( + [...nonWorkingDaysAndHolidaysSet].filter( + value => !new Set(this.rescheduledWorkDays).has(value) + ) + ) + this.workingDaysAmount = + this.daysTotalAmount - + nonWorkingDaysAndHolidaysSetWithoutRescheduled.size + this.nonWorkingDaysAndHolidays = + nonWorkingDaysAndHolidaysSetWithoutRescheduled.size + } + }, + + /** + * The function counts the number of working days, holidays and non-working days + */ + async getAllHolidays(firstDay, lastDay) { + const [holidaysList, nonWorkingDaysList, rescheduledWorkDaysList] = + await this.orm.call('holidays.calendar', 'get_all_holidays', [ + firstDay, + lastDay + ]) + this.holidays = holidaysList + this.nonWorkingDays = nonWorkingDaysList + this.rescheduledWorkDays = rescheduledWorkDaysList + }, + + /** + * The function defines the first day of the month + */ + getFirstMonthDay(rangeStart, rangeEnd) { + if ((rangeEnd - rangeStart) / (1000 * 60 * 60 * 24) > 7) { + this.weekScale = false + let newRangeStartMonth = 1 + let newRangeStartYear = rangeStart.year + if (rangeStart.month + 1 <= 12) { + newRangeStartMonth = + rangeStart.month + 1 > 9 + ? `${rangeStart.month + 1}` + : `0${rangeStart.month + 1}` + } else { + newRangeStartMonth = '01' + newRangeStartYear += 1 + } + return rangeStart.day === 1 + ? serializeDate(rangeStart, 'datetime') + : `${newRangeStartYear}-${newRangeStartMonth}-01` + } else { + this.weekScale = true + } + }, + + /** + * The function determines the last day of the month + */ + getLastMonthDay(firstDay, rangeStart, rangeEnd) { + let lastDay = new Date(firstDay) + // 42 is the number of days displayed on the calendar on a monthly scale. + // In this case, we check that the scale of the year is displayed + if ((rangeEnd - rangeStart) / (1000 * 60 * 60 * 24) > 42) { + return serializeDate(rangeEnd, 'datetime') + } else { + lastDay.setMonth(lastDay.getMonth() + 1) + lastDay.setDate(0) + const newLastDayMonth = + lastDay.getMonth() + 1 > 9 + ? `${lastDay.getMonth() + 1}` + : `0${lastDay.getMonth() + 1}` + return `${lastDay.getFullYear()}-${newLastDayMonth}-${lastDay.getDate()}` + } + }, + + /** + * The function gets a list of days off in a given period + */ + getUnusualDaysList(firstDay, lastDay) { + const newFirstDay = new Date(firstDay) + const newLastDay = new Date(lastDay) + const unusualDaysList = this.props.model.unusualDays + const unusualDaysFiltered = unusualDaysList.filter(day => { + const newDate = new Date(day) + return (newDate >= newFirstDay) & (newDate <= newLastDay) + }) + return unusualDaysFiltered + } +}) diff --git a/static/src/views/calendar/filter_panel/calendar_filter_panel.xml b/static/src/views/calendar/filter_panel/calendar_filter_panel.xml new file mode 100644 index 0000000..64087d6 --- /dev/null +++ b/static/src/views/calendar/filter_panel/calendar_filter_panel.xml @@ -0,0 +1,38 @@ + + + + + + 13 Public Holiday + + + + 13 Non-working day + + + + + + + + + +
+
Number of days
+
    +
  • +

    + continuous days: +

    +

    + working days: +

    +

    + non-working days and holidays: +

    +
  • +
+
+
+
+
\ No newline at end of file diff --git a/static/src/views/calendar/year/calendar_year_renderer.js b/static/src/views/calendar/year/calendar_year_renderer.js new file mode 100644 index 0000000..502edc7 --- /dev/null +++ b/static/src/views/calendar/year/calendar_year_renderer.js @@ -0,0 +1,66 @@ +/** @odoo-module **/ + +import { patch } from '@web/core/utils/patch' + +import { TimeOffCalendarYearRenderer } from '@hr_holidays/views/calendar/year/calendar_year_renderer' +import { onWillStart } from '@odoo/owl' +import { serializeDate } from '@web/core/l10n/dates' + +patch( + TimeOffCalendarYearRenderer.prototype, + { + /** + * We add onWillStart to load data from the holidays.calendar model to get holidays, non-working days and rescheduled working days. + */ + setup() { + super.setup(...arguments) + onWillStart(async () => { + const rangeStart = serializeDate( + this.props.model.rangeStart, + 'datetime' + ) + const rangeEnd = serializeDate(this.props.model.rangeEnd, 'datetime') + const [holidaysList, nonWorkingDaysList, rescheduledWorkDaysList] = + await this.orm.call('holidays.calendar', 'get_all_holidays', [ + rangeStart, + rangeEnd + ]) + this.holidays = holidaysList + this.nonWorkingDays = nonWorkingDaysList + this.rescheduledWorkDay = rescheduledWorkDaysList + }) + }, + + /** + * Redefining onDayRender, adding additional functionality to it to display holidays, non-working days and postponed working days. + */ + onDayRender(info) { + const date = luxon.DateTime.fromJSDate(info.date).toISODate() + if (this.props.model.unusualDays.includes(date)) { + info.el.classList.add('o_calendar_disabled') + } + + // New code + if (this.nonWorkingDays.includes(date)) { + info.el.classList.add('o_calendar_disabled') + } + const element = document.querySelector( + `.fc-content-skeleton [data-date=${CSS.escape(date)}]` + ) + if (element && element.classList.contains('fc-today')) { + element.classList.remove('fc-today') + element.classList.add('fc-today-day-number-year') + } + if (this.holidays.includes(date)) { + if (element) { + const day_number = element.querySelector('.fc-day-number') + day_number.classList.add('hr_holiday_calendar') + info.el.classList.add("o_calendar_disabled"); + } + } + if (this.rescheduledWorkDay.includes(date)) { + info.el.classList.remove('o_calendar_disabled') + } + } + } +) diff --git a/views/hr_holidays_menus_view.xml b/views/hr_holidays_menus_view.xml new file mode 100644 index 0000000..9dd39bb --- /dev/null +++ b/views/hr_holidays_menus_view.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/views/hr_holidays_views.xml b/views/hr_holidays_views.xml new file mode 100644 index 0000000..c9940f6 --- /dev/null +++ b/views/hr_holidays_views.xml @@ -0,0 +1,170 @@ + + + + + + + hr.holidays.settings_tree + holidays.calendar + + + + + + + + + + hr.holidays.settings_form + holidays.calendar + +
+ +

+ +

+ + + + + + + + + + + + + + + +
+
+
+
+ + + + Holidays and non-working days + holidays.calendar + ir.actions.act_window + tree,form + + + + + hr_holidays_ru.hr_leave_view_form_inherit + hr.leave + + + +
+
+ + Hours +
+
+ + Calendar days +
+
+ + Working days +
+
+
+ + + + + + + + + + +
+
+ + + + hr_holidays_ru.hr_leave_view_kanban_inherit + hr.leave + + + + + + Calendar days + + + Working days + + + + + + + hr_holidays_ru.hr_leave_allocation_view_form_inherit + hr.leave.allocation + + + +
+ + + Calendar days + Working days + Hours +
+
+
+
+ + + + + hr_holidays_ru.edit_holiday_status_form_inherit + hr.leave.type + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/views/hr_leave_allocation_views.xml b/views/hr_leave_allocation_views.xml new file mode 100644 index 0000000..c9eccfa --- /dev/null +++ b/views/hr_leave_allocation_views.xml @@ -0,0 +1,7 @@ + + + + {'search_default_approve': 1} + + \ No newline at end of file diff --git a/views/hr_leave_report_calendar_view.xml b/views/hr_leave_report_calendar_view.xml new file mode 100644 index 0000000..4d225b9 --- /dev/null +++ b/views/hr_leave_report_calendar_view.xml @@ -0,0 +1,7 @@ + + + + {'hide_employee_name': 1, 'search_default_department': 1} + + \ No newline at end of file diff --git a/views/hr_leave_views.xml b/views/hr_leave_views.xml new file mode 100644 index 0000000..36543cb --- /dev/null +++ b/views/hr_leave_views.xml @@ -0,0 +1,50 @@ + + + + hr.leave.view.form.inh + hr.leave + + + + + + + + +
+
+
+ + Days +
+
+ + Days +
+
+ +
+
+
+
+
+
+ + + { + 'hide_employee_name': 1, + 'holiday_status_name_get': False} + + + + hr.leave.view.tree.inh + hr.leave + + + + + + + +
\ No newline at end of file diff --git a/wizard/__init__.py b/wizard/__init__.py new file mode 100644 index 0000000..8a88df7 --- /dev/null +++ b/wizard/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import hr_holidays_summary_department diff --git a/wizard/hr_holidays_summary_department.py b/wizard/hr_holidays_summary_department.py new file mode 100644 index 0000000..6b5a66f --- /dev/null +++ b/wizard/hr_holidays_summary_department.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- + +from datetime import timedelta +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class HrHolidaySummaryReport(models.AbstractModel): + _inherit = "report.hr_holidays.report_holidayssummary" + + def _get_header_info(self, start_date, end_date, holiday_type): + st_date = fields.Date.from_string(start_date) + end_date_ = ( + fields.Date.from_string(end_date) + if end_date + else fields.Date.from_string(start_date + relativedelta(days=59)) + ) + return { + "start_date": fields.Date.to_string(st_date), + "end_date": fields.Date.to_string(end_date_), + "holiday_type": "Confirmed and Approved" + if holiday_type == "both" + else holiday_type, + } + + def _get_months(self, start_date, end_date): + # it works for geting month name between two dates. + res = [] + start_date = fields.Date.from_string(start_date) + # end_date = start_date + relativedelta(days=59) + end_date_ = ( + fields.Date.from_string(end_date) + if end_date + else fields.Date.from_string(start_date + relativedelta(days=59)) + ) + while start_date <= end_date_: + last_date = start_date + relativedelta(day=1, months=+1, days=-1) + if last_date > end_date_: + last_date = end_date_ + month_days = (last_date - start_date).days + 1 + res.append({"month_name": start_date.strftime("%B"), "days": month_days}) + start_date += relativedelta(day=1, months=+1) + return res + + def _get_day(self, start_date, end_date): + res = [] + start_date = fields.Date.from_string(start_date) + end_date_ = fields.Date.from_string(end_date) + for x in range((end_date_ - start_date).days + 1): + color = "#ababab" if self._date_is_day_off(start_date) else "" + res.append( + { + "day_str": start_date.strftime("%a"), + "day": start_date.day, + "color": color, + } + ) + start_date = start_date + relativedelta(days=1) + return res + + def _get_data_from_report(self, data): + res = [] + Employee = self.env["hr.employee"] + if "depts" in data: + for department in self.env["hr.department"].browse(data["depts"]): + res.append( + { + "dept": department.name, + "data": [], + "color": self._get_day(data["date_from"], data["date_to"]), + } + ) + for emp in Employee.search([("department_id", "=", department.id)]): + res[len(res) - 1]["data"].append( + { + "emp": emp.name, + "display": self._get_leaves_summary( + data["date_from"], + data["date_to"], + emp.id, + data["holiday_type"], + ), + "sum": self.sum, + } + ) + elif "emp" in data: + res.append({"data": []}) + for emp in Employee.browse(data["emp"]): + res[0]["data"].append( + { + "emp": emp.name, + "display": self._get_leaves_summary( + data["date_from"], + data["date_to"], + emp.id, + data["holiday_type"], + ), + "sum": self.sum, + } + ) + return res + + def _get_leaves_summary(self, start_date, end_date, empid, holiday_type): + res = [] + count = 0 + start_date = fields.Date.from_string(start_date) + end_date_ = ( + fields.Date.from_string(end_date) + if end_date + else fields.Date.from_string(start_date + relativedelta(days=59)) + ) + for index in range((end_date_ - start_date).days + 1): + current = start_date + timedelta(index) + res.append({"day": current.day, "color": ""}) + if self._date_is_day_off(current): + res[index]["color"] = "#ababab" + # count and get leave summary details. + holiday_type = ( + ["confirm", "validate"] + if holiday_type == "both" + else ["confirm"] + if holiday_type == "Confirmed" + else ["validate"] + ) + holidays = self.env["hr.leave"].search( + [ + ("employee_id", "=", empid), + ("state", "in", holiday_type), + ("date_from", "<=", str(end_date_)), + ("date_to", ">=", str(start_date)), + ] + ) + for holiday in holidays: + # Convert date to user timezone, otherwise the report will not be consistent with the + # value displayed in the interface. + date_from = fields.Datetime.from_string(holiday.date_from) + date_from = fields.Datetime.context_timestamp(holiday, date_from).date() + date_to = fields.Datetime.from_string(holiday.date_to) + date_to = fields.Datetime.context_timestamp(holiday, date_to).date() + for index in range(0, ((date_to - date_from).days + 1)): + if date_from >= start_date and date_from <= end_date_: + res[(date_from - start_date).days][ + "color" + ] = holiday.holiday_status_id.color_name + date_from += timedelta(1) + count += holiday.number_of_days + self.sum = count + return res + + @api.model + def _get_report_values(self, docids, data=None): + if not data.get("form"): + raise UserError( + _("Form content is missing, this report cannot be printed.") + ) + + holidays_report = self.env["ir.actions.report"]._get_report_from_name( + "hr_holidays.report_holidayssummary" + ) + holidays = self.env["hr.leave"].browse(self.ids) + return { + "doc_ids": self.ids, + "doc_model": holidays_report.model, + "docs": holidays, + "get_header_info": self._get_header_info( + data["form"]["date_from"], + data["form"]["date_to"], + data["form"]["holiday_type"], + ), + "get_day": self._get_day( + data["form"]["date_from"], data["form"]["date_to"] + ), + "get_months": self._get_months( + data["form"]["date_from"], data["form"]["date_to"] + ), + "get_data_from_report": self._get_data_from_report(data["form"]), + "get_holidays_status": self._get_holidays_status(), + }