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
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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(),
+ }