This commit is contained in:
parent b458d7e298
commit 9c5b162283
237 changed files with 351632 additions and 0 deletions

18
__init__.py Normal file
View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
from . import populate
from . import report
from . import wizard
from odoo import api, SUPERUSER_ID
def _hr_holiday_post_init(env):
french_companies = env['res.company'].search_count([('partner_id.country_id.code', '=', 'FR')])
if french_companies:
env['ir.module.module'].search([
('name', '=', 'l10n_fr_hr_work_entry_holidays'),
('state', '=', 'uninstalled')
]).sudo().button_install()

87
__manifest__.py Normal file
View File

@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Time Off',
'version': '1.6',
'category': 'Human Resources/Time Off',
'sequence': 85,
'summary': 'Allocate PTOs and follow leaves requests',
'website': 'https://www.odoo.com/app/time-off',
'description': """
Manage time off requests and allocations
=====================================
This application controls the time off schedule of your company. It allows employees to request time off. Then, managers can review requests for time off and approve or reject them. This way you can control the overall time off planning for the company or department.
You can configure several kinds of time off (sickness, paid days, ...) and allocate time off to an employee or department quickly using time off allocation. An employee can also make a request for more days off by making a new time off allocation. It will increase the total of available days for that time off type (if the request is accepted).
You can keep track of time off in different ways by following reports:
* Time Off Summary
* Time Off by Department
* Time Off Analysis
A synchronization with an internal agenda (Meetings of the CRM module) is also possible in order to automatically create a meeting when a time off request is accepted by setting up a type of meeting in time off Type.
""",
'depends': ['hr', 'calendar', 'resource'],
'data': [
'data/report_paperformat.xml',
'data/mail_activity_type_data.xml',
'data/mail_message_subtype_data.xml',
'data/hr_holidays_data.xml',
'data/ir_cron_data.xml',
'security/hr_holidays_security.xml',
'security/ir.model.access.csv',
'views/resource_views.xml',
'views/hr_leave_views.xml',
'views/hr_leave_type_views.xml',
'views/hr_leave_allocation_views.xml',
'views/hr_leave_accrual_views.xml',
'views/hr_leave_mandatory_day_views.xml',
'views/mail_activity_views.xml',
'wizard/hr_holidays_cancel_leave_views.xml',
'wizard/hr_holidays_summary_employees_views.xml',
'wizard/hr_departure_wizard_views.xml',
'report/hr_holidays_templates.xml',
'report/hr_holidays_reports.xml',
'report/hr_leave_reports.xml',
'report/hr_leave_report_calendar.xml',
'report/hr_leave_employee_type_report.xml',
'views/hr_views.xml',
'views/hr_holidays_views.xml',
],
'demo': [
'data/hr_holidays_demo.xml',
],
'installable': True,
'application': True,
'assets': {
'web.assets_backend': [
'hr_holidays/static/src/**/*',
# Don't include dark mode files in light mode
('remove', 'hr_holidays/static/src/**/*.dark.scss'),
],
"web.assets_web_dark": [
'hr_holidays/static/src/**/*.dark.scss',
],
'web.tests_assets': [
'hr_holidays/static/tests/helpers/**/*',
],
'web.qunit_suite_tests': [
'hr_holidays/static/tests/**/*.js',
('remove', 'hr_holidays/static/tests/tours/**/*'),
('remove', 'hr_holidays/static/tests/helpers/**/*'),
],
'web.assets_tests': [
'/hr_holidays/static/tests/tours/**/*'
],
},
'post_init_hook': '_hr_holiday_post_init',
'license': 'LGPL-3',
}

2
controllers/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*
from . import main

48
controllers/main.py Normal file
View File

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mail.controllers.mail import MailController
from odoo import http
class HrHolidaysController(http.Controller):
@http.route('/leave/validate', type='http', auth='user', methods=['GET'])
def hr_holidays_request_validate(self, res_id, token):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('hr.leave', int(res_id), token)
if comparison and record:
try:
record.action_approve()
except Exception:
return MailController._redirect_to_messaging()
return redirect
@http.route('/leave/refuse', type='http', auth='user', methods=['GET'])
def hr_holidays_request_refuse(self, res_id, token):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('hr.leave', int(res_id), token)
if comparison and record:
try:
record.action_refuse()
except Exception:
return MailController._redirect_to_messaging()
return redirect
@http.route('/allocation/validate', type='http', auth='user', methods=['GET'])
def hr_holidays_allocation_validate(self, res_id, token):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('hr.leave.allocation', int(res_id), token)
if comparison and record:
try:
record.action_approve()
except Exception:
return MailController._redirect_to_messaging()
return redirect
@http.route('/allocation/refuse', type='http', auth='user', methods=['GET'])
def hr_holidays_allocation_refuse(self, res_id, token):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('hr.leave.allocation', int(res_id), token)
if comparison and record:
try:
record.action_refuse()
except Exception:
return MailController._redirect_to_messaging()
return redirect

315
data/hr_holidays_data.xml Normal file
View File

@ -0,0 +1,315 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="icon_1" model="ir.attachment">
<field name="name">Annual_Time_Off.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Annual_Time_Off.svg</field>
</record>
<record id="icon_2" model="ir.attachment">
<field name="name">Annual_Time_Off_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Annual_Time_Off_2.svg</field>
</record>
<record id="icon_3" model="ir.attachment">
<field name="name">Annual_Time_Off_3.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Annual_Time_Off_3.svg</field>
</record>
<record id="icon_4" model="ir.attachment">
<field name="name">Compensatory_Time_Off.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Compensatory_Time_Off.svg</field>
</record>
<record id="icon_5" model="ir.attachment">
<field name="name">Compensatory_Time_Off_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Compensatory_Time_Off_2.svg</field>
</record>
<record id="icon_6" model="ir.attachment">
<field name="name">Compensatory_Time_Off_3.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Compensatory_Time_Off_3.svg</field>
</record>
<record id="icon_7" model="ir.attachment">
<field name="name">Credit_Time.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Credit_Time.svg</field>
</record>
<record id="icon_8" model="ir.attachment">
<field name="name">Credit_Time_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Credit_Time_2.svg</field>
</record>
<record id="icon_9" model="ir.attachment">
<field name="name">Extra_Time_Off.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Extra_Time_Off.svg</field>
</record>
<record id="icon_10" model="ir.attachment">
<field name="name">Extra_Time_Off_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Extra_Time_Off_2.svg</field>
</record>
<record id="icon_11" model="ir.attachment">
<field name="name">Maternity_Time_Off.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Maternity_Time_Off.svg</field>
</record>
<record id="icon_12" model="ir.attachment">
<field name="name">Maternity_Time_Off_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Maternity_Time_Off_2.svg</field>
</record>
<record id="icon_13" model="ir.attachment">
<field name="name">Maternity_Time_Off_3.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Maternity_Time_Off_3.svg</field>
</record>
<record id="icon_14" model="ir.attachment">
<field name="name">Paid_Time_Off.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Paid_Time_Off.svg</field>
</record>
<record id="icon_15" model="ir.attachment">
<field name="name">Paid_Time_Off_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Paid_Time_Off_2.svg</field>
</record>
<record id="icon_16" model="ir.attachment">
<field name="name">Paid_Time_Off_3.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Paid_Time_Off_3.svg</field>
</record>
<record id="icon_17" model="ir.attachment">
<field name="name">Parental_Time_Off.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Parental_Time_Off.svg</field>
</record>
<record id="icon_18" model="ir.attachment">
<field name="name">Parental_Time_Off_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Parental_Time_Off_2.svg</field>
</record>
<record id="icon_19" model="ir.attachment">
<field name="name">Recovery_Bank_Holiday.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Recovery_Bank_Holiday.svg</field>
</record>
<record id="icon_20" model="ir.attachment">
<field name="name">Recovery_Bank_Holiday_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Recovery_Bank_Holiday_2.svg</field>
</record>
<record id="icon_21" model="ir.attachment">
<field name="name">Sick_Time_Off.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Sick_Time_Off.svg</field>
</record>
<record id="icon_22" model="ir.attachment">
<field name="name">Sick_Time_Off_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Sick_Time_Off_2.svg</field>
</record>
<record id="icon_23" model="ir.attachment">
<field name="name">Small_Unemployement.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Small_Unemployement.svg</field>
</record>
<record id="icon_24" model="ir.attachment">
<field name="name">Small_Unemployement_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Small_Unemployement_2.svg</field>
</record>
<record id="icon_25" model="ir.attachment">
<field name="name">Small_Unemployement_3.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Small_Unemployement_3.svg</field>
</record>
<record id="icon_26" model="ir.attachment">
<field name="name">Training_Time_Off.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Training_Time_Off.svg</field>
</record>
<record id="icon_27" model="ir.attachment">
<field name="name">Training_Time_Off_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Training_Time_Off_2.svg</field>
</record>
<record id="icon_28" model="ir.attachment">
<field name="name">Unpaid_Time_Off.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Unpaid_Time_Off.svg</field>
</record>
<record id="icon_29" model="ir.attachment">
<field name="name">Unpaid_Time_Off_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Unpaid_Time_Off_2.svg</field>
</record>
<record id="icon_30" model="ir.attachment">
<field name="name">Work_Accident_Time_Off.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Work_Accident_Time_Off.svg</field>
</record>
<record id="icon_31" model="ir.attachment">
<field name="name">Work_Accident_Time_Off_2.svg</field>
<field name="res_model">hr.leave.type</field>
<field name="res_field">icon_id</field>
<field name="public" eval="True"/>
<field name="type">url</field>
<field name="url">/hr_holidays/static/src/img/icons/Work_Accident_Time_Off_2.svg</field>
</record>
</data>
<data noupdate="1">
<!-- Casual leave -->
<record id="holiday_status_cl" model="hr.leave.type">
<field name="name">Paid Time Off</field>
<field name="requires_allocation">yes</field>
<field name="employee_requests">no</field>
<field name="leave_validation_type">both</field>
<field name="allocation_validation_type">officer</field>
<field name="leave_notif_subtype_id" ref="mt_leave"/>
<field name="allocation_notif_subtype_id" ref="mt_leave_allocation"/>
<field name="responsible_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="icon_id" ref="hr_holidays.icon_14"/>
<field name="color">2</field>
<field name="company_id" eval="False"/> <!-- Explicitely set to False for it to be available to all companies -->
<field name="sequence">1</field>
</record>
<!-- Sick leave -->
<record id="holiday_status_sl" model="hr.leave.type">
<field name="name">Sick Time Off</field>
<field name="requires_allocation">no</field>
<field name="leave_notif_subtype_id" ref="mt_leave_sick"/>
<field name="responsible_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="support_document">True</field>
<field name="icon_id" ref="hr_holidays.icon_22"/>
<field name="color">3</field>
<field name="company_id" eval="False"/> <!-- Explicitely set to False for it to be available to all companies -->
<field name="sequence">2</field>
</record>
<!-- Compensatory Days -->
<record id="holiday_status_comp" model="hr.leave.type">
<field name="name">Compensatory Days</field>
<field name="requires_allocation">yes</field>
<field name="employee_requests">yes</field>
<field name="leave_validation_type">manager</field>
<field name="allocation_validation_type">officer</field>
<field name="request_unit">hour</field>
<field name="leave_notif_subtype_id" ref="mt_leave"/>
<field name="responsible_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="icon_id" ref="hr_holidays.icon_4"/>
<field name="color">4</field>
<field name="company_id" eval="False"/> <!-- Explicitely set to False for it to be available to all companies -->
<field name="sequence">4</field>
</record>
<!--Unpaid Time Off -->
<record id="holiday_status_unpaid" model="hr.leave.type">
<field name="name">Unpaid</field>
<field name="requires_allocation">no</field>
<field name="leave_validation_type">both</field>
<field name="allocation_validation_type">officer</field>
<field name="request_unit">hour</field>
<field name="unpaid" eval="True"/>
<field name="leave_notif_subtype_id" ref="mt_leave_unpaid"/>
<field name="responsible_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="icon_id" ref="hr_holidays.icon_28"/>
<field name="color">5</field>
<field name="company_id" eval="False"/> <!-- Explicitely set to False for it to be available to all companies -->
<field name="sequence">3</field>
</record>
</data>
</odoo>

468
data/hr_holidays_demo.xml Normal file
View File

@ -0,0 +1,468 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="base.user_demo" model="res.users">
<field name="groups_id" eval="[(3, ref('hr_holidays.group_hr_holidays_manager'))]"/>
</record>
<!--Time Off Type-->
<record id="hr_holiday_status_dv" model="hr.leave.type">
<field name="name">Parental Leaves</field>
<field name="requires_allocation">yes</field>
<field name="employee_requests">no</field>
<field name="leave_validation_type">both</field>
<field name="allocation_validation_type">officer</field>
<field name="responsible_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="icon_id" ref="hr_holidays.icon_11"/>
</record>
<record id="holiday_status_training" model="hr.leave.type">
<field name="name">Training Time Off</field>
<field name="requires_allocation">yes</field>
<field name="employee_requests">no</field>
<field name="leave_validation_type">both</field>
<field name="allocation_validation_type">officer</field>
<field name="responsible_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="icon_id" ref="hr_holidays.icon_26"/>
<field name="allows_negative" eval="True"/>
<field name="max_allowed_negative" eval="20"/>
</record>
<!-- Accrual Plan -->
<record id="hr_accrual_plan_1" model="hr.leave.accrual.plan">
<field name="name">Seniority Plan</field>
</record>
<record id="hr_accrual_level_1" model="hr.leave.accrual.level">
<field name="accrual_plan_id" ref="hr_accrual_plan_1" />
<field name="start_count">1</field>
<field name="start_type">day</field>
<field name="added_value">1</field>
<field name="frequency">yearly</field>
</record>
<record id="hr_accrual_level_2" model="hr.leave.accrual.level">
<field name="accrual_plan_id" ref="hr_accrual_plan_1" />
<field name="start_count">4</field>
<field name="start_type">year</field>
<field name="added_value">2</field>
<field name="frequency">yearly</field>
</record>
<record id="hr_accrual_level_3" model="hr.leave.accrual.level">
<field name="accrual_plan_id" ref="hr_accrual_plan_1" />
<field name="start_count">8</field>
<field name="start_type">year</field>
<field name="added_value">3</field>
<field name="frequency">yearly</field>
</record>
<!-- ++++++++++++++++++++++ Mitchell Admin ++++++++++++++++++++++ -->
<record id="hr_holidays_allocation_cl" model="hr.leave.allocation">
<field name="name">Paid Time Off for Mitchell Admin</field>
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_admin'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<record id="hr_holidays_int_tour" model="hr.leave.allocation">
<field name="name">International Tour</field>
<field name="holiday_status_id" ref="holiday_status_comp"/>
<field name="number_of_days">7</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_admin'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<record id="hr_holidays_vc" model="hr.leave.allocation">
<field name="name">Functional Training</field>
<field name="holiday_status_id" ref="holiday_status_training"/>
<field name="number_of_days">7</field>
<field name="state">confirm</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_admin'))]"/>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<record id='hr_holidays_cl_allocation' model="hr.leave.allocation">
<field name="name">Compensation</field>
<field name="holiday_status_id" ref="holiday_status_comp"/>
<field name="number_of_days">12</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_admin'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<function model="hr.leave.allocation" name="action_validate">
<value eval="[ref('hr_holidays_allocation_cl'), ref('hr_holidays_int_tour'), ref('hr_holidays_cl_allocation')]"/>
</function>
<!-- leave request -->
<record id="hr_holidays_cl" model="hr.leave">
<field name="name">Trip with Family</field>
<field name="holiday_status_id" ref="holiday_status_comp"/>
<field name="request_date_from" eval="(datetime.today().date() + relativedelta(day=1, weekday=0))"/>
<field name="request_date_to" eval="(datetime.today().date() + relativedelta(day=1, weekday=0) + relativedelta(weekday=2))"/>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_admin'))]"/>
</record>
<record id="hr_holidays_sl" model="hr.leave">
<field name="name">Doctor Appointment</field>
<field name="holiday_status_id" ref="holiday_status_sl"/>
<field name="request_date_from" eval="(datetime.today().date() + relativedelta(day=20, weekday=0))"/>
<field name="request_date_to" eval="(datetime.today().date() + relativedelta(day=20, weekday=0) + relativedelta(weekday=2))"/>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_admin'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
<value eval="ref('hr_holidays.hr_holidays_sl')"/>
</function>
<record id="hr.employee_al" model="hr.employee">
<field name="leave_manager_id" ref="base.user_admin"/>
</record>
<record id="hr.employee_mit" model="hr.employee">
<field name="leave_manager_id" ref="base.user_admin"/>
</record>
<record id="hr.employee_qdp" model="hr.employee">
<field name="leave_manager_id" ref="base.user_admin"/>
</record>
<record id="hr.employee_niv" model="hr.employee">
<field name="leave_manager_id" ref="base.user_admin"/>
</record>
<record id="hr.employee_jve" model="hr.employee">
<field name="leave_manager_id" ref="base.user_admin"/>
</record>
<!-- ++++++++++++++++++++++ Ronnie Hart ++++++++++++++++++++++ -->
<record id="hr_holidays_allocation_cl_al" model="hr.leave.allocation">
<field name="name">Paid Time Off for Ronnie Hart</field>
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_al"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_al'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<record id="hr_holidays_allocation_pl_al" model="hr.leave.allocation">
<field name="name">Parental Leaves</field>
<field name="holiday_status_id" ref="hr_holiday_status_dv"/>
<field name="number_of_days">10</field>
<field name="employee_id" ref="hr.employee_al"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_al'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<record id="hr_holidays_vc_al" model="hr.leave.allocation">
<field name="name">Soft Skills Training</field>
<field name="holiday_status_id" ref="holiday_status_training"/>
<field name="number_of_days">12</field>
<field name="employee_id" ref="hr.employee_al"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_al'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<function model="hr.leave.allocation" name="action_validate">
<value eval="[ref('hr_holidays_allocation_cl_al'), ref('hr_holidays_allocation_pl_al'), ref('hr_holidays_vc_al')]"/>
</function>
<!-- leave request -->
<record id="hr_holidays_cl_al" model="hr.leave">
<field name="name">Trip with Friends</field>
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="request_date_from" eval="time.strftime('%Y-%m-14')"/>
<field name="request_date_to" eval="time.strftime('%Y-%m-20')"/>
<field name="employee_id" ref="hr.employee_al"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_al'))]"/>
</record>
<function model="hr.leave" name="action_validate">
<value eval="ref('hr_holidays.hr_holidays_cl_al')"/>
</function>
<record id="hr_holidays_sl_al" model="hr.leave">
<field name="name">Dentist appointment</field>
<field name="holiday_status_id" ref="holiday_status_sl"/>
<field name="request_date_from" eval="(datetime.today().date() + relativedelta(months=1, day=17, weekday=0))"/>
<field name="request_date_to" eval="(datetime.today().date() + relativedelta(months=1, day=17, weekday=0) + relativedelta(weekday=2))"/>
<field name="employee_id" ref="hr.employee_al"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_al'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
<value eval="ref('hr_holidays.hr_holidays_sl_al')"/>
</function>
<!-- ++++++++++++++++++++++ Anita Oliver ++++++++++++++++++++++ -->
<record id="hr_holidays_allocation_cl_mit" model="hr.leave.allocation">
<field name="name">Paid Time Off for Anita Oliver</field>
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_mit'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<function model="hr.leave.allocation" name="action_validate">
<value eval="[ref('hr_holidays_allocation_cl_mit')]"/>
</function>
<record id="hr_holidays_vc_mit" model="hr.leave.allocation">
<field name="name">Compliance Training</field>
<field name="holiday_status_id" ref="holiday_status_training"/>
<field name="number_of_days">7</field>
<field name="state">confirm</field>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_mit'))]"/>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<!-- leave request -->
<record id="hr_holidays_cl_mit" model="hr.leave">
<field name="name">Trip to Paris</field>
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="request_date_from" eval="time.strftime('%Y-%m-22')"/>
<field name="request_date_to" eval="time.strftime('%Y-%m-28')"/>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_mit'))]"/>
</record>
<function model="hr.leave" name="action_validate">
<value eval="ref('hr_holidays.hr_holidays_cl_mit')"/>
</function>
<record id="hr_holidays_cl_mit_2" model="hr.leave">
<field name="name">Trip</field>
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="request_date_from" eval="(datetime.today().date() + relativedelta(day=5, weekday=0))"/>
<field name="request_date_to" eval="(datetime.today().date() + relativedelta(day=5, weekday=0) + relativedelta(weekday=2))"/>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_mit'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_approve">
<value eval="ref('hr_holidays.hr_holidays_cl_mit_2')"/>
</function>
<!-- ++++++++++++++++++++++ Marc Demo ++++++++++++++++++++++ -->
<record id="hr_holidays_allocation_cl_qdp" model="hr.leave.allocation">
<field name="name">Paid Time Off for Marc Demo</field>
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_qdp'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<record id="hr_holidays_vc_qdp" model="hr.leave.allocation">
<field name="name">Time Management Training</field>
<field name="holiday_status_id" ref="holiday_status_training"/>
<field name="number_of_days">7</field>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_qdp'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<function model="hr.leave.allocation" name="action_validate">
<value eval="[ref('hr_holidays.hr_holidays_allocation_cl_qdp'), ref('hr_holidays.hr_holidays_vc_qdp')]"/>
</function>
<!-- leave request -->
<record id="hr_holidays_cl_qdp" model="hr.leave">
<field name="name">Sick day</field>
<field name="holiday_status_id" ref="holiday_status_sl"/>
<field name="request_date_from" eval="(datetime.today().date()+relativedelta(months=1, day=3, weekday=0))"/>
<field name="request_date_to" eval="(datetime.today().date()+relativedelta(months=1, day=3, weekday=0) + relativedelta(weekday=2))"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_qdp'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
<value eval="ref('hr_holidays.hr_holidays_cl_qdp')"/>
</function>
<record id="hr_holidays_sl_qdp" model="hr.leave">
<field name="name">Sick day</field>
<field name="holiday_status_id" ref="holiday_status_sl"/>
<field name="request_date_from" eval="(datetime.today().date() + relativedelta(day=1, weekday=0))"/>
<field name="request_date_to" eval="(datetime.today().date() + relativedelta(day=1, weekday=0) + relativedelta(days=2))"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_qdp'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
<value eval="ref('hr_holidays.hr_holidays_sl_qdp')"/>
</function>
<!-- ++++++++++++++++++++++ Audrey Peterson ++++++++++++++++++++++ -->
<record id="hr_holidays_allocation_cl_fpi" model="hr.leave.allocation">
<field name="name">Paid Time Off for Audrey Peterson</field>
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_fpi"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_fpi'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<function model="hr.leave.allocation" name="action_validate">
<value eval="[ref('hr_holidays.hr_holidays_allocation_cl_fpi')]"/>
</function>
<record id="hr_holidays_vc_fpi" model="hr.leave.allocation">
<field name="name">Consulting Training</field>
<field name="holiday_status_id" ref="holiday_status_training"/>
<field name="number_of_days">7</field>
<field name="employee_id" ref="hr.employee_fpi"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_fpi'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<!-- ++++++++++++++++++++++ Olivia ++++++++++++++++++++++ -->
<record id="hr_holidays_allocation_cl_vad" model="hr.leave.allocation">
<field name="name">Paid Time Off for Olivia</field>
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_niv'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<record id="hr_holidays_vc_vad" model="hr.leave.allocation">
<field name="name">Software Development Training</field>
<field name="holiday_status_id" ref="holiday_status_training"/>
<field name="number_of_days">5</field>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_niv'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<function model="hr.leave.allocation" name="action_validate">
<value eval="[ref('hr_holidays.hr_holidays_allocation_cl_vad'), ref('hr_holidays.hr_holidays_vc_vad')]"/>
</function>
<record id="hr_holidays_cl_vad" model="hr.leave">
<field name="name">Trip to London</field>
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="request_date_from" eval="time.strftime('%Y-%m-09')"/>
<field name="request_date_to" eval="time.strftime('%Y-%m-16')"/>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_niv'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
<value eval="ref('hr_holidays.hr_holidays_cl_vad')"/>
</function>
<record id="hr_holidays_sl_vad" model="hr.leave">
<field name="name">Doctor Appointment</field>
<field name="holiday_status_id" ref="holiday_status_sl"/>
<field name="request_date_from" eval="(datetime.today().date() + relativedelta(day=25, weekday=0))"/>
<field name="request_date_to" eval="(datetime.today().date() + relativedelta(day=25, weekday=0) + relativedelta(weekday=2))"/>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_niv'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
<value eval="ref('hr_holidays.hr_holidays_sl_vad')"/>
</function>
<!-- ++++++++++++++++++++++ Kim ++++++++++++++++++++++ -->
<record id="hr_holidays_allocation_cl_kim" model="hr.leave.allocation">
<field name="name">Paid Time Off for Kim</field>
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_jve"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_jve'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<record id="hr_holidays_vc_kim" model="hr.leave.allocation">
<field name="name">Onboarding Training</field>
<field name="holiday_status_id" ref="holiday_status_training"/>
<field name="number_of_days">5</field>
<field name="state">confirm</field>
<field name="employee_id" ref="hr.employee_jve"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_jve'))]"/>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
<!-- leave request -->
<record id="hr_holidays_sl_kim" model="hr.leave">
<field name="name">Dentist appointment</field>
<field name="holiday_status_id" ref="holiday_status_sl"/>
<field name="request_date_from" eval="(datetime.today().date() + relativedelta(months=1, day=1, weekday=0))"/>
<field name="request_date_to" eval="(datetime.today().date() + relativedelta(months=1, day=1, weekday=0))"/>
<field name="employee_id" ref="hr.employee_jve"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_jve'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
<value eval="ref('hr_holidays.hr_holidays_sl_kim')"/>
</function>
<record id="hr_holidays_sl_kim_2" model="hr.leave">
<field name="name">Second dentist appointment</field>
<field name="holiday_status_id" ref="holiday_status_sl"/>
<field name="request_date_from" eval="(datetime.today().date()+relativedelta(months=4, day=1, weekday=2))"/>
<field name="request_date_to" eval="(datetime.today().date()+relativedelta(months=4, day=1, weekday=2))"/>
<field name="employee_id" ref="hr.employee_jve"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_jve'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
<value eval="ref('hr_holidays.hr_holidays_sl_kim_2')"/>
</function>
<!-- Public time off -->
<record id="resource_public_time_off_1" model="resource.calendar.leaves">
<field name="name">Public Time Off</field>
<field name="company_id" ref="base.main_company"/>
<field name="calendar_id" ref="resource.resource_calendar_std"/>
<field name="date_from" eval="time.strftime('%Y-02-13 05:00:00')"/>
<field name="date_to" eval="time.strftime('%Y-02-13 17:00:00')"/>
</record>
<!-- Mandatory day -->
<record id="hr_leave_mandatory_day_1" model="hr.leave.mandatory.day">
<field name="name">Company Celebration</field>
<field name="company_id" ref="base.main_company"/>
<field name="start_date" eval="(datetime.today() + relativedelta(days=+7)).strftime('%Y-%m-%d 07:00:00')"></field>
<field name="end_date" eval="(datetime.today() + relativedelta(days=+7)).strftime('%Y-%m-%d 16:00:00')"></field>
<field name="color">9</field>
</record>
</data>
</odoo>

25
data/ir_cron_data.xml Normal file
View File

@ -0,0 +1,25 @@
<?xml version='1.0' encoding='UTF-8' ?>
<odoo>
<record id="hr_leave_allocation_cron_accrual" model="ir.cron">
<field name="name">Accrual Time Off: Updates the number of time off</field>
<field name="model_id" ref="model_hr_leave_allocation"/>
<field name="state">code</field>
<field name="code">model._update_accrual()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="True"/>
</record>
<record id="hr_leave_cron_cancel_invalid" model="ir.cron">
<field name="name">Time Off: Cancel invalid leaves</field>
<field name="model_id" ref="model_hr_leave"/>
<field name="state">code</field>
<field name="code">model._cancel_invalid_leaves()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="True"/>
</record>
</odoo>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Leave specific activities -->
<record id="mail_act_leave_approval" model="mail.activity.type">
<field name="name">Time Off Approval</field>
<field name="icon">fa-sun-o</field>
<field name="res_model">hr.leave</field>
<field name="delay_count">15</field>
</record>
<record id="mail_act_leave_second_approval" model="mail.activity.type">
<field name="name">Time Off Second Approve</field>
<field name="icon">fa-sun-o</field>
<field name="res_model">hr.leave</field>
</record>
<!-- Leave specific activities -->
<record id="mail_act_leave_allocation_approval" model="mail.activity.type">
<field name="name">Allocation Approval</field>
<field name="icon">fa-sun-o</field>
<field name="res_model">hr.leave.allocation</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Holidays-related subtypes for messaging / Chatter -->
<record id="mt_leave" model="mail.message.subtype">
<field name="name">Time Off</field>
<field name="res_model">hr.leave</field>
<field name="description">Time Off Request</field>
</record>
<record id="mt_leave_home_working" model="mail.message.subtype">
<field name="name">Home Working</field>
<field name="res_model">hr.leave</field>
<field name="description">Home Working</field>
</record>
<record id="mt_leave_sick" model="mail.message.subtype">
<field name="name">Sick Time Off</field>
<field name="res_model">hr.leave</field>
<field name="description">Sick Time Off</field>
</record>
<record id="mt_leave_unpaid" model="mail.message.subtype">
<field name="name">Unpaid Time Off</field>
<field name="res_model">hr.leave</field>
<field name="description">Unpaid Time Off</field>
</record>
<!-- Allocation-related subtypes for messaging / Chatter -->
<record id="mt_leave_allocation" model="mail.message.subtype">
<field name="name">Allocation</field>
<field name="res_model">hr.leave.allocation</field>
<field name="description">Allocation Request</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="paperformat_hrsummary" model="report.paperformat">
<field name="name">Time Off Summary</field>
<field name="default" eval="True"/>
<field name="format">custom</field>
<field name="page_height">297</field>
<field name="page_width">210</field>
<field name="orientation">Landscape</field>
<field name="margin_top">30</field>
<field name="margin_bottom">23</field>
<field name="margin_left">5</field>
<field name="margin_right">5</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">20</field>
<field name="dpi">90</field>
</record>
</odoo>

4351
i18n/af.po Normal file

File diff suppressed because it is too large Load Diff

4347
i18n/am.po Normal file

File diff suppressed because it is too large Load Diff

4932
i18n/ar.po Normal file

File diff suppressed because it is too large Load Diff

4396
i18n/az.po Normal file

File diff suppressed because it is too large Load Diff

4811
i18n/bg.po Normal file

File diff suppressed because it is too large Load Diff

4352
i18n/bs.po Normal file

File diff suppressed because it is too large Load Diff

4948
i18n/ca.po Normal file

File diff suppressed because it is too large Load Diff

4904
i18n/cs.po Normal file

File diff suppressed because it is too large Load Diff

4910
i18n/da.po Normal file

File diff suppressed because it is too large Load Diff

5030
i18n/de.po Normal file

File diff suppressed because it is too large Load Diff

4357
i18n/el.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/en_GB.po Normal file

File diff suppressed because it is too large Load Diff

5010
i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

5019
i18n/es_419.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/es_BO.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/es_CL.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/es_CO.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/es_CR.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/es_DO.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/es_EC.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/es_PE.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/es_PY.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/es_VE.po Normal file

File diff suppressed because it is too large Load Diff

4921
i18n/et.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/eu.po Normal file

File diff suppressed because it is too large Load Diff

4866
i18n/fa.po Normal file

File diff suppressed because it is too large Load Diff

4941
i18n/fi.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/fo.po Normal file

File diff suppressed because it is too large Load Diff

5011
i18n/fr.po Normal file

File diff suppressed because it is too large Load Diff

4349
i18n/fr_BE.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/fr_CA.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/gl.po Normal file

File diff suppressed because it is too large Load Diff

4356
i18n/gu.po Normal file

File diff suppressed because it is too large Load Diff

4812
i18n/he.po Normal file

File diff suppressed because it is too large Load Diff

4349
i18n/hi.po Normal file

File diff suppressed because it is too large Load Diff

4391
i18n/hr.po Normal file

File diff suppressed because it is too large Load Diff

4781
i18n/hr_holidays.pot Normal file

File diff suppressed because it is too large Load Diff

4835
i18n/hu.po Normal file

File diff suppressed because it is too large Load Diff

4975
i18n/id.po Normal file

File diff suppressed because it is too large Load Diff

4351
i18n/is.po Normal file

File diff suppressed because it is too large Load Diff

5016
i18n/it.po Normal file

File diff suppressed because it is too large Load Diff

4858
i18n/ja.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/ka.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/kab.po Normal file

File diff suppressed because it is too large Load Diff

4352
i18n/km.po Normal file

File diff suppressed because it is too large Load Diff

4870
i18n/ko.po Normal file

File diff suppressed because it is too large Load Diff

4351
i18n/lb.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/lo.po Normal file

File diff suppressed because it is too large Load Diff

4823
i18n/lt.po Normal file

File diff suppressed because it is too large Load Diff

4816
i18n/lv.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/mk.po Normal file

File diff suppressed because it is too large Load Diff

4401
i18n/mn.po Normal file

File diff suppressed because it is too large Load Diff

4362
i18n/nb.po Normal file

File diff suppressed because it is too large Load Diff

4347
i18n/ne.po Normal file

File diff suppressed because it is too large Load Diff

5000
i18n/nl.po Normal file

File diff suppressed because it is too large Load Diff

4933
i18n/pl.po Normal file

File diff suppressed because it is too large Load Diff

4803
i18n/pt.po Normal file

File diff suppressed because it is too large Load Diff

4990
i18n/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

4404
i18n/ro.po Normal file

File diff suppressed because it is too large Load Diff

5005
i18n/ru.po Normal file

File diff suppressed because it is too large Load Diff

4875
i18n/sk.po Normal file

File diff suppressed because it is too large Load Diff

4810
i18n/sl.po Normal file

File diff suppressed because it is too large Load Diff

4350
i18n/sq.po Normal file

File diff suppressed because it is too large Load Diff

4913
i18n/sr.po Normal file

File diff suppressed because it is too large Load Diff

4354
i18n/sr@latin.po Normal file

File diff suppressed because it is too large Load Diff

4858
i18n/sv.po Normal file

File diff suppressed because it is too large Load Diff

4928
i18n/th.po Normal file

File diff suppressed because it is too large Load Diff

4907
i18n/tr.po Normal file

File diff suppressed because it is too large Load Diff

4984
i18n/uk.po Normal file

File diff suppressed because it is too large Load Diff

4965
i18n/vi.po Normal file

File diff suppressed because it is too large Load Diff

4859
i18n/zh_CN.po Normal file

File diff suppressed because it is too large Load Diff

4859
i18n/zh_TW.po Normal file

File diff suppressed because it is too large Load Diff

16
models/__init__.py Normal file
View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import resource
from . import hr_employee_base
from . import hr_employee
from . import hr_department
from . import hr_leave
from . import hr_leave_allocation
from . import hr_leave_type
from . import hr_leave_accrual_plan_level
from . import hr_leave_accrual_plan
from . import hr_leave_mandatory_day
from . import mail_message_subtype
from . import res_partner
from . import res_users

76
models/hr_department.py Normal file
View File

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
from odoo.osv import expression
import ast
class Department(models.Model):
_inherit = 'hr.department'
absence_of_today = fields.Integer(
compute='_compute_leave_count', string='Absence by Today')
leave_to_approve_count = fields.Integer(
compute='_compute_leave_count', string='Time Off to Approve')
allocation_to_approve_count = fields.Integer(
compute='_compute_leave_count', string='Allocation to Approve')
def _compute_leave_count(self):
Requests = self.env['hr.leave']
Allocations = self.env['hr.leave.allocation']
today_date = datetime.datetime.utcnow().date()
today_start = fields.Datetime.to_string(today_date) # get the midnight of the current utc day
today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59))
leave_data = Requests._read_group(
[('department_id', 'in', self.ids),
('state', '=', 'confirm')],
['department_id'], ['__count'])
allocation_data = Allocations._read_group(
[('department_id', 'in', self.ids),
('state', '=', 'confirm')],
['department_id'], ['__count'])
absence_data = Requests._read_group(
[('department_id', 'in', self.ids), ('state', 'not in', ['cancel', 'refuse']),
('date_from', '<=', today_end), ('date_to', '>=', today_start)],
['department_id'], ['__count'])
res_leave = {department.id: count for department, count in leave_data}
res_allocation = {department.id: count for department, count in allocation_data}
res_absence = {department.id: count for department, count in absence_data}
for department in self:
department.leave_to_approve_count = res_leave.get(department.id, 0)
department.allocation_to_approve_count = res_allocation.get(department.id, 0)
department.absence_of_today = res_absence.get(department.id, 0)
def _get_action_context(self):
return {
'search_default_approve': 1,
'search_default_active_employee': 2,
'search_default_department_id': self.id,
'default_department_id': self.id,
'searchpanel_default_department_id': self.id,
}
def action_open_leave_department(self):
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.hr_leave_action_action_approve_department")
action['context'] = {
**self._get_action_context(),
'search_default_active_time_off': 3,
'hide_employee_name': 1,
'holiday_status_display_name': False
}
return action
def action_open_allocation_department(self):
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.hr_leave_allocation_action_approve_department")
action['context'] = self._get_action_context()
action['context']['search_default_second_approval'] = 3
action['domain'] = expression.AND([ast.literal_eval(action['domain']), [('state', '=', 'confirm')]])
return action

582
models/hr_employee.py Normal file
View File

@ -0,0 +1,582 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, date, time
from collections import defaultdict
from dateutil.relativedelta import relativedelta
import pytz
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_round
from odoo.addons.resource.models.utils import HOURS_PER_DAY
class HrEmployeeBase(models.AbstractModel):
_inherit = "hr.employee.base"
leave_manager_id = fields.Many2one(
'res.users', string='Time Off',
compute='_compute_leave_manager', store=True, readonly=False,
domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
help='Select the user responsible for approving "Time Off" of this employee.\n'
'If empty, the approval is done by an Administrator or Approver (determined in settings/users).')
remaining_leaves = fields.Float(
compute='_compute_remaining_leaves', string='Remaining Paid Time Off',
help='Total number of paid time off allocated to this employee, change this value to create allocation/time off request. '
'Total based on all the time off types without overriding limit.')
current_leave_state = fields.Selection(compute='_compute_leave_status', string="Current Time Off Status",
selection=[
('draft', 'New'),
('confirm', 'Waiting Approval'),
('refuse', 'Refused'),
('validate1', 'Waiting Second Approval'),
('validate', 'Approved'),
('cancel', 'Cancelled')
])
leave_date_from = fields.Date('From Date', compute='_compute_leave_status')
leave_date_to = fields.Date('To Date', compute='_compute_leave_status')
leaves_count = fields.Float('Number of Time Off', compute='_compute_remaining_leaves')
allocation_count = fields.Float('Total number of days allocated.', compute='_compute_allocation_count')
allocations_count = fields.Integer('Total number of allocations', compute="_compute_allocation_count")
show_leaves = fields.Boolean('Able to see Remaining Time Off', compute='_compute_show_leaves')
is_absent = fields.Boolean('Absent Today', compute='_compute_leave_status', search='_search_absent_employee')
allocation_display = fields.Char(compute='_compute_allocation_remaining_display')
allocation_remaining_display = fields.Char(compute='_compute_allocation_remaining_display')
hr_icon_display = fields.Selection(selection_add=[('presence_holiday_absent', 'On leave'),
('presence_holiday_present', 'Present but on leave')])
def _get_remaining_leaves(self):
""" Helper to compute the remaining leaves for the current employees
:returns dict where the key is the employee id, and the value is the remain leaves
"""
self._cr.execute("""
SELECT
sum(h.number_of_days) AS days,
h.employee_id
FROM
(
SELECT holiday_status_id, number_of_days,
state, employee_id
FROM hr_leave_allocation
UNION ALL
SELECT holiday_status_id, (number_of_days * -1) as number_of_days,
state, employee_id
FROM hr_leave
) h
join hr_leave_type s ON (s.id=h.holiday_status_id)
WHERE
s.active = true AND h.state='validate' AND
s.requires_allocation='yes' AND
h.employee_id in %s
GROUP BY h.employee_id""", (tuple(self.ids),))
return dict((row['employee_id'], row['days']) for row in self._cr.dictfetchall())
def _compute_remaining_leaves(self):
remaining = {}
if self.ids:
remaining = self._get_remaining_leaves()
for employee in self:
value = float_round(remaining.get(employee.id, 0.0), precision_digits=2)
employee.leaves_count = value
employee.remaining_leaves = value
def _compute_allocation_count(self):
# Don't get allocations that are expired
current_date = date.today()
data = self.env['hr.leave.allocation']._read_group([
('employee_id', 'in', self.ids),
('holiday_status_id.active', '=', True),
('holiday_status_id.requires_allocation', '=', 'yes'),
('state', '=', 'validate'),
('date_from', '<=', current_date),
'|',
('date_to', '=', False),
('date_to', '>=', current_date),
], ['employee_id'], ['__count', 'number_of_days:sum'])
rg_results = {employee.id: (count, days) for employee, count, days in data}
for employee in self:
count, days = rg_results.get(employee.id, (0, 0))
employee.allocation_count = float_round(days, precision_digits=2)
employee.allocations_count = count
def _compute_allocation_remaining_display(self):
current_date = date.today()
allocations = self.env['hr.leave.allocation'].search([('employee_id', 'in', self.ids)])
leaves_taken = self._get_consumed_leaves(allocations.holiday_status_id)[0]
for employee in self:
employee_remaining_leaves = 0
employee_max_leaves = 0
for leave_type in leaves_taken[employee]:
if leave_type.requires_allocation == 'no':
continue
for allocation in leaves_taken[employee][leave_type]:
if allocation and allocation.date_from <= current_date\
and (not allocation.date_to or allocation.date_to >= current_date):
virtual_remaining_leaves = leaves_taken[employee][leave_type][allocation]['virtual_remaining_leaves']
employee_remaining_leaves += virtual_remaining_leaves\
if leave_type.request_unit in ['day', 'half_day']\
else virtual_remaining_leaves / (employee.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
employee_max_leaves += allocation.number_of_days
employee.allocation_remaining_display = "%g" % float_round(employee_remaining_leaves, precision_digits=2)
employee.allocation_display = "%g" % float_round(employee_max_leaves, precision_digits=2)
def _compute_presence_icon(self):
super()._compute_presence_icon()
employees_absent = self.filtered(lambda employee:
employee.hr_presence_state != 'present'
and employee.is_absent)
employees_absent.update({'hr_icon_display': 'presence_holiday_absent'})
employees_present = self.filtered(lambda employee:
employee.hr_presence_state == 'present'
and employee.is_absent)
employees_present.update({'hr_icon_display': 'presence_holiday_present'})
def _compute_leave_status(self):
# Used SUPERUSER_ID to forcefully get status of other user's leave, to bypass record rule
holidays = self.env['hr.leave'].sudo().search([
('employee_id', 'in', self.ids),
('date_from', '<=', fields.Datetime.now()),
('date_to', '>=', fields.Datetime.now()),
('state', '=', 'validate'),
])
leave_data = {}
for holiday in holidays:
leave_data[holiday.employee_id.id] = {}
leave_data[holiday.employee_id.id]['leave_date_from'] = holiday.date_from.date()
leave_data[holiday.employee_id.id]['leave_date_to'] = holiday.date_to.date()
leave_data[holiday.employee_id.id]['current_leave_state'] = holiday.state
for employee in self:
employee.leave_date_from = leave_data.get(employee.id, {}).get('leave_date_from')
employee.leave_date_to = leave_data.get(employee.id, {}).get('leave_date_to')
employee.current_leave_state = leave_data.get(employee.id, {}).get('current_leave_state')
employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id, {}).get('current_leave_state') in ['validate']
@api.depends('parent_id')
def _compute_leave_manager(self):
for employee in self:
previous_manager = employee._origin.parent_id.user_id
manager = employee.parent_id.user_id
if manager and employee.leave_manager_id == previous_manager or not employee.leave_manager_id:
employee.leave_manager_id = manager
elif not employee.leave_manager_id:
employee.leave_manager_id = False
def _compute_show_leaves(self):
show_leaves = self.env['res.users'].has_group('hr_holidays.group_hr_holidays_user')
for employee in self:
if show_leaves or employee.user_id == self.env.user:
employee.show_leaves = True
else:
employee.show_leaves = False
def _search_absent_employee(self, operator, value):
if operator not in ('=', '!=') or not isinstance(value, bool):
raise UserError(_('Operation not supported'))
# This search is only used for the 'Absent Today' filter however
# this only returns employees that are absent right now.
today_date = datetime.utcnow().date()
today_start = fields.Datetime.to_string(today_date)
today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59))
holidays = self.env['hr.leave'].sudo().search([
('employee_id', '!=', False),
('state', '=', 'validate'),
('date_from', '<=', today_end),
('date_to', '>=', today_start),
])
operator = ['in', 'not in'][(operator == '=') != value]
return [('id', operator, holidays.mapped('employee_id').ids)]
@api.model_create_multi
def create(self, vals_list):
if self.env.context.get('salary_simulation'):
return super().create(vals_list)
approver_group = self.env.ref('hr_holidays.group_hr_holidays_responsible', raise_if_not_found=False)
group_updates = []
for vals in vals_list:
if 'parent_id' in vals:
manager = self.env['hr.employee'].browse(vals['parent_id']).user_id
vals['leave_manager_id'] = vals.get('leave_manager_id', manager.id)
if approver_group and vals.get('leave_manager_id'):
group_updates.append((4, vals['leave_manager_id']))
if group_updates:
approver_group.sudo().write({'users': group_updates})
return super().create(vals_list)
def write(self, values):
if 'parent_id' in values:
manager = self.env['hr.employee'].browse(values['parent_id']).user_id
if manager:
to_change = self.filtered(lambda e: e.leave_manager_id == e.parent_id.user_id or not e.leave_manager_id)
to_change.write({'leave_manager_id': values.get('leave_manager_id', manager.id)})
old_managers = self.env['res.users']
if 'leave_manager_id' in values:
old_managers = self.mapped('leave_manager_id')
if values['leave_manager_id']:
leave_manager = self.env['res.users'].browse(values['leave_manager_id'])
old_managers -= leave_manager
approver_group = self.env.ref('hr_holidays.group_hr_holidays_responsible', raise_if_not_found=False)
if approver_group and not leave_manager.has_group('hr_holidays.group_hr_holidays_responsible'):
leave_manager.sudo().write({'groups_id': [(4, approver_group.id)]})
res = super(HrEmployeeBase, self).write(values)
# remove users from the Responsible group if they are no longer leave managers
old_managers.sudo()._clean_leave_responsible_users()
# Change the resource calendar of the employee's leaves in the future
# Other modules can disable this behavior by setting the context key
# 'no_leave_resource_calendar_update'
if 'resource_calendar_id' in values and not self.env.context.get('no_leave_resource_calendar_update'):
try:
self.env['hr.leave'].search([
('employee_id', 'in', self.ids),
('resource_calendar_id', '!=', int(values['resource_calendar_id'])),
('date_from', '>', fields.Datetime.now())]).write({'resource_calendar_id': values['resource_calendar_id']})
except ValidationError:
raise ValidationError(_("Changing this working schedule results in the affected employee(s) not having enough "
"leaves allocated to accomodate for their leaves already taken in the future. Please "
"review this employee's leaves and adjust their allocation accordingly."))
if 'parent_id' in values or 'department_id' in values:
today_date = fields.Datetime.now()
hr_vals = {}
if values.get('parent_id') is not None:
hr_vals['manager_id'] = values['parent_id']
if values.get('department_id') is not None:
hr_vals['department_id'] = values['department_id']
holidays = self.env['hr.leave'].sudo().search(['|', ('state', 'in', ['draft', 'confirm']), ('date_from', '>', today_date), ('employee_id', 'in', self.ids)])
holidays.write(hr_vals)
allocations = self.env['hr.leave.allocation'].sudo().search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
allocations.write(hr_vals)
return res
class HrEmployee(models.Model):
_inherit = 'hr.employee'
current_leave_id = fields.Many2one('hr.leave.type', compute='_compute_current_leave', string="Current Time Off Type",
groups="hr.group_hr_user")
def _compute_current_leave(self):
self.current_leave_id = False
holidays = self.env['hr.leave'].sudo().search([
('employee_id', 'in', self.ids),
('date_from', '<=', fields.Datetime.now()),
('date_to', '>=', fields.Datetime.now()),
('state', '=', 'validate'),
])
for holiday in holidays:
employee = self.filtered(lambda e: e.id == holiday.employee_id.id)
employee.current_leave_id = holiday.holiday_status_id.id
def _get_user_m2o_to_empty_on_archived_employees(self):
return super()._get_user_m2o_to_empty_on_archived_employees() + ['leave_manager_id']
def action_time_off_dashboard(self):
return {
'name': _('Time Off Dashboard'),
'type': 'ir.actions.act_window',
'res_model': 'hr.leave',
'views': [[self.env.ref('hr_holidays.hr_leave_employee_view_dashboard').id, 'calendar']],
'domain': [('employee_id', 'in', self.ids)],
'context': {
'employee_id': self.ids,
},
}
def _is_leave_user(self):
return self == self.env.user.employee_id and self.user_has_groups('hr_holidays.group_hr_holidays_user')
def get_mandatory_days(self, start_date, end_date):
all_days = {}
self = self or self.env.user.employee_id
mandatory_days = self._get_mandatory_days(start_date, end_date)
for mandatory_day in mandatory_days:
num_days = (mandatory_day.end_date - mandatory_day.start_date).days
for d in range(num_days + 1):
all_days[str(mandatory_day.start_date + relativedelta(days=d))] = mandatory_day.color
return all_days
@api.model
def get_special_days_data(self, date_start, date_end):
return {
'mandatoryDays': self.get_mandatory_days_data(date_start, date_end),
'bankHolidays': self.get_public_holidays_data(date_start, date_end),
}
@api.model
def get_public_holidays_data(self, date_start, date_end):
self = self._get_contextual_employee()
employee_tz = pytz.timezone(self._get_tz() if self else self.env.user.tz or 'utc')
public_holidays = self._get_public_holidays(date_start, date_end).sorted('date_from')
return list(map(lambda bh: {
'id': -bh.id,
'colorIndex': 0,
'end': datetime.combine(bh.date_to.astimezone(employee_tz), datetime.max.time()).isoformat(),
'endType': "datetime",
'isAllDay': True,
'start': datetime.combine(bh.date_from.astimezone(employee_tz), datetime.min.time()).isoformat(),
'startType': "datetime",
'title': bh.name,
}, public_holidays))
def _get_public_holidays(self, date_start, date_end):
domain = [
('resource_id', '=', False),
('company_id', 'in', self.env.companies.ids),
('date_from', '<=', date_end),
('date_to', '>=', date_start),
]
# a user with hr_holidays permissions will be able to see all public holidays from his calendar
if not self._is_leave_user():
domain += [
'|',
('calendar_id', '=', False),
('calendar_id', '=', self.resource_calendar_id.id),
]
return self.env['resource.calendar.leaves'].search(domain)
@api.model
def get_mandatory_days_data(self, date_start, date_end):
self = self._get_contextual_employee()
mandatory_days = self._get_mandatory_days(date_start, date_end).sorted('start_date')
return list(map(lambda sd: {
'id': -sd.id,
'colorIndex': sd.color,
'end': datetime.combine(sd.end_date, datetime.max.time()).isoformat(),
'endType': "datetime",
'isAllDay': True,
'start': datetime.combine(sd.start_date, datetime.min.time()).isoformat(),
'startType': "datetime",
'title': sd.name,
}, mandatory_days))
def _get_mandatory_days(self, start_date, end_date):
domain = [
('start_date', '<=', end_date),
('end_date', '>=', start_date),
('company_id', 'in', self.env.companies.ids),
]
# a user with hr_holidays permissions will be able to see all mandatory days from his calendar
if not self._is_leave_user():
domain += [
'|',
('resource_calendar_id', '=', False),
('resource_calendar_id', '=', self.resource_calendar_id.id),
]
if self.department_id:
domain += [
'|',
('department_ids', '=', False),
('department_ids', 'parent_of', self.department_id.id),
]
else:
domain += [('department_ids', '=', False)]
return self.env['hr.leave.mandatory.day'].search(domain)
@api.model
def _get_contextual_employee(self):
ctx = self.env.context
return self.browse(ctx.get('employee_id') or ctx.get('default_employee_id')) or self.env.user.employee_id
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
# allocation_leaves_consumed is 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.
# 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,
}),
'total_virtual_excess': 0,
'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 ['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 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.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 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 == 'hour':
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._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)

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class HrEmployeeBase(models.AbstractModel):
_inherit = "hr.employee.base"
def _compute_presence_state(self):
super()._compute_presence_state()
employees = self.filtered(lambda employee: employee.hr_presence_state != 'present' and employee.is_absent)
employees.update({'hr_presence_state': 'absent'})

1771
models/hr_leave.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.addons.hr_holidays.models.hr_leave_accrual_plan_level import _get_selection_days
DAY_SELECT_VALUES = [str(i) for i in range(1, 29)] + ['last']
DAY_SELECT_SELECTION_NO_LAST = tuple(zip(DAY_SELECT_VALUES, (str(i) for i in range(1, 29))))
class AccrualPlan(models.Model):
_name = "hr.leave.accrual.plan"
_description = "Accrual Plan"
active = fields.Boolean(default=True)
name = fields.Char('Name', required=True)
time_off_type_id = fields.Many2one('hr.leave.type', string="Time Off Type",
check_company=True,
help="""Specify if this accrual plan can only be used with this Time Off Type.
Leave empty if this accrual plan can be used with any Time Off Type.""")
employees_count = fields.Integer("Employees", compute='_compute_employee_count')
level_ids = fields.One2many('hr.leave.accrual.level', 'accrual_plan_id', copy=True, string="Milestone")
allocation_ids = fields.One2many('hr.leave.allocation', 'accrual_plan_id')
company_id = fields.Many2one('res.company', string='Company',
compute="_compute_company_id", store="True", readonly=False)
transition_mode = fields.Selection([
('immediately', 'Immediately'),
('end_of_accrual', "After this accrual's period")],
string="Milestone Transition", default="immediately", required=True,
help="""Specify what occurs if a level transition takes place in the middle of a pay period.\n
'Immediately' will switch the employee to the new accrual level on the exact date during the ongoing pay period.\n
'After this accrual's period' will keep the employee on the same accrual level until the ongoing pay period is complete.
After it is complete, the new level will take effect when the next pay period begins.""")
show_transition_mode = fields.Boolean(compute='_compute_show_transition_mode')
is_based_on_worked_time = fields.Boolean("Based on worked time", compute="_compute_is_based_on_worked_time", store=True, readonly=False,
help="If checked, the accrual period will be calculated according to the work days, not calendar days.")
accrued_gain_time = fields.Selection([
("start", "At the start of the accrual period"),
("end", "At the end of the accrual period")],
default="end", required=True)
carryover_date = fields.Selection([
("year_start", "At the start of the year"),
("allocation", "At the allocation date"),
("other", "Other")],
default="year_start", required=True, string="Carry-Over Time")
carryover_day = fields.Integer(default=1)
carryover_day_display = fields.Selection(
_get_selection_days, compute='_compute_carryover_day_display', inverse='_inverse_carryover_day_display')
carryover_month = fields.Selection([
("jan", "January"),
("feb", "February"),
("mar", "March"),
("apr", "April"),
("may", "May"),
("jun", "June"),
("jul", "July"),
("aug", "August"),
("sep", "September"),
("oct", "October"),
("nov", "November"),
("dec", "December")
], default="jan")
added_value_type = fields.Selection([('day', 'Days'), ('hour', 'Hours')], compute='_compute_added_value_type', store=True)
@api.depends('level_ids')
def _compute_show_transition_mode(self):
for plan in self:
plan.show_transition_mode = len(plan.level_ids) > 1
level_count = fields.Integer('Levels', compute='_compute_level_count')
@api.depends('level_ids')
def _compute_level_count(self):
level_read_group = self.env['hr.leave.accrual.level']._read_group(
[('accrual_plan_id', 'in', self.ids)],
groupby=['accrual_plan_id'],
aggregates=['__count'],
)
mapped_count = {accrual_plan.id: count for accrual_plan, count in level_read_group}
for plan in self:
plan.level_count = mapped_count.get(plan.id, 0)
@api.depends('allocation_ids')
def _compute_employee_count(self):
allocations_read_group = self.env['hr.leave.allocation']._read_group(
[('accrual_plan_id', 'in', self.ids)],
['accrual_plan_id'],
['employee_id:count_distinct'],
)
allocations_dict = {accrual_plan.id: count for accrual_plan, count in allocations_read_group}
for plan in self:
plan.employees_count = allocations_dict.get(plan.id, 0)
@api.depends('time_off_type_id.company_id')
def _compute_company_id(self):
for accrual_plan in self:
if accrual_plan.time_off_type_id:
accrual_plan.company_id = accrual_plan.time_off_type_id.company_id
else:
accrual_plan.company_id = self.env.company
@api.depends("accrued_gain_time")
def _compute_is_based_on_worked_time(self):
for plan in self:
if plan.accrued_gain_time == "start":
plan.is_based_on_worked_time = False
@api.depends("level_ids")
def _compute_added_value_type(self):
for plan in self:
if plan.level_ids:
plan.added_value_type = plan.level_ids[0].added_value_type
@api.depends("carryover_day")
def _compute_carryover_day_display(self):
days_select = _get_selection_days(self)
for plan in self:
plan.carryover_day_display = days_select[min(plan.carryover_day - 1, 28)][0]
def _inverse_carryover_day_display(self):
for plan in self:
if plan.carryover_day_display == 'last':
plan.carryover_day = 31
else:
plan.carryover_day = DAY_SELECT_VALUES.index(plan.carryover_day_display) + 1
def action_open_accrual_plan_employees(self):
self.ensure_one()
return {
'name': _("Accrual Plan's Employees"),
'type': 'ir.actions.act_window',
'view_mode': 'kanban,tree,form',
'res_model': 'hr.employee',
'domain': [('id', 'in', self.allocation_ids.employee_id.ids)],
}
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
default = dict(default or {},
name=_("%s (copy)", self.name))
return super().copy(default=default)

View File

@ -0,0 +1,305 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models
DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
# Used for displaying the days and reversing selection -> integer
DAY_SELECT_VALUES = [str(i) for i in range(1, 29)] + ['last']
DAY_SELECT_SELECTION_NO_LAST = tuple(zip(DAY_SELECT_VALUES, (str(i) for i in range(1, 29))))
def _get_selection_days(self):
return DAY_SELECT_SELECTION_NO_LAST + (("last", _("last day")),)
class AccrualPlanLevel(models.Model):
_name = "hr.leave.accrual.level"
_description = "Accrual Plan Level"
_order = 'sequence asc'
sequence = fields.Integer(
string='sequence', compute='_compute_sequence', store=True,
help='Sequence is generated automatically by start time delta.')
accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', "Accrual Plan", required=True, ondelete="cascade")
accrued_gain_time = fields.Selection(related='accrual_plan_id.accrued_gain_time')
start_count = fields.Integer(
"Start after",
help="The accrual starts after a defined period from the allocation start date. This field defines the number of days, months or years after which accrual is used.", default="1")
start_type = fields.Selection(
[('day', 'Days'),
('month', 'Months'),
('year', 'Years')],
default='day', string=" ", required=True,
help="This field defines the unit of time after which the accrual starts.")
# Accrue of
added_value = fields.Float(
"Rate", digits=(16, 5), required=True, default=1,
help="The number of hours/days that will be incremented in the specified Time Off Type for every period")
added_value_type = fields.Selection([
('day', 'Days'),
('hour', 'Hours')
], compute="_compute_added_value_type", store=True, required=True, default="day")
frequency = fields.Selection([
('hourly', 'Hourly'),
('daily', 'Daily'),
('weekly', 'Weekly'),
('bimonthly', 'Twice a month'),
('monthly', 'Monthly'),
('biyearly', 'Twice a year'),
('yearly', 'Yearly'),
], default='daily', required=True, string="Frequency")
week_day = fields.Selection([
('mon', 'Monday'),
('tue', 'Tuesday'),
('wed', 'Wednesday'),
('thu', 'Thursday'),
('fri', 'Friday'),
('sat', 'Saturday'),
('sun', 'Sunday'),
], default='mon', required=True, string="Allocation on")
first_day = fields.Integer(default=1)
first_day_display = fields.Selection(
_get_selection_days, compute='_compute_days_display', inverse='_inverse_first_day_display')
second_day = fields.Integer(default=15)
second_day_display = fields.Selection(
_get_selection_days, compute='_compute_days_display', inverse='_inverse_second_day_display')
first_month_day = fields.Integer(default=1)
first_month_day_display = fields.Selection(
_get_selection_days, compute='_compute_days_display', inverse='_inverse_first_month_day_display')
first_month = fields.Selection([
('jan', 'January'),
('feb', 'February'),
('mar', 'March'),
('apr', 'April'),
('may', 'May'),
('jun', 'June'),
], default="jan")
second_month_day = fields.Integer(default=1)
second_month_day_display = fields.Selection(
_get_selection_days, compute='_compute_days_display', inverse='_inverse_second_month_day_display')
second_month = fields.Selection([
('jul', 'July'),
('aug', 'August'),
('sep', 'September'),
('oct', 'October'),
('nov', 'November'),
('dec', 'December')
], default="jul")
yearly_month = fields.Selection([
('jan', 'January'),
('feb', 'February'),
('mar', 'March'),
('apr', 'April'),
('may', 'May'),
('jun', 'June'),
('jul', 'July'),
('aug', 'August'),
('sep', 'September'),
('oct', 'October'),
('nov', 'November'),
('dec', 'December')
], default="jan")
yearly_day = fields.Integer(default=1)
yearly_day_display = fields.Selection(
_get_selection_days, compute='_compute_days_display', inverse='_inverse_yearly_day_display')
cap_accrued_time = fields.Boolean("Cap accrued time", default=True)
maximum_leave = fields.Float(
'Limit to', digits=(16, 2), compute="_compute_maximum_leave", readonly=False, store=True,
help="Choose a cap for this accrual.")
action_with_unused_accruals = fields.Selection(
[('lost', 'None. Accrued time reset to 0'),
('all', 'All accrued time carried over'),
('maximum', 'Carry over with a maximum')],
string="Carry over",
default='all', required=True)
postpone_max_days = fields.Integer("Maximum amount of accruals to transfer",
help="Set a maximum of accruals an allocation keeps at the end of the year.")
can_modify_value_type = fields.Boolean(compute="_compute_can_modify_value_type")
_sql_constraints = [
('check_dates',
"CHECK( (frequency IN ('daily', 'hourly')) or"
"(week_day IS NOT NULL AND frequency = 'weekly') or "
"(first_day > 0 AND second_day > first_day AND first_day <= 31 AND second_day <= 31 AND frequency = 'bimonthly') or "
"(first_day > 0 AND first_day <= 31 AND frequency = 'monthly')or "
"(first_month_day > 0 AND first_month_day <= 31 AND second_month_day > 0 AND second_month_day <= 31 AND frequency = 'biyearly') or "
"(yearly_day > 0 AND yearly_day <= 31 AND frequency = 'yearly'))",
"The dates you've set up aren't correct. Please check them."),
('start_count_check', "CHECK( start_count >= 0 )", "You can not start an accrual in the past."),
('added_value_greater_than_zero', 'CHECK(added_value > 0)', 'You must give a rate greater than 0 in accrual plan levels.')
]
@api.depends('start_count', 'start_type')
def _compute_sequence(self):
# Not 100% accurate because of odd months/years, but good enough
start_type_multipliers = {
'day': 1,
'month': 30,
'year': 365,
}
for level in self:
level.sequence = level.start_count * start_type_multipliers[level.start_type]
@api.depends('accrual_plan_id', 'accrual_plan_id.level_ids', 'accrual_plan_id.time_off_type_id')
def _compute_can_modify_value_type(self):
for level in self:
level.can_modify_value_type = not level.accrual_plan_id.time_off_type_id and level.accrual_plan_id.level_ids and level.accrual_plan_id.level_ids[0] == level
@api.depends('accrual_plan_id', 'accrual_plan_id.level_ids', 'accrual_plan_id.time_off_type_id')
def _compute_added_value_type(self):
for level in self:
if level.accrual_plan_id.time_off_type_id:
level.added_value_type = "day" if level.accrual_plan_id.time_off_type_id.request_unit in ["day", "half_day"] else "hour"
elif level.accrual_plan_id.level_ids and level.accrual_plan_id.level_ids[0] != level:
level.added_value_type = level.accrual_plan_id.level_ids[0].added_value_type
@api.depends('first_day', 'second_day', 'first_month_day', 'second_month_day', 'yearly_day')
def _compute_days_display(self):
days_select = _get_selection_days(self)
for level in self:
level.first_day_display = days_select[min(level.first_day - 1, 28)][0]
level.second_day_display = days_select[min(level.second_day - 1, 28)][0]
level.first_month_day_display = days_select[min(level.first_month_day - 1, 28)][0]
level.second_month_day_display = days_select[min(level.second_month_day - 1, 28)][0]
level.yearly_day_display = days_select[min(level.yearly_day - 1, 28)][0]
@api.depends('cap_accrued_time')
def _compute_maximum_leave(self):
for level in self:
level.maximum_leave = 100 if level.cap_accrued_time else 0
def _inverse_first_day_display(self):
for level in self:
if level.first_day_display == 'last':
level.first_day = 31
else:
level.first_day = DAY_SELECT_VALUES.index(level.first_day_display) + 1
def _inverse_second_day_display(self):
for level in self:
if level.second_day_display == 'last':
level.second_day = 31
else:
level.second_day = DAY_SELECT_VALUES.index(level.second_day_display) + 1
def _inverse_first_month_day_display(self):
for level in self:
if level.first_month_day_display == 'last':
level.first_month_day = 31
else:
level.first_month_day = DAY_SELECT_VALUES.index(level.first_month_day_display) + 1
def _inverse_second_month_day_display(self):
for level in self:
if level.second_month_day_display == 'last':
level.second_month_day = 31
else:
level.second_month_day = DAY_SELECT_VALUES.index(level.second_month_day_display) + 1
def _inverse_yearly_day_display(self):
for level in self:
if level.yearly_day_display == 'last':
level.yearly_day = 31
else:
level.yearly_day = DAY_SELECT_VALUES.index(level.yearly_day_display) + 1
def _get_next_date(self, last_call):
"""
Returns the next date with the given last call
"""
self.ensure_one()
if self.frequency in ['hourly', 'daily']:
return last_call + relativedelta(days=1)
elif self.frequency == 'weekly':
daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
weekday = daynames.index(self.week_day)
return last_call + relativedelta(days=1, weekday=weekday)
elif self.frequency == 'bimonthly':
first_date = last_call + relativedelta(day=self.first_day)
second_date = last_call + relativedelta(day=self.second_day)
if last_call < first_date:
return first_date
elif last_call < second_date:
return second_date
else:
return last_call + relativedelta(months=1, day=self.first_day)
elif self.frequency == 'monthly':
date = last_call + relativedelta(day=self.first_day)
if last_call < date:
return date
else:
return last_call + relativedelta(months=1, day=self.first_day)
elif self.frequency == 'biyearly':
first_month = MONTHS.index(self.first_month) + 1
second_month = MONTHS.index(self.second_month) + 1
first_date = last_call + relativedelta(month=first_month, day=self.first_month_day)
second_date = last_call + relativedelta(month=second_month, day=self.second_month_day)
if last_call < first_date:
return first_date
elif last_call < second_date:
return second_date
else:
return last_call + relativedelta(years=1, month=first_month, day=self.first_month_day)
elif self.frequency == 'yearly':
month = MONTHS.index(self.yearly_month) + 1
date = last_call + relativedelta(month=month, day=self.yearly_day)
if last_call < date:
return date
else:
return last_call + relativedelta(years=1, month=month, day=self.yearly_day)
else:
return False
def _get_previous_date(self, last_call):
"""
Returns the date a potential previous call would have been at
For example if you have a monthly level giving 16/02 would return 01/02
Contrary to `_get_next_date` this function will return the 01/02 if that date is given
"""
self.ensure_one()
if self.frequency in ['hourly', 'daily']:
return last_call
elif self.frequency == 'weekly':
daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
weekday = daynames.index(self.week_day)
return last_call + relativedelta(days=-6, weekday=weekday)
elif self.frequency == 'bimonthly':
second_date = last_call + relativedelta(day=self.second_day)
first_date = last_call + relativedelta(day=self.first_day)
if last_call >= second_date:
return second_date
elif last_call >= first_date:
return first_date
else:
return last_call + relativedelta(months=-1, day=self.second_day)
elif self.frequency == 'monthly':
date = last_call + relativedelta(day=self.first_day)
if last_call >= date:
return date
else:
return last_call + relativedelta(months=-1, day=self.first_day)
elif self.frequency == 'biyearly':
first_month = MONTHS.index(self.first_month) + 1
second_month = MONTHS.index(self.second_month) + 1
first_date = last_call + relativedelta(month=first_month, day=self.first_month_day)
second_date = last_call + relativedelta(month=second_month, day=self.second_month_day)
if last_call >= second_date:
return second_date
elif last_call >= first_date:
return first_date
else:
return last_call + relativedelta(years=-1, month=second_month, day=self.second_month_day)
elif self.frequency == 'yearly':
month = MONTHS.index(self.yearly_month) + 1
year_date = last_call + relativedelta(month=month, day=self.yearly_day)
if last_call >= year_date:
return year_date
else:
return last_call + relativedelta(years=-1, month=month, day=self.yearly_day)
else:
return False

View File

@ -0,0 +1,917 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
from datetime import datetime, date, time
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.addons.resource.models.utils import HOURS_PER_DAY
from odoo.addons.hr_holidays.models.hr_leave import get_employee_from_context
from odoo.exceptions import AccessError, UserError
from odoo.tools.float_utils import float_round
from odoo.tools.date_utils import get_timedelta
from odoo.osv import expression
MONTHS_TO_INTEGER = {"jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6, "jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12}
class HolidaysAllocation(models.Model):
""" Allocation Requests Access specifications: similar to leave requests """
_name = "hr.leave.allocation"
_description = "Time Off Allocation"
_order = "create_date desc"
_inherit = ['mail.thread', 'mail.activity.mixin']
_mail_post_access = 'read'
def _default_holiday_status_id(self):
if self.user_has_groups('hr_holidays.group_hr_holidays_user'):
domain = [('has_valid_allocation', '=', True), ('requires_allocation', '=', 'yes')]
else:
domain = [('has_valid_allocation', '=', True), ('requires_allocation', '=', 'yes'), ('employee_requests', '=', 'yes')]
return self.env['hr.leave.type'].search(domain, limit=1)
def _domain_holiday_status_id(self):
if self.user_has_groups('hr_holidays.group_hr_holidays_user'):
return [('requires_allocation', '=', 'yes')]
return [('employee_requests', '=', 'yes')]
name = fields.Char(
string='Description',
compute='_compute_description',
inverse='_inverse_description',
search='_search_description',
compute_sudo=False)
name_validity = fields.Char('Description with validity', compute='_compute_description_validity')
active = fields.Boolean(default=True)
private_name = fields.Char('Allocation Description', groups='hr_holidays.group_hr_holidays_user')
state = fields.Selection([
('confirm', 'To Approve'),
('refuse', 'Refused'),
('validate', 'Approved')],
string='Status', readonly=True, tracking=True, copy=False, default='confirm',
help="The status is set to 'To Submit', when an allocation request is created."
"\nThe status is 'To Approve', when an allocation request is confirmed by user."
"\nThe status is 'Refused', when an allocation request is refused by manager."
"\nThe status is 'Approved', when an allocation request is approved by manager.")
date_from = fields.Date('Start Date', index=True, copy=False, default=fields.Date.context_today,
tracking=True, required=True)
date_to = fields.Date('End Date', copy=False, tracking=True)
holiday_status_id = fields.Many2one(
"hr.leave.type", compute='_compute_holiday_status_id', store=True, string="Time Off Type", required=True, readonly=False,
domain=_domain_holiday_status_id,
default=_default_holiday_status_id)
employee_id = fields.Many2one(
'hr.employee', compute='_compute_from_employee_ids', store=True, string='Employee', index=True, readonly=False, ondelete="restrict", tracking=True)
employee_company_id = fields.Many2one(related='employee_id.company_id', readonly=True, store=True)
active_employee = fields.Boolean('Active Employee', related='employee_id.active', readonly=True)
manager_id = fields.Many2one('hr.employee', compute='_compute_manager_id', store=True, string='Manager')
notes = fields.Text('Reasons', readonly=False)
# duration
number_of_days = fields.Float(
'Number of Days', compute='_compute_from_holiday_status_id', store=True, readonly=False, tracking=True, default=1,
help='Duration in days. Reference field to use when necessary.')
number_of_days_display = fields.Float(
'Duration (days)', compute='_compute_number_of_days_display',
help="For an Accrual Allocation, this field contains the theorical amount of time given to the employee, due to a previous start date, on the first run of the plan. This can be manually edited.")
number_of_hours_display = fields.Float(
'Duration (hours)', compute='_compute_number_of_hours_display',
help="For an Accrual Allocation, this field contains the theorical amount of time given to the employee, due to a previous start date, on the first run of the plan. This can be manually edited.")
duration_display = fields.Char('Allocated (Days/Hours)', compute='_compute_duration_display',
help="Field allowing to see the allocation duration in days or hours depending on the type_request_unit")
# details
parent_id = fields.Many2one('hr.leave.allocation', string='Parent')
linked_request_ids = fields.One2many('hr.leave.allocation', 'parent_id', string='Linked Requests')
approver_id = fields.Many2one(
'hr.employee', string='First Approval', readonly=True, copy=False,
help='This area is automatically filled by the user who validates the allocation')
validation_type = fields.Selection(string='Validation Type', related='holiday_status_id.allocation_validation_type', readonly=True)
can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve')
type_request_unit = fields.Selection([
('hour', 'Hours'),
('half_day', 'Half Day'),
('day', 'Day'),
], compute="_compute_type_request_unit")
# mode
holiday_type = fields.Selection([
('employee', 'By Employee'),
('company', 'By Company'),
('department', 'By Department'),
('category', 'By Employee Tag')],
string='Allocation Mode', readonly=False, required=True, default='employee',
help="Allow to create requests in batchs:\n- By Employee: for a specific employee"
"\n- By Company: all employees of the specified company"
"\n- By Department: all employees of the specified department"
"\n- By Employee Tag: all employees of the specific employee group category")
employee_ids = fields.Many2many(
'hr.employee', compute='_compute_from_holiday_type', store=True, string='Employees', readonly=False)
multi_employee = fields.Boolean(
compute='_compute_from_employee_ids', store=True,
help='Holds whether this allocation concerns more than 1 employee')
mode_company_id = fields.Many2one(
'res.company', compute='_compute_from_holiday_type', store=True, string='Company Mode', readonly=False)
department_id = fields.Many2one(
'hr.department', compute='_compute_department_id', store=True, string='Department',
readonly=False)
category_id = fields.Many2one(
'hr.employee.category', compute='_compute_from_holiday_type', store=True, string='Employee Tag', readonly=False)
# accrual configuration
lastcall = fields.Date("Date of the last accrual allocation", readonly=True)
nextcall = fields.Date("Date of the next accrual allocation", readonly=True, default=False)
already_accrued = fields.Boolean()
allocation_type = fields.Selection([
('regular', 'Regular Allocation'),
('accrual', 'Accrual Allocation')
], string="Allocation Type", default="regular", required=True, readonly=True)
is_officer = fields.Boolean(compute='_compute_is_officer')
accrual_plan_id = fields.Many2one('hr.leave.accrual.plan',
compute="_compute_from_holiday_status_id", store=True, readonly=False, tracking=True,
domain="['|', ('time_off_type_id', '=', False), ('time_off_type_id', '=', holiday_status_id)]")
max_leaves = fields.Float(compute='_compute_leaves')
leaves_taken = fields.Float(compute='_compute_leaves', string='Time off Taken')
has_accrual_plan = fields.Boolean(compute='_compute_has_accrual_plan', string='Accrual Plan Available')
_sql_constraints = [
('type_value',
"CHECK( (holiday_type='employee' AND (employee_id IS NOT NULL OR multi_employee IS TRUE)) or "
"(holiday_type='category' AND category_id IS NOT NULL) or "
"(holiday_type='department' AND department_id IS NOT NULL) or "
"(holiday_type='company' AND mode_company_id IS NOT NULL))",
"The employee, department, company or employee category of this request is missing. Please make sure that your user login is linked to an employee."),
('duration_check', "CHECK( ( number_of_days > 0 AND allocation_type='regular') or (allocation_type != 'regular'))", "The duration must be greater than 0."),
]
@api.constrains('date_from', 'date_to')
def _check_date_from_date_to(self):
if any(allocation.date_to and allocation.date_from > allocation.date_to for allocation in self):
raise UserError(_("The Start Date of the Validity Period must be anterior to the End Date."))
# The compute does not get triggered without a depends on record creation
# aka keep the 'useless' depends
@api.depends_context('uid')
@api.depends('allocation_type')
def _compute_is_officer(self):
self.is_officer = self.env.user.has_group("hr_holidays.group_hr_holidays_user")
# Useless depends, so that name is computed on new, before saving the record
@api.depends_context('uid')
@api.depends('holiday_status_id')
def _compute_description(self):
self.check_access_rights('read')
self.check_access_rule('read')
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
for allocation in self:
if is_officer or allocation.employee_id.user_id == self.env.user or allocation.employee_id.leave_manager_id == self.env.user:
title = allocation.sudo().private_name
if allocation.env.context.get('is_employee_allocation'):
if allocation.holiday_status_id:
allocation_duration = allocation.number_of_days_display if allocation.type_request_unit != 'hour' else allocation.number_of_hours_display
title = _("%s allocation request (%s %s)",
allocation.holiday_status_id.name,
allocation_duration,
allocation.type_request_unit)
else:
title = _("Allocation Request")
allocation.name = title
else:
allocation.name = '*****'
def _inverse_description(self):
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
for allocation in self:
if is_officer or allocation.employee_id.user_id == self.env.user or allocation.employee_id.leave_manager_id == self.env.user:
allocation.sudo().private_name = allocation.name
def _search_description(self, operator, value):
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
domain = [('private_name', operator, value)]
if not is_officer:
domain = expression.AND([domain, [('employee_id.user_id', '=', self.env.user.id)]])
allocations = self.sudo().search(domain)
return [('id', 'in', allocations.ids)]
@api.depends('accrual_plan_id')
def _compute_has_accrual_plan(self):
self.has_accrual_plan = bool(self.env['hr.leave.accrual.plan'].sudo().search_count([('active', '=', True)]))
@api.depends('name', 'date_from', 'date_to')
def _compute_description_validity(self):
for allocation in self:
if allocation.date_to:
name_validity = _("%s (from %s to %s)", allocation.name, allocation.date_from.strftime("%b %d %Y"), allocation.date_to.strftime("%b %d %Y"))
else:
name_validity = _("%s (from %s to No Limit)", allocation.name, allocation.date_from.strftime("%b %d %Y"))
allocation.name_validity = name_validity
@api.depends('employee_id', 'holiday_status_id')
def _compute_leaves(self):
date_from = fields.Date.from_string(self._context['default_date_from']) if 'default_date_from' in self._context else fields.Date.today()
employee_days_per_allocation = self.employee_id._get_consumed_leaves(self.holiday_status_id, date_from, ignore_future=True)[0]
for allocation in self:
allocation.max_leaves = allocation.number_of_hours_display if allocation.type_request_unit == 'hour' else allocation.number_of_days
allocation.leaves_taken = employee_days_per_allocation[allocation.employee_id][allocation.holiday_status_id][allocation]['leaves_taken']
@api.depends('number_of_days')
def _compute_number_of_days_display(self):
for allocation in self:
allocation.number_of_days_display = allocation.number_of_days
@api.depends('number_of_days', 'holiday_status_id', 'employee_id', 'holiday_type')
def _compute_number_of_hours_display(self):
for allocation in self:
allocation_calendar = allocation.holiday_status_id.company_id.resource_calendar_id
if allocation.holiday_type == 'employee' and allocation.employee_id:
allocation_calendar = allocation.employee_id.sudo().resource_calendar_id
allocation.number_of_hours_display = allocation.number_of_days * (allocation_calendar.hours_per_day or HOURS_PER_DAY)
@api.depends('number_of_hours_display', 'number_of_days_display')
def _compute_duration_display(self):
for allocation in self:
allocation.duration_display = '%g %s' % (
(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)),
_('hours') if allocation.type_request_unit == 'hour' else _('days'))
@api.depends('state', 'employee_id', 'department_id')
def _compute_can_approve(self):
for allocation in self:
try:
if allocation.state == 'confirm' and allocation.validation_type != 'no':
allocation._check_approval_update('validate')
except (AccessError, UserError):
allocation.can_approve = False
else:
allocation.can_approve = True
@api.depends('employee_ids')
def _compute_from_employee_ids(self):
for allocation in self:
if len(allocation.employee_ids) == 1:
allocation.employee_id = allocation.employee_ids[0]._origin
else:
allocation.employee_id = False
allocation.multi_employee = (len(allocation.employee_ids) > 1)
@api.depends('holiday_type')
def _compute_from_holiday_type(self):
default_employee_ids = self.env['hr.employee'].browse(self.env.context.get('default_employee_id')) or self.env.user.employee_id
for allocation in self:
if allocation.holiday_type == 'employee':
if not allocation.employee_ids:
allocation.employee_ids = self.env.user.employee_id
allocation.mode_company_id = False
allocation.category_id = False
elif allocation.holiday_type == 'company':
allocation.employee_ids = False
if not allocation.mode_company_id:
allocation.mode_company_id = self.env.company
allocation.category_id = False
elif allocation.holiday_type == 'department':
allocation.employee_ids = False
allocation.mode_company_id = False
allocation.category_id = False
elif allocation.holiday_type == 'category':
allocation.employee_ids = False
allocation.mode_company_id = False
else:
allocation.employee_ids = default_employee_ids
@api.depends('holiday_type', 'employee_id')
def _compute_department_id(self):
for allocation in self:
if allocation.holiday_type == 'employee':
allocation.department_id = allocation.employee_id.department_id
elif allocation.holiday_type == 'department':
if not allocation.department_id:
allocation.department_id = self.env.user.employee_id.department_id
elif allocation.holiday_type == 'category':
allocation.department_id = False
@api.depends('employee_id')
def _compute_manager_id(self):
for allocation in self:
allocation.manager_id = allocation.employee_id and allocation.employee_id.parent_id
@api.depends('accrual_plan_id')
def _compute_holiday_status_id(self):
default_holiday_status_id = None
for allocation in self:
if not allocation.holiday_status_id:
if allocation.accrual_plan_id:
allocation.holiday_status_id = allocation.accrual_plan_id.time_off_type_id
else:
if not default_holiday_status_id: # fetch when we need it
default_holiday_status_id = self._default_holiday_status_id()
allocation.holiday_status_id = default_holiday_status_id
@api.depends('holiday_status_id', 'allocation_type', 'number_of_hours_display', 'number_of_days_display', 'date_to')
def _compute_from_holiday_status_id(self):
accrual_allocations = self.filtered(lambda alloc: alloc.allocation_type == 'accrual' and not alloc.accrual_plan_id and alloc.holiday_status_id)
accruals_read_group = self.env['hr.leave.accrual.plan']._read_group(
[('time_off_type_id', 'in', accrual_allocations.holiday_status_id.ids)],
['time_off_type_id'],
['id:array_agg'],
)
accruals_dict = {time_off_type.id: ids for time_off_type, ids in accruals_read_group}
for allocation in self:
allocation_unit = allocation._get_request_unit()
if allocation_unit != 'hour':
allocation.number_of_days = allocation.number_of_days_display
else:
hours_per_day = allocation.employee_id.sudo().resource_calendar_id.hours_per_day\
or allocation.holiday_status_id.company_id.resource_calendar_id.hours_per_day\
or HOURS_PER_DAY
allocation.number_of_days = allocation.number_of_hours_display / hours_per_day
if allocation.accrual_plan_id.time_off_type_id.id not in (False, allocation.holiday_status_id.id):
allocation.accrual_plan_id = False
if allocation.allocation_type == 'accrual' and not allocation.accrual_plan_id:
if allocation.holiday_status_id:
allocation.accrual_plan_id = accruals_dict.get(allocation.holiday_status_id.id, [False])[0]
def _get_request_unit(self):
self.ensure_one()
if self.allocation_type == "accrual" and self.accrual_plan_id:
return self.accrual_plan_id.sudo().added_value_type
elif self.allocation_type == "regular":
return self.holiday_status_id.request_unit
else:
return "day"
@api.depends("allocation_type", "holiday_status_id", "accrual_plan_id")
def _compute_type_request_unit(self):
for allocation in self:
allocation.type_request_unit = allocation._get_request_unit()
def _get_carryover_date(self, date_from):
self.ensure_one()
carryover_time = self.accrual_plan_id.carryover_date
accrual_plan = self.accrual_plan_id
carryover_date = False
if carryover_time == 'year_start':
carryover_date = date(date_from.year, 1, 1)
elif carryover_time == 'allocation':
carryover_date = date(date_from.year, self.date_from.month, self.date_from.day)
else:
carryover_date = date(date_from.year, MONTHS_TO_INTEGER[accrual_plan.carryover_month], accrual_plan.carryover_day)
if date_from > carryover_date:
carryover_date += relativedelta(years=1)
return carryover_date
def _add_days_to_allocation(self, current_level, current_level_maximum_leave, leaves_taken, period_start, period_end):
days_to_add = self._process_accrual_plan_level(
current_level, period_start, self.lastcall, period_end, self.nextcall)
self.number_of_days += days_to_add
if current_level.cap_accrued_time:
self.number_of_days = min(self.number_of_days, current_level_maximum_leave + leaves_taken)
def _get_current_accrual_plan_level_id(self, date, level_ids=False):
"""
Returns a pair (accrual_plan_level, idx) where accrual_plan_level is the level for the given date
and idx is the index for the plan in the ordered set of levels
"""
self.ensure_one()
if not self.accrual_plan_id.level_ids:
return (False, False)
# Sort by sequence which should be equivalent to the level
if not level_ids:
level_ids = self.accrual_plan_id.level_ids.sorted('sequence')
current_level = False
current_level_idx = -1
for idx, level in enumerate(level_ids):
if date > self.date_from + get_timedelta(level.start_count, level.start_type):
current_level = level
current_level_idx = idx
# If transition_mode is set to `immediately` or we are currently on the first level
# the current_level is simply the first level in the list.
if current_level_idx <= 0 or self.accrual_plan_id.transition_mode == "immediately":
return (current_level, current_level_idx)
# In this case we have to verify that the 'previous level' is not the current one due to `end_of_accrual`
level_start_date = self.date_from + get_timedelta(current_level.start_count, current_level.start_type)
previous_level = level_ids[current_level_idx - 1]
# If the next date from the current level's start date is before the last call of the previous level
# return the previous level
if current_level._get_next_date(level_start_date) < previous_level._get_next_date(level_start_date):
return (previous_level, current_level_idx - 1)
return (current_level, current_level_idx)
def _get_accrual_plan_level_work_entry_prorata(self, level, start_period, start_date, end_period, end_date):
self.ensure_one()
datetime_min_time = datetime.min.time()
start_dt = datetime.combine(start_date, datetime_min_time)
end_dt = datetime.combine(end_date, datetime_min_time)
worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\
[self.employee_id.id]['hours']
if start_period != start_date or end_period != end_date:
start_dt = datetime.combine(start_period, datetime_min_time)
end_dt = datetime.combine(end_period, datetime_min_time)
planned_worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\
[self.employee_id.id]['hours']
else:
planned_worked = worked
left = self.employee_id.sudo()._get_leave_days_data_batch(start_dt, end_dt,
domain=[('time_type', '=', 'leave')])[self.employee_id.id]['hours']
if level.frequency == 'hourly':
if level.accrual_plan_id.is_based_on_worked_time:
work_entry_prorata = planned_worked
else:
work_entry_prorata = planned_worked + left
else:
work_entry_prorata = worked / (left + planned_worked) if (left + planned_worked) else 0
return work_entry_prorata
def _process_accrual_plan_level(self, level, start_period, start_date, end_period, end_date):
"""
Returns the added days for that level
"""
self.ensure_one()
if level.frequency == 'hourly' or level.accrual_plan_id.is_based_on_worked_time:
work_entry_prorata = self._get_accrual_plan_level_work_entry_prorata(level, start_period, start_date, end_period, end_date)
added_value = work_entry_prorata * level.added_value
else:
added_value = level.added_value
# Convert time in hours to time in days in case the level is encoded in hours
if level.added_value_type == 'hour':
added_value = added_value / (self.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
period_prorata = 1
if (start_period != start_date or end_period != end_date) and not level.accrual_plan_id.is_based_on_worked_time:
period_days = (end_period - start_period)
call_days = (end_date - start_date)
period_prorata = min(1, call_days / period_days) if period_days else 1
return added_value * period_prorata
def _process_accrual_plans(self, date_to=False, force_period=False, log=True):
"""
This method is part of the cron's process.
The goal of this method is to retroactively apply accrual plan levels and progress from nextcall to date_to or today.
If force_period is set, the accrual will run until date_to in a prorated way (used for end of year accrual actions).
"""
date_to = date_to or fields.Date.today()
first_allocation = _("""This allocation have already ran once, any modification won't be effective to the days allocated to the employee. If you need to change the configuration of the allocation, delete and create a new one.""")
for allocation in self:
level_ids = allocation.accrual_plan_id.level_ids.sorted('sequence')
if not level_ids:
continue
# "cache" leaves taken, as it gets recomputed every time allocation.number_of_days is assigned to. Without this,
# every loop will take 1+ second. It can be removed if computes don't chain in a way to always reassign accrual plan
# even if the value doesn't change. This is the best performance atm.
first_level = level_ids[0]
first_level_start_date = allocation.date_from + get_timedelta(first_level.start_count, first_level.start_type)
leaves_taken = allocation.leaves_taken if first_level.added_value_type == "day" else allocation.leaves_taken / (self.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
# first time the plan is run, initialize nextcall and take carryover / level transition into account
if not allocation.nextcall:
# Accrual plan is not configured properly or has not started
if date_to < first_level_start_date:
continue
allocation.lastcall = max(allocation.lastcall, first_level_start_date)
allocation.nextcall = first_level._get_next_date(allocation.lastcall)
# adjust nextcall for carryover
carryover_date = allocation._get_carryover_date(allocation.nextcall)
allocation.nextcall = min(carryover_date, allocation.nextcall)
# adjust nextcall for level_transition
if len(level_ids) > 1:
second_level_start_date = allocation.date_from + get_timedelta(level_ids[1].start_count, level_ids[1].start_type)
allocation.nextcall = min(second_level_start_date, allocation.nextcall)
if log:
allocation._message_log(body=first_allocation)
(current_level, current_level_idx) = (False, 0)
current_level_maximum_leave = 0.0
# all subsequent runs, at every loop:
# get current level and normal period boundaries, then set nextcall, adjusted for level transition and carryover
# add days, trimmed if there is a maximum_leave
while allocation.nextcall <= date_to:
(current_level, current_level_idx) = allocation._get_current_accrual_plan_level_id(allocation.nextcall)
if not current_level:
break
if current_level.cap_accrued_time:
current_level_maximum_leave = current_level.maximum_leave if current_level.added_value_type == "day" else current_level.maximum_leave / (allocation.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
nextcall = current_level._get_next_date(allocation.nextcall)
# Since _get_previous_date returns the given date if it corresponds to a call date
# this will always return lastcall except possibly on the first call
# this is used to prorate the first number of days given to the employee
period_start = current_level._get_previous_date(allocation.lastcall)
period_end = current_level._get_next_date(allocation.lastcall)
# There are 2 cases where nextcall could be closer than the normal period:
# 1. Passing from one level to another, if mode is set to 'immediately'
if current_level_idx < (len(level_ids) - 1) and allocation.accrual_plan_id.transition_mode == 'immediately':
next_level = level_ids[current_level_idx + 1]
current_level_last_date = allocation.date_from + get_timedelta(next_level.start_count, next_level.start_type)
if allocation.nextcall != current_level_last_date:
nextcall = min(nextcall, current_level_last_date)
# 2. On carry-over date
carryover_date = allocation._get_carryover_date(allocation.nextcall)
if allocation.nextcall < carryover_date < nextcall:
nextcall = min(nextcall, carryover_date)
if not allocation.already_accrued:
allocation._add_days_to_allocation(current_level, current_level_maximum_leave, leaves_taken, period_start, period_end)
# if it's the carry-over date, adjust days using current level's carry-over policy, then continue
if allocation.nextcall == carryover_date:
if current_level.action_with_unused_accruals in ['lost', 'maximum']:
allocation_days = allocation.number_of_days + leaves_taken
allocation_max_days = current_level.postpone_max_days + leaves_taken
allocation.number_of_days = min(allocation_days, allocation_max_days)
allocation.lastcall = allocation.nextcall
allocation.nextcall = nextcall
allocation.already_accrued = False
if force_period and allocation.nextcall > date_to:
allocation.nextcall = date_to
force_period = False
# if plan.accrued_gain_time == 'start', process next period and set flag 'already_accrued', this will skip adding days
# once, preventing double allocation.
if allocation.accrual_plan_id.accrued_gain_time == 'start':
# check that we are at the start of a period, not on a carry-over or level transition date
current_level = current_level or allocation.accrual_plan_id.level_ids[0]
period_start = current_level._get_previous_date(allocation.lastcall)
if allocation.lastcall != period_start:
continue
if current_level.cap_accrued_time:
current_level_maximum_leave = current_level.maximum_leave if current_level.added_value_type == "day" else current_level.maximum_leave / (allocation.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
allocation._add_days_to_allocation(current_level, current_level_maximum_leave, leaves_taken, allocation.lastcall, allocation.nextcall)
allocation.already_accrued = True
@api.model
def _update_accrual(self):
"""
Method called by the cron task in order to increment the number_of_days when
necessary.
"""
today = datetime.combine(fields.Date.today(), time(0, 0, 0))
allocations = self.search([
('allocation_type', '=', 'accrual'), ('state', '=', 'validate'),
('accrual_plan_id', '!=', False), ('employee_id', '!=', False),
'|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()),
'|', ('nextcall', '=', False), ('nextcall', '<=', today)])
allocations._process_accrual_plans()
def _get_future_leaves_on(self, accrual_date):
# As computing future accrual allocation days automatically updates the allocation,
# We need to create a temporary copy of that allocation to return the difference in number of days
# to see how much more days will be allocated from now until that date.
self.ensure_one()
if not accrual_date or accrual_date <= date.today():
return 0
if not (self.accrual_plan_id
and self.state == 'validate'
and self.allocation_type == 'accrual'
and (not self.date_to or self.date_to > accrual_date)
and (not self.nextcall or self.nextcall <= accrual_date)):
return 0
fake_allocation = self.env['hr.leave.allocation'].new(origin=self)
fake_allocation.sudo()._process_accrual_plans(accrual_date, log=False)
if self.type_request_unit in ['hour']:
return float_round(fake_allocation.number_of_hours_display - self.number_of_hours_display, precision_digits=2)
return round((fake_allocation.number_of_days - self.number_of_days), 2)
####################################################
# ORM Overrides methods
####################################################
def onchange(self, values, field_names, fields_spec):
# Try to force the leave_type display_name when creating new records
# This is called right after pressing create and returns the display_name for
# most fields in the view.
if values and 'employee_id' in fields_spec and 'employee_id' not in self._context:
employee_id = get_employee_from_context(values, self._context, self.env.user.employee_id.id)
self = self.with_context(employee_id=employee_id)
return super().onchange(values, field_names, fields_spec)
@api.depends(
'holiday_type', 'mode_company_id', 'department_id',
'category_id', 'employee_id', 'holiday_status_id',
'type_request_unit', 'number_of_days',
)
def _compute_display_name(self):
for allocation in self:
if allocation.holiday_type == 'company':
target = allocation.mode_company_id.name
elif allocation.holiday_type == 'department':
target = allocation.department_id.name
elif allocation.holiday_type == 'category':
target = allocation.category_id.name
elif allocation.employee_id:
target = allocation.employee_id.name
elif len(allocation.employee_ids) <= 3:
target = ', '.join(allocation.employee_ids.sudo().mapped('name'))
else:
target = _('%(first)s, %(second)s and %(amount)s others',
first=allocation.employee_ids[0].sudo().name,
second=allocation.employee_ids[1].sudo().name,
amount=len(allocation.employee_ids) - 2)
allocation.display_name = _("Allocation of %s: %.2f %s to %s",
allocation.holiday_status_id.sudo().name,
allocation.number_of_hours_display if allocation.type_request_unit == 'hour' else allocation.number_of_days,
_('hours') if allocation.type_request_unit == 'hour' else _('days'),
target,
)
def _add_lastcalls(self):
for allocation in self:
if allocation.allocation_type != 'accrual':
continue
today = fields.Date.today()
(current_level, current_level_idx) = allocation._get_current_accrual_plan_level_id(today)
if not allocation.lastcall:
if not current_level:
allocation.lastcall = today
continue
allocation.lastcall = max(
current_level._get_previous_date(today),
allocation.date_from + get_timedelta(current_level.start_count, current_level.start_type)
)
if not allocation.nextcall and allocation.lastcall < today:
accrual_plan = allocation.accrual_plan_id
next_level = False
allocation.nextcall = current_level._get_next_date(allocation.lastcall)
if current_level_idx < (len(accrual_plan.level_ids) - 1) and accrual_plan.transition_mode == 'immediately':
next_level = allocation.accrual_plan_id.level_ids[current_level_idx + 1]
next_level_start = allocation.date_from + get_timedelta(next_level.start_count, next_level.start_type)
allocation.nextcall = min(allocation.nextcall, next_level_start)
def add_follower(self, employee_id):
employee = self.env['hr.employee'].browse(employee_id)
if employee.user_id:
self.message_subscribe(partner_ids=employee.user_id.partner_id.ids)
@api.model_create_multi
def create(self, vals_list):
""" Override to avoid automatic logging of creation """
for values in vals_list:
if 'state' in values and values['state'] not in ('draft', 'confirm'):
raise UserError(_('Incorrect state for new allocation'))
employee_id = values.get('employee_id', False)
if not values.get('department_id'):
values.update({'department_id': self.env['hr.employee'].browse(employee_id).department_id.id})
allocations = super(HolidaysAllocation, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
allocations._add_lastcalls()
for allocation in allocations:
partners_to_subscribe = set()
if allocation.employee_id.user_id:
partners_to_subscribe.add(allocation.employee_id.user_id.partner_id.id)
if allocation.validation_type == 'officer':
partners_to_subscribe.add(allocation.employee_id.parent_id.user_id.partner_id.id)
partners_to_subscribe.add(allocation.employee_id.leave_manager_id.partner_id.id)
allocation.message_subscribe(partner_ids=tuple(partners_to_subscribe))
if not self._context.get('import_file'):
allocation.activity_update()
if allocation.validation_type == 'no' and allocation.state == 'confirm':
allocation.action_validate()
return allocations
def write(self, values):
if not self.env.context.get('toggle_active') and not bool(values.get('active', True)):
if any(allocation.state not in ['refuse'] for allocation in self):
raise UserError(_('You cannot archive an allocation which is in confirm or validate state.'))
employee_id = values.get('employee_id', False)
if values.get('state'):
self._check_approval_update(values['state'])
result = super(HolidaysAllocation, self).write(values)
self.add_follower(employee_id)
return result
@api.ondelete(at_uninstall=False)
def _unlink_if_correct_states(self):
state_description_values = {elem[0]: elem[1] for elem in self._fields['state']._description_selection(self.env)}
for allocation in self.filtered(lambda allocation: allocation.state not in ['confirm', 'refuse']):
raise UserError(_('You cannot delete an allocation request which is in %s state.', state_description_values.get(allocation.state)))
@api.ondelete(at_uninstall=False)
def _unlink_if_no_leaves(self):
if any(allocation.holiday_status_id.requires_allocation == 'yes' and allocation.leaves_taken > 0 for allocation in self):
raise UserError(_('You cannot delete an allocation request which has some validated leaves.'))
def _get_mail_redirect_suggested_company(self):
return self.holiday_status_id.company_id
####################################################
# Business methods
####################################################
def _prepare_holiday_values(self, employees):
self.ensure_one()
return [{
'name': self.name,
'holiday_type': 'employee',
'holiday_status_id': self.holiday_status_id.id,
'notes': self.notes,
'number_of_days': self.number_of_days,
'parent_id': self.id,
'employee_id': employee.id,
'employee_ids': [(6, 0, [employee.id])],
'state': 'confirm',
'allocation_type': self.allocation_type,
'date_from': self.date_from,
'date_to': self.date_to,
'accrual_plan_id': self.accrual_plan_id.id,
} for employee in employees]
def action_validate(self):
to_validate = self.filtered(lambda alloc: alloc.state != 'validate')
if to_validate:
to_validate.write({
'state': 'validate',
'approver_id': self.env.user.employee_id.id
})
to_validate._action_validate_create_childs()
to_validate.activity_update()
return True
def _action_validate_create_childs(self):
allocation_vals = []
for allocation in self:
# In the case we are in holiday_type `employee` and there is only one employee we can keep the same allocation
# Otherwise we do need to create an allocation for all employees to have a behaviour that is in line
# with the other holiday_type
if allocation.state == 'validate' and (allocation.holiday_type in ['category', 'department', 'company'] or
(allocation.holiday_type == 'employee' and len(allocation.employee_ids) > 1)):
if allocation.holiday_type == 'employee':
employees = allocation.employee_ids
elif allocation.holiday_type == 'category':
employees = allocation.category_id.employee_ids
elif allocation.holiday_type == 'department':
employees = allocation.department_id.member_ids
else:
employees = self.env['hr.employee'].search([('company_id', '=', allocation.mode_company_id.id)])
allocation_vals += allocation._prepare_holiday_values(employees)
if allocation_vals:
children = self.env['hr.leave.allocation'].with_context(
mail_notify_force_send=False,
mail_activity_automation_skip=True
).create(allocation_vals)
children.filtered(lambda c: c.validation_type != 'no').action_validate()
def action_refuse(self):
current_employee = self.env.user.employee_id
if any(allocation.state not in ['confirm', 'validate'] for allocation in self):
raise UserError(_('Allocation request must be confirmed or validated in order to refuse it.'))
days_per_allocation = self.employee_id._get_consumed_leaves(self.holiday_status_id)[0]
for allocation in self:
days_taken = days_per_allocation[allocation.employee_id][allocation.holiday_status_id][allocation]['virtual_leaves_taken']
if days_taken > 0:
raise UserError(_('You cannot refuse this allocation request since the employee has already taken leaves for it. Please refuse or delete those leaves first.'))
self.write({'state': 'refuse', 'approver_id': current_employee.id})
# If a category that created several holidays, cancel all related
linked_requests = self.mapped('linked_request_ids')
if linked_requests:
linked_requests.action_refuse()
self.activity_update()
return True
def _check_approval_update(self, state):
""" Check if target state is achievable. """
if self.env.is_superuser():
return
current_employee = self.env.user.employee_id
if not current_employee:
return
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
is_manager = self.env.user.has_group('hr_holidays.group_hr_holidays_manager')
for allocation in self:
val_type = allocation.holiday_status_id.sudo().allocation_validation_type
if state == 'confirm':
continue
if not is_officer and self.env.user != allocation.employee_id.leave_manager_id and not val_type == 'no':
raise UserError(_('Only a time off Officer/Responsible or Manager can approve or refuse time off requests.'))
if is_officer or self.env.user == allocation.employee_id.leave_manager_id:
# use ir.rule based first access check: department, members, ... (see security.xml)
allocation.check_access_rule('write')
if allocation.employee_id == current_employee and not is_manager and not val_type == 'no':
raise UserError(_('Only a time off Manager can approve its own requests.'))
@api.onchange('allocation_type')
def _onchange_allocation_type(self):
if self.allocation_type == 'accrual':
self.number_of_days = 0.0
elif not self.number_of_days_display:
self.number_of_days = 1.0
# Allows user to simulate how many days an accrual plan would give from a certain start date.
# it uses the actual computation function but resets values of lastcall, nextcall and nbr of days
# before every run, as if it was run from date_from, after an optional change in the allocation value
# the user can simply confirm and validate the allocation. The record is in correct state for the next
# call of the cron job.
@api.onchange('date_from', 'accrual_plan_id')
def _onchange_date_from(self):
now = date.today()
if self.allocation_type != 'accrual' or self.state == 'validate' or not self.accrual_plan_id\
or not self.employee_id or not (not self.date_to or self.date_to > now):
return
self.lastcall = self.date_from
self.nextcall = False
self.number_of_days_display = 0.0
self._process_accrual_plans()
# ------------------------------------------------------------
# Activity methods
# ------------------------------------------------------------
def _get_responsible_for_approval(self):
self.ensure_one()
responsible = self.env.user
if self.validation_type == 'officer' or self.validation_type == 'set':
if self.holiday_status_id.responsible_ids:
responsible = self.holiday_status_id.responsible_ids
return responsible
def activity_update(self):
to_clean, to_do = self.env['hr.leave.allocation'], self.env['hr.leave.allocation']
activity_vals = []
for allocation in self:
if allocation.validation_type != 'no':
note = _(
'New Allocation Request created by %(user)s: %(count)s Days of %(allocation_type)s',
user=allocation.create_uid.name,
count=allocation.number_of_days,
allocation_type=allocation.holiday_status_id.name
)
if allocation.state == 'confirm':
if allocation.holiday_status_id.responsible_ids:
user_ids = allocation.sudo()._get_responsible_for_approval().ids
for user_id in user_ids:
activity_vals.append({
'activity_type_id': self.env.ref('hr_holidays.mail_act_leave_allocation_approval').id,
'automated': True,
'note': note,
'user_id': user_id,
'res_id': allocation.id,
'res_model_id': self.env.ref('hr_holidays.model_hr_leave_allocation').id,
})
elif allocation.state == 'validate':
to_do |= allocation
elif allocation.state == 'refuse':
to_clean |= allocation
if activity_vals:
self.env['mail.activity'].create(activity_vals)
if to_clean:
to_clean.activity_unlink(['hr_holidays.mail_act_leave_allocation_approval'])
if to_do:
to_do.activity_feedback(['hr_holidays.mail_act_leave_allocation_approval'])
####################################################
# Messaging methods
####################################################
def _track_subtype(self, init_values):
if 'state' in init_values and self.state == 'validate':
allocation_notif_subtype_id = self.holiday_status_id.allocation_notif_subtype_id
return allocation_notif_subtype_id or self.env.ref('hr_holidays.mt_leave_allocation')
return super(HolidaysAllocation, self)._track_subtype(init_values)
def _notify_get_recipients_groups(self, message, model_description, msg_vals=None):
""" Handle HR users and officers recipients that can validate or refuse holidays
directly from email. """
groups = super()._notify_get_recipients_groups(
message, model_description, msg_vals=msg_vals
)
if not self:
return groups
local_msg_vals = dict(msg_vals or {})
self.ensure_one()
hr_actions = []
if self.state == 'confirm':
app_action = self._notify_get_action_link('controller', controller='/allocation/validate', **local_msg_vals)
hr_actions += [{'url': app_action, 'title': _('Approve')}]
if self.state in ['confirm', 'validate']:
ref_action = self._notify_get_action_link('controller', controller='/allocation/refuse', **local_msg_vals)
hr_actions += [{'url': ref_action, 'title': _('Refuse')}]
holiday_user_group_id = self.env.ref('hr_holidays.group_hr_holidays_user').id
new_group = (
'group_hr_holidays_user',
lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'],
{
'actions': hr_actions,
'active': True,
'has_button_access': True,
}
)
return [new_group] + groups
def message_subscribe(self, partner_ids=None, subtype_ids=None):
# due to record rule can not allow to add follower and mention on validated leave so subscribe through sudo
if any(state in ['validate'] for state in self.mapped('state')):
self.check_access_rights('read')
self.check_access_rule('read')
return super(HolidaysAllocation, self.sudo()).message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
return super().message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)

View File

@ -0,0 +1,24 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from random import randint
from odoo import fields, models
class MandatoryDay(models.Model):
_name = 'hr.leave.mandatory.day'
_description = 'Mandatory Day'
_order = 'start_date desc, end_date desc'
name = fields.Char(required=True)
company_id = fields.Many2one('res.company', default=lambda self: self.env.company, required=True)
start_date = fields.Date(required=True)
end_date = fields.Date(required=True)
color = fields.Integer(default=lambda dummy: randint(1, 11))
resource_calendar_id = fields.Many2one(
'resource.calendar', 'Working Hours',
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
department_ids = fields.Many2many('hr.department', string="Departments")
_sql_constraints = [
('date_from_after_day_to', 'CHECK(start_date <= end_date)', 'The start date must be anterior than the end date.')
]

502
models/hr_leave_type.py Normal file
View File

@ -0,0 +1,502 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
import logging
import pytz
from collections import defaultdict
from datetime import time, datetime
from odoo import api, fields, models
from odoo.tools import format_date
from odoo.tools.translate import _
from odoo.tools.float_utils import float_round
_logger = logging.getLogger(__name__)
class HolidaysType(models.Model):
_name = "hr.leave.type"
_description = "Time Off Type"
_order = 'sequence'
@api.model
def _model_sorting_key(self, leave_type):
remaining = leave_type.virtual_remaining_leaves > 0
taken = leave_type.leaves_taken > 0
return -1 * leave_type.sequence, leave_type.employee_requests == 'no' and remaining, leave_type.employee_requests == 'yes' and remaining, taken
name = fields.Char('Time Off Type', required=True, translate=True)
sequence = fields.Integer(default=100,
help='The type with the smallest sequence is the default value in time off request')
create_calendar_meeting = fields.Boolean(string="Display Time Off in Calendar", default=True)
color = fields.Integer(string='Color', help="The color selected here will be used in every screen with the time off type.")
icon_id = fields.Many2one('ir.attachment', string='Cover Image', domain="[('res_model', '=', 'hr.leave.type'), ('res_field', '=', 'icon_id')]")
active = fields.Boolean('Active', default=True,
help="If the active field is set to false, it will allow you to hide the time off type without removing it.")
# employee specific computed data
max_leaves = fields.Float(compute='_compute_leaves', string='Maximum Allowed', search='_search_max_leaves',
help='This value is given by the sum of all time off requests with a positive value.')
leaves_taken = fields.Float(
compute='_compute_leaves', string='Time off Already Taken',
help='This value is given by the sum of all time off requests with a negative value.')
virtual_remaining_leaves = fields.Float(
compute='_compute_leaves', search='_search_virtual_remaining_leaves', string='Virtual Remaining Time Off',
help='Maximum Time Off Allowed - Time Off Already Taken - Time Off Waiting Approval')
allocation_count = fields.Integer(
compute='_compute_allocation_count', string='Allocations')
group_days_leave = fields.Float(
compute='_compute_group_days_leave', string='Group Time Off')
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
responsible_ids = fields.Many2many(
'res.users', 'hr_leave_type_res_users_rel', 'hr_leave_type_id', 'res_users_id', string='Notified Time Off Officer',
domain=lambda self: [('groups_id', 'in', self.env.ref('hr_holidays.group_hr_holidays_user').id),
('share', '=', False),
('company_ids', 'in', self.env.company.id)],
auto_join=True,
help="Choose the Time Off Officers who will be notified to approve allocation or Time Off Request. If empty, nobody will be notified")
leave_validation_type = fields.Selection([
('no_validation', 'No Validation'),
('hr', 'By Time Off Officer'),
('manager', "By Employee's Approver"),
('both', "By Employee's Approver and Time Off Officer")], default='hr', string='Time Off Validation')
requires_allocation = fields.Selection([
('yes', 'Yes'),
('no', 'No Limit')], default="yes", required=True, string='Requires allocation',
help="""Yes: Time off requests need to have a valid allocation.\n
No Limit: Time Off requests can be taken without any prior allocation.""")
employee_requests = fields.Selection([
('yes', 'Extra Days Requests Allowed'),
('no', 'Not Allowed')], default="no", required=True, string="Employee Requests",
help="""Extra Days Requests Allowed: User can request an allocation for himself.\n
Not Allowed: User cannot request an allocation.""")
allocation_validation_type = fields.Selection([
('officer', 'Approved by Time Off Officer'),
('no', 'No validation needed')], default='no', string='Approval',
compute='_compute_allocation_validation_type', store=True, readonly=False,
help="""Select the level of approval needed in case of request by employee
- No validation needed: The employee's request is automatically approved.
- Approved by Time Off Officer: The employee's request need to be manually approved by the Time Off Officer.""")
has_valid_allocation = fields.Boolean(compute='_compute_valid', search='_search_valid', help='This indicates if it is still possible to use this type of leave')
time_type = fields.Selection([('other', 'Worked Time'), ('leave', 'Absence')], default='leave', string="Kind of Time Off",
help="The distinction between working time (ex. Attendance) and absence (ex. Training) will be used in the computation of Accrual's plan rate.")
request_unit = fields.Selection([
('day', 'Day'),
('half_day', 'Half Day'),
('hour', 'Hours')], default='day', string='Take Time Off in', required=True)
unpaid = fields.Boolean('Is Unpaid', default=False)
leave_notif_subtype_id = fields.Many2one('mail.message.subtype', string='Time Off Notification Subtype', default=lambda self: self.env.ref('hr_holidays.mt_leave', raise_if_not_found=False))
allocation_notif_subtype_id = fields.Many2one('mail.message.subtype', string='Allocation Notification Subtype', default=lambda self: self.env.ref('hr_holidays.mt_leave_allocation', raise_if_not_found=False))
support_document = fields.Boolean(string='Supporting Document')
accruals_ids = fields.One2many('hr.leave.accrual.plan', 'time_off_type_id')
accrual_count = fields.Float(compute="_compute_accrual_count", string="Accruals count")
# negative time off
allows_negative = fields.Boolean(string='Allow Negative Cap',
help="If checked, users request can exceed the allocated days and balance can go in negative.")
max_allowed_negative = fields.Integer(string="Amount in Negative",
help="Define the maximum level of negative days this kind of time off can reach. Value must be at least 1.")
_sql_constraints = [(
'check_negative',
'CHECK(NOT allows_negative OR max_allowed_negative > 0)',
'The negative amount must be greater than 0. If you want to set 0, disable the negative cap instead.'
)]
@api.model
def _search_valid(self, operator, value):
""" Returns leave_type ids for which a valid allocation exists
or that don't need an allocation
return [('id', domain_operator, [x['id'] for x in res])]
"""
date_from = self._context.get('default_date_from') or fields.Date.today().strftime('%Y-1-1')
date_to = self._context.get('default_date_to') or fields.Date.today().strftime('%Y-12-31')
employee_id = self._context.get('default_employee_id', self._context.get('employee_id')) or self.env.user.employee_id.id
if not isinstance(value, bool):
raise ValueError('Invalid value: %s' % (value))
if operator not in ['=', '!=']:
raise ValueError('Invalid operator: %s' % (operator))
# '!=' True or '=' False
if (operator == '=') ^ value:
new_operator = 'not in'
# '=' True or '!=' False
else:
new_operator = 'in'
leave_types = self.env['hr.leave.allocation'].search([
('employee_id', '=', employee_id),
('state', '=', 'validate'),
('date_from', '<=', date_to),
'|',
('date_to', '>=', date_from),
('date_to', '=', False),
]).holiday_status_id
return [('id', new_operator, leave_types.ids)]
@api.depends('requires_allocation', 'max_leaves', 'virtual_remaining_leaves')
def _compute_valid(self):
date_from = self._context.get('default_date_from', fields.Datetime.today())
date_to = self._context.get('default_date_to', fields.Datetime.today())
employee_id = self._context.get('default_employee_id', self._context.get('employee_id', self.env.user.employee_id.id))
for holiday_type in self:
if holiday_type.requires_allocation == 'yes':
allocations = self.env['hr.leave.allocation'].search([
('holiday_status_id', '=', holiday_type.id),
('allocation_type', '=', 'accrual'),
('employee_id', '=', employee_id),
('date_from', '<=', date_from),
'|',
('date_to', '>=', date_to),
('date_to', '=', False),
])
allowed_excess = holiday_type.max_allowed_negative if holiday_type.allows_negative else 0
allocations = allocations.filtered(lambda alloc:
alloc.allocation_type == 'accrual'
or (alloc.max_leaves > 0 and alloc.virtual_remaining_leaves > -allowed_excess)
)
holiday_type.has_valid_allocation = bool(allocations)
else:
holiday_type.has_valid_allocation = True
def _search_max_leaves(self, operator, value):
value = float(value)
employee = self.env['hr.employee']._get_contextual_employee()
leaves = defaultdict(int)
if employee:
allocations = self.env['hr.leave.allocation'].search([
('employee_id', '=', employee.id),
('state', '=', 'validate')
])
for allocation in allocations:
leaves[allocation.holiday_status_id.id] += allocation.number_of_days
valid_leave = []
for leave in leaves:
if operator == '>':
if leaves[leave] > value:
valid_leave.append(leave)
elif operator == '<':
if leaves[leave] < value:
valid_leave.append(leave)
elif operator == '=':
if leaves[leave] == value:
valid_leave.append(leave)
elif operator == '!=':
if leaves[leave] != value:
valid_leave.append(leave)
return [('id', 'in', valid_leave)]
def _search_virtual_remaining_leaves(self, operator, value):
value = float(value)
leave_types = self.env['hr.leave.type'].search([])
valid_leave_types = self.env['hr.leave.type']
for leave_type in leave_types:
if leave_type.requires_allocation == "yes":
if operator == '>' and leave_type.virtual_remaining_leaves > value:
valid_leave_types |= leave_type
elif operator == '<' and leave_type.virtual_remaining_leaves < value:
valid_leave_types |= leave_type
elif operator == '>=' and leave_type.virtual_remaining_leaves >= value:
valid_leave_types |= leave_type
elif operator == '<=' and leave_type.virtual_remaining_leaves <= value:
valid_leave_types |= leave_type
elif operator == '=' and leave_type.virtual_remaining_leaves == value:
valid_leave_types |= leave_type
elif operator == '!=' and leave_type.virtual_remaining_leaves != value:
valid_leave_types |= leave_type
else:
valid_leave_types |= leave_type
return [('id', 'in', valid_leave_types.ids)]
@api.depends_context('employee_id', 'default_employee_id', 'default_date_from')
def _compute_leaves(self):
employee = self.env['hr.employee']._get_contextual_employee()
target_date = self._context['default_date_from'] if 'default_date_from' in self._context else None
data_days = self.get_allocation_data(employee, target_date)[employee]
for holiday_status in self:
result = [item for item in data_days if item[0] == holiday_status.name]
leave_type_tuple = result[0] if result else ('', {})
holiday_status.max_leaves = leave_type_tuple[1].get('max_leaves', 0)
holiday_status.leaves_taken = leave_type_tuple[1].get('leaves_taken', 0)
holiday_status.virtual_remaining_leaves = leave_type_tuple[1].get('virtual_remaining_leaves', 0)
def _compute_allocation_count(self):
min_datetime = fields.Datetime.to_string(datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
max_datetime = fields.Datetime.to_string(datetime.now().replace(month=12, day=31, hour=23, minute=59, second=59))
domain = [
('holiday_status_id', 'in', self.ids),
('date_from', '>=', min_datetime),
('date_from', '<=', max_datetime),
('state', 'in', ('confirm', 'validate')),
]
grouped_res = self.env['hr.leave.allocation']._read_group(
domain,
['holiday_status_id'],
['__count'],
)
grouped_dict = {holiday_status.id: count for holiday_status, count in grouped_res}
for allocation in self:
allocation.allocation_count = grouped_dict.get(allocation.id, 0)
def _compute_group_days_leave(self):
min_datetime = fields.Datetime.to_string(datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
max_datetime = fields.Datetime.to_string(datetime.now().replace(month=12, day=31, hour=23, minute=59, second=59))
domain = [
('holiday_status_id', 'in', self.ids),
('date_from', '>=', min_datetime),
('date_from', '<=', max_datetime),
('state', 'in', ('validate', 'validate1', 'confirm')),
]
grouped_res = self.env['hr.leave']._read_group(
domain,
['holiday_status_id'],
['__count'],
)
grouped_dict = {holiday_status.id: count for holiday_status, count in grouped_res}
for allocation in self:
allocation.group_days_leave = grouped_dict.get(allocation.id, 0)
def _compute_accrual_count(self):
accrual_allocations = self.env['hr.leave.accrual.plan']._read_group([('time_off_type_id', 'in', self.ids)], ['time_off_type_id'], ['__count'])
mapped_data = {time_off_type.id: count for time_off_type, count in accrual_allocations}
for leave_type in self:
leave_type.accrual_count = mapped_data.get(leave_type.id, 0)
@api.depends('employee_requests')
def _compute_allocation_validation_type(self):
for leave_type in self:
if leave_type.employee_requests == 'no':
leave_type.allocation_validation_type = 'officer'
def requested_display_name(self):
return self._context.get('holiday_status_display_name', True) and self._context.get('employee_id')
@api.depends('requires_allocation', 'virtual_remaining_leaves', 'max_leaves', 'request_unit')
@api.depends_context('holiday_status_display_name', 'employee_id', 'from_manager_leave_form')
def _compute_display_name(self):
if not self.requested_display_name():
# leave counts is based on employee_id, would be inaccurate if not based on correct employee
return super()._compute_display_name()
for record in self:
name = record.name
if record.requires_allocation == "yes" and not self._context.get('from_manager_leave_form'):
name = "{name} ({count})".format(
name=name,
count=_('%g remaining out of %g') % (
float_round(record.virtual_remaining_leaves, precision_digits=2) or 0.0,
float_round(record.max_leaves, precision_digits=2) or 0.0,
) + (_(' hours') if record.request_unit == 'hour' else _(' days')),
)
record.display_name = name
@api.model
def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None):
""" Override _search to order the results, according to some employee.
The order is the following
- allocation fixed first, then allowing allocation, then free allocation
- virtual remaining leaves (higher the better, so using reverse on sorted)
This override is necessary because those fields are not stored and depends
on an employee_id given in context. This sort will be done when there
is an employee_id in context and that no other order has been given
to the method.
"""
employee = self.env['hr.employee']._get_contextual_employee()
if order == self._order and employee:
# retrieve all leaves, sort them, then apply offset and limit
leaves = self.browse(super()._search(domain, access_rights_uid=access_rights_uid))
leaves = leaves.sorted(key=self._model_sorting_key, reverse=True)
leaves = leaves[offset:(offset + limit) if limit else None]
return leaves._as_query()
return super()._search(domain, offset, limit, order, access_rights_uid)
def action_see_days_allocated(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.hr_leave_allocation_action_all")
action['domain'] = [
('holiday_status_id', 'in', self.ids),
]
action['context'] = {
'default_holiday_type': 'department',
'default_holiday_status_id': self.ids[0],
'search_default_approved_state': 1,
'search_default_year': 1,
}
return action
def action_see_group_leaves(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.hr_leave_action_action_approve_department")
action['domain'] = [
('holiday_status_id', '=', self.ids[0]),
]
action['context'] = {
'default_holiday_status_id': self.ids[0],
}
return action
def action_see_accrual_plans(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.open_view_accrual_plans")
action['domain'] = [
('time_off_type_id', '=', self.id),
]
action['context'] = {
'default_time_off_type_id': self.id,
}
return action
# ------------------------------------------------------------
# Leave - Allocation link methods
# ------------------------------------------------------------
@api.model
def get_allocation_data_request(self, target_date=None):
leave_types = self.search([
'|',
('company_id', 'in', self.env.context.get('allowed_company_ids')),
('company_id', '=', False),
], order='id')
employee = self.env['hr.employee']._get_contextual_employee()
if employee:
return leave_types.get_allocation_data(employee, target_date)[employee]
return []
def get_allocation_data(self, employees, target_date=None):
allocation_data = defaultdict(list)
if target_date and isinstance(target_date, str):
target_date = datetime.fromisoformat(target_date).date()
elif target_date and isinstance(target_date, datetime):
target_date = target_date.date()
elif not target_date:
target_date = fields.Date.today()
allocations_leaves_consumed, extra_data = employees.with_context(
ignored_leave_ids=self.env.context.get('ignored_leave_ids')
)._get_consumed_leaves(self, target_date)
leave_type_requires_allocation = self.filtered(lambda lt: lt.requires_allocation == 'yes')
for employee in employees:
for leave_type in leave_type_requires_allocation:
if len(allocations_leaves_consumed[employee][leave_type]) == 0:
continue
lt_info = (
leave_type.name,
{
'remaining_leaves': 0,
'virtual_remaining_leaves': 0,
'max_leaves': 0,
'accrual_bonus': 0,
'leaves_taken': 0,
'virtual_leaves_taken': 0,
'leaves_requested': 0,
'leaves_approved': 0,
'closest_allocation_remaining': 0,
'closest_allocation_expire': False,
'holds_changes': False,
'total_virtual_excess': 0,
'virtual_excess_data': {},
'exceeding_duration': extra_data[employee][leave_type]['exceeding_duration'],
'request_unit': leave_type.request_unit,
'icon': leave_type.sudo().icon_id.url,
'has_accrual_allocation': False,
'allows_negative': leave_type.allows_negative,
'max_allowed_negative': leave_type.max_allowed_negative,
},
leave_type.requires_allocation,
leave_type.id)
for excess_date, excess_days in extra_data[employee][leave_type]['excess_days'].items():
amount = excess_days['amount']
lt_info[1]['virtual_excess_data'].update({
excess_date.strftime('%Y-%m-%d'): excess_days
}),
if not leave_type.allows_negative:
continue
lt_info[1]['virtual_leaves_taken'] += amount
lt_info[1]['virtual_remaining_leaves'] -= amount
lt_info[1]['total_virtual_excess'] += amount
if excess_days['is_virtual']:
lt_info[1]['leaves_requested'] += amount
else:
lt_info[1]['leaves_approved'] += amount
lt_info[1]['leaves_taken'] += amount
lt_info[1]['remaining_leaves'] -= amount
allocations_now = self.env['hr.leave.allocation']
allocations_date = self.env['hr.leave.allocation']
allocations_with_remaining_leaves = self.env['hr.leave.allocation']
for allocation, data in allocations_leaves_consumed[employee][leave_type].items():
# We only need the allocation that are valid at the given date
if allocation:
if allocation.allocation_type == 'accrual':
lt_info[1]['has_accrual_allocation'] = True
today = fields.Date.today()
if allocation.date_from <= today and (not allocation.date_to or allocation.date_to >= today):
# we get each allocation available now to indicate visually if
# the future evaluation holds changes compared to now
allocations_now |= allocation
if allocation.date_from <= target_date and (not allocation.date_to or allocation.date_to >= target_date):
# we get each allocation available now to indicate visually if
# the future evaluation holds changes compared to now
allocations_date |= allocation
if allocation.date_from > target_date:
continue
if allocation.date_to and allocation.date_to < target_date:
continue
lt_info[1]['remaining_leaves'] += data['remaining_leaves']
lt_info[1]['virtual_remaining_leaves'] += data['virtual_remaining_leaves']
lt_info[1]['max_leaves'] += data['max_leaves']
lt_info[1]['accrual_bonus'] += data['accrual_bonus']
lt_info[1]['leaves_taken'] += data['leaves_taken']
lt_info[1]['virtual_leaves_taken'] += data['virtual_leaves_taken']
lt_info[1]['leaves_requested'] += data['virtual_leaves_taken'] - data['leaves_taken']
lt_info[1]['leaves_approved'] += data['leaves_taken']
if data['virtual_remaining_leaves'] > 0:
allocations_with_remaining_leaves |= allocation
closest_allocation = allocations_with_remaining_leaves[0] if allocations_with_remaining_leaves else self.env['hr.leave.allocation']
closest_allocations = allocations_with_remaining_leaves.filtered(lambda a: a.date_to == closest_allocation.date_to)
closest_allocation_remaining = 0
for closest_allocation in closest_allocations:
closest_allocation_remaining += allocations_leaves_consumed[employee][leave_type][closest_allocation]['virtual_remaining_leaves']
if closest_allocation.date_to:
closest_allocation_expire = format_date(self.env, closest_allocation.date_to)
calendar = employee.resource_calendar_id\
or employee.company_id.resource_calendar_id
# closest_allocation_duration corresponds to the time remaining before the allocation expires
closest_allocation_duration =\
calendar._attendance_intervals_batch(
datetime.combine(closest_allocation.date_to, time.min).replace(tzinfo=pytz.UTC),
datetime.combine(target_date, time.max).replace(tzinfo=pytz.UTC))\
if leave_type.request_unit in ['hour']\
else (closest_allocation.date_to - target_date).days + 1
else:
closest_allocation_expire = False
closest_allocation_duration = False
# the allocations are assumed to be different from today's allocations if there is any
# accrual days granted or if there is any difference between allocations now and on the selected date
holds_changes = (lt_info[1]['accrual_bonus'] > 0
or bool(allocations_date - allocations_now)
or bool(allocations_now - allocations_date))\
and target_date != fields.Date.today()
lt_info[1].update({
'closest_allocation_remaining': closest_allocation_remaining,
'closest_allocation_expire': closest_allocation_expire,
'closest_allocation_duration': closest_allocation_duration,
'holds_changes': holds_changes,
})
if not self.env.context.get('from_dashboard', False) or lt_info[1]['max_leaves']:
allocation_data[employee].append(lt_info)
for employee in allocation_data:
for leave_type_data in allocation_data[employee]:
for key, value in leave_type_data[1].items():
if isinstance(value, float):
leave_type_data[1][key] = round(value, 2)
return allocation_data

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import api, models
_logger = logging.getLogger(__name__)
class MailMessageSubtype(models.Model):
_inherit = 'mail.message.subtype'
def _get_department_subtype(self):
return self.search([
('res_model', '=', 'hr.department'),
('parent_id', '=', self.id)])
def _update_department_subtype(self):
for subtype in self:
department_subtype = subtype._get_department_subtype()
if department_subtype:
department_subtype.write({
'name': subtype.name,
'default': subtype.default,
})
else:
department_subtype = self.create({
'name': subtype.name,
'res_model': 'hr.department',
'default': subtype.default or False,
'parent_id': subtype.id,
'relation_field': 'department_id',
})
return department_subtype
@api.model_create_multi
def create(self, vals_list):
result = super(MailMessageSubtype, self).create(vals_list)
result.filtered(
lambda st: st.res_model in ['hr.leave', 'hr.leave.allocation']
)._update_department_subtype()
return result
def write(self, vals):
result = super(MailMessageSubtype, self).write(vals)
self.filtered(
lambda subtype: subtype.res_model in ['hr.leave', 'hr.leave.allocation']
)._update_department_subtype()
return result

42
models/res_partner.py Normal file
View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT
class ResPartner(models.Model):
_inherit = 'res.partner'
def _compute_im_status(self):
super(ResPartner, self)._compute_im_status()
absent_now = self._get_on_leave_ids()
for partner in self:
if partner.id in absent_now:
if partner.im_status == 'online':
partner.im_status = 'leave_online'
elif partner.im_status == 'away':
partner.im_status = 'leave_away'
else:
partner.im_status = 'leave_offline'
@api.model
def _get_on_leave_ids(self):
return self.env['res.users']._get_on_leave_ids(partner=True)
def mail_partner_format(self, fields=None):
"""Override to add the current leave status."""
partners_format = super().mail_partner_format(fields=fields)
if not fields:
fields = {'out_of_office_date_end': True}
for partner in self:
if 'out_of_office_date_end' in fields:
# in the rare case of multi-user partner, return the earliest possible return date
dates = partner.mapped('user_ids.leave_date_to')
states = partner.mapped('user_ids.current_leave_state')
date = sorted(dates)[0] if dates and all(dates) else False
state = sorted(states)[0] if states and all(states) else False
partners_format.get(partner).update({
'out_of_office_date_end': date.strftime(DEFAULT_SERVER_DATE_FORMAT) if state == 'validate' and date else False,
})
return partners_format

82
models/res_users.py Normal file
View File

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, Command
class User(models.Model):
_inherit = "res.users"
leave_manager_id = fields.Many2one(related='employee_id.leave_manager_id')
show_leaves = fields.Boolean(related='employee_id.show_leaves')
allocation_count = fields.Float(related='employee_id.allocation_count')
leave_date_to = fields.Date(related='employee_id.leave_date_to')
current_leave_state = fields.Selection(related='employee_id.current_leave_state')
is_absent = fields.Boolean(related='employee_id.is_absent')
allocation_remaining_display = fields.Char(related='employee_id.allocation_remaining_display')
allocation_display = fields.Char(related='employee_id.allocation_display')
hr_icon_display = fields.Selection(related='employee_id.hr_icon_display')
@property
def SELF_READABLE_FIELDS(self):
return super().SELF_READABLE_FIELDS + [
'leave_manager_id',
'show_leaves',
'allocation_count',
'leave_date_to',
'current_leave_state',
'is_absent',
'allocation_remaining_display',
'allocation_display',
'hr_icon_display',
]
def _compute_im_status(self):
super(User, self)._compute_im_status()
on_leave_user_ids = self._get_on_leave_ids()
for user in self:
if user.id in on_leave_user_ids:
if user.im_status == 'online':
user.im_status = 'leave_online'
elif user.im_status == 'away':
user.im_status = 'leave_away'
else:
user.im_status = 'leave_offline'
@api.model
def _get_on_leave_ids(self, partner=False):
now = fields.Datetime.now()
field = 'partner_id' if partner else 'id'
self.flush_model(['active'])
self.env['hr.leave'].flush_model(['user_id', 'state', 'date_from', 'date_to'])
self.env.cr.execute('''SELECT res_users.%s FROM res_users
JOIN hr_leave ON hr_leave.user_id = res_users.id
AND state = 'validate'
AND hr_leave.active = 't'
AND res_users.active = 't'
AND date_from <= %%s AND date_to >= %%s''' % field, (now, now))
return [r[0] for r in self.env.cr.fetchall()]
def _clean_leave_responsible_users(self):
# self = old bunch of leave responsibles
# This method compares the current leave managers
# and remove the access rights to those who don't
# need them anymore
approver_group = 'hr_holidays.group_hr_holidays_responsible'
if not any(u.has_group(approver_group) for u in self):
return
res = self.env['hr.employee']._read_group(
[('leave_manager_id', 'in', self.ids)],
['leave_manager_id'])
responsibles_to_remove_ids = set(self.ids) - {leave_manager.id for [leave_manager] in res}
if responsibles_to_remove_ids:
self.browse(responsibles_to_remove_ids).write({
'groups_id': [Command.unlink(self.env.ref(approver_group).id)],
})
@api.model_create_multi
def create(self, vals_list):
users = super().create(vals_list)
users.sudo()._clean_leave_responsible_users()
return users

169
models/resource.py Normal file
View File

@ -0,0 +1,169 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError
from odoo.osv import expression
import pytz
from datetime import datetime
class CalendarLeaves(models.Model):
_inherit = "resource.calendar.leaves"
holiday_id = fields.Many2one("hr.leave", string='Time Off Request')
@api.constrains('date_from', 'date_to', 'calendar_id')
def _check_compare_dates(self):
all_existing_leaves = self.env['resource.calendar.leaves'].search([
('resource_id', '=', False),
('company_id', 'in', self.company_id.ids),
('date_from', '<=', max(self.mapped('date_to'))),
('date_to', '>=', min(self.mapped('date_from'))),
])
for record in self:
if not record.resource_id:
existing_leaves = all_existing_leaves.filtered(lambda leave:
record.id != leave.id
and record['company_id'] == leave['company_id']
and record['date_from'] <= leave['date_to']
and record['date_to'] >= leave['date_from'])
if record.calendar_id:
existing_leaves = existing_leaves.filtered(lambda l: not l.calendar_id or l.calendar_id == record.calendar_id)
if existing_leaves:
raise ValidationError(_('Two public holidays cannot overlap each other for the same working hours.'))
def _get_domain(self, time_domain_dict):
domain = []
for date in time_domain_dict:
domain = expression.OR([domain, [
('employee_company_id', '=', date['company_id']),
('date_to', '>', date['date_from']),
('date_from', '<', date['date_to'])]
])
return expression.AND([domain, [('state', '!=', 'refuse'), ('active', '=', True)]])
def _get_time_domain_dict(self):
return [{
'company_id' : record.company_id.id,
'date_from' : record.date_from,
'date_to' : record.date_to
} for record in self if not record.resource_id]
def _reevaluate_leaves(self, time_domain_dict):
if not time_domain_dict:
return
domain = self._get_domain(time_domain_dict)
leaves = self.env['hr.leave'].search(domain)
if not leaves:
return
previous_durations = leaves.mapped('number_of_days')
previous_states = leaves.mapped('state')
leaves.sudo().write({
'state': 'draft',
})
self.env.add_to_compute(self.env['hr.leave']._fields['number_of_days'], leaves)
sick_time_status = self.env.ref('hr_holidays.holiday_status_sl')
for previous_duration, leave, state in zip(previous_durations, leaves, previous_states):
duration_difference = previous_duration - leave.number_of_days
message = False
if duration_difference > 0 and leave.holiday_status_id.requires_allocation == 'yes':
message = _("Due to a change in global time offs, you have been granted %s day(s) back.", duration_difference)
if leave.number_of_days > previous_duration\
and leave.holiday_status_id not in sick_time_status:
message = _("Due to a change in global time offs, %s extra day(s) have been taken from your allocation. Please review this leave if you need it to be changed.", -1 * duration_difference)
try:
leave.write({'state': state})
leave._check_validity()
except ValidationError:
leave.action_refuse()
message = _("Due to a change in global time offs, this leave no longer has the required amount of available allocation and has been set to refused. Please review this leave.")
if message:
leave._notify_change(message)
def _convert_timezone(self, utc_naive_datetime, tz_from, tz_to):
"""
Convert a naive date to another timezone that initial timezone
used to generate the date.
:param utc_naive_datetime: utc date without tzinfo
:type utc_naive_datetime: datetime
:param tz_from: timezone used to obtained `utc_naive_datetime`
:param tz_to: timezone in which we want the date
:return: datetime converted into tz_to without tzinfo
:rtype: datetime
"""
naive_datetime_from = utc_naive_datetime.astimezone(tz_from).replace(tzinfo=None)
aware_datetime_to = tz_to.localize(naive_datetime_from)
utc_naive_datetime_to = aware_datetime_to.astimezone(pytz.utc).replace(tzinfo=None)
return utc_naive_datetime_to
def _ensure_datetime(self, datetime_representation, date_format=None):
"""
Be sure to get a datetime object if we have the necessary information.
:param datetime_reprentation: object which should represent a datetime
:rtype: datetime if a correct datetime_represtion, None otherwise
"""
if isinstance(datetime_representation, datetime):
return datetime_representation
elif isinstance(datetime_representation, str) and date_format:
return datetime.strptime(datetime_representation, date_format)
else:
return None
def _prepare_public_holidays_values(self, vals_list):
for vals in vals_list:
# Manage the case of create a Public Time Off in another timezone
# The datetime created has to be in UTC for the calendar's timezone
if not vals.get('calendar_id') or vals.get('resource_id') or \
not isinstance(vals.get('date_from'), (datetime, str)) or \
not isinstance(vals.get('date_to'), (datetime, str)):
continue
user_tz = pytz.timezone(self.env.user.tz) if self.env.user.tz else pytz.utc
calendar_tz = pytz.timezone(self.env['resource.calendar'].browse(vals['calendar_id']).tz)
if user_tz != calendar_tz:
datetime_from = self._ensure_datetime(vals['date_from'], '%Y-%m-%d %H:%M:%S')
datetime_to = self._ensure_datetime(vals['date_to'], '%Y-%m-%d %H:%M:%S')
if datetime_from and datetime_to:
vals['date_from'] = self._convert_timezone(datetime_from, user_tz, calendar_tz)
vals['date_to'] = self._convert_timezone(datetime_to, user_tz, calendar_tz)
return vals_list
@api.model_create_multi
def create(self, vals_list):
vals_list = self._prepare_public_holidays_values(vals_list)
res = super().create(vals_list)
time_domain_dict = res._get_time_domain_dict()
self._reevaluate_leaves(time_domain_dict)
return res
def write(self, vals):
time_domain_dict = self._get_time_domain_dict()
res = super().write(vals)
time_domain_dict.extend(self._get_time_domain_dict())
self._reevaluate_leaves(time_domain_dict)
return res
def unlink(self):
time_domain_dict = self._get_time_domain_dict()
res = super().unlink()
self._reevaluate_leaves(time_domain_dict)
return res
class ResourceCalendar(models.Model):
_inherit = "resource.calendar"
associated_leaves_count = fields.Integer("Time Off Count", compute='_compute_associated_leaves_count')
def _compute_associated_leaves_count(self):
leaves_read_group = self.env['resource.calendar.leaves']._read_group(
[('resource_id', '=', False), ('calendar_id', 'in', [False, *self.ids])],
['calendar_id'],
['__count'],
)
result = {calendar.id if calendar else 'global': count for calendar, count in leaves_read_group}
global_leave_count = result.get('global', 0)
for calendar in self:
calendar.associated_leaves_count = result.get(calendar.id, 0) + global_leave_count

4
populate/__init__.py Normal file
View File

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

67
populate/hr_leave.py Normal file
View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from odoo import models
from odoo.tools import populate
from dateutil.relativedelta import relativedelta
from itertools import groupby
class HolidaysType(models.Model):
_inherit = "hr.leave.type"
_populate_sizes = {"small": 10, "medium": 30, "large": 100}
_populate_dependencies = ['res.company']
def _populate_factories(self):
company_ids = self.env.registry.populated_models['res.company']
return [
('name', populate.constant('leave_type_{counter}')),
('company_id', populate.randomize(company_ids)),
('requires_allocation', populate.randomize(['yes', 'no'], [0.3, 0.7])),
('employee_requests', populate.randomize(['yes', 'no'], [0.2, 0.8])),
('request_unit', populate.randomize(['hour', 'day'], [0.2, 0.8])),
]
class HolidaysRequest(models.Model):
_inherit = "hr.leave"
_populate_sizes = {"small": 100, "medium": 800, "large": 10000}
_populate_dependencies = ['hr.employee', 'hr.leave.type']
def _populate_factories(self):
employee_ids = self.env.registry.populated_models['hr.employee']
hr_leave_type_ids = self.env.registry.populated_models['hr.leave.type']
hr_leave_type_records = self.env['hr.leave.type'].browse(hr_leave_type_ids)
allocationless_leave_type_ids = hr_leave_type_records.filtered(lambda lt: lt.requires_allocation == 'no').ids
employee_records = self.env['hr.employee'].browse(employee_ids)
employee_by_company = {k: list(v) for k, v in groupby(employee_records, key=lambda rec: rec['company_id'].id)}
company_by_type = {rec.id: rec.company_id.id for rec in self.env['hr.leave.type'].browse(hr_leave_type_ids)}
def compute_employee_id(random=None, values=None, **kwargs):
company_id = company_by_type[values['holiday_status_id']]
return random.choice(employee_by_company[company_id]).id
def compute_request_date_from(counter, **kwargs):
return datetime.datetime.today() + relativedelta(days=int(3 * int(counter)))
def compute_request_date_to(counter, random=None, **kwargs):
return datetime.datetime.today() + relativedelta(days=int(3 * int(counter)) + random.randint(0, 2))
return [
('holiday_status_id', populate.randomize(allocationless_leave_type_ids)),
('employee_id', populate.compute(compute_employee_id)),
('holiday_type', populate.constant('employee')),
('request_date_from', populate.compute(compute_request_date_from)),
('request_date_to', populate.compute(compute_request_date_to)),
('state', populate.randomize([
'draft',
'confirm',
])),
]

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.tools import populate
class HolidaysAllocation(models.Model):
_inherit = "hr.leave.allocation"
_populate_sizes = {"small": 100, "medium": 800, "large": 10000}
_populate_dependencies = ['hr.employee', 'hr.leave.type']
def _populate_factories(self):
employee_ids = self.env.registry.populated_models['hr.employee']
hr_leave_type_ids = self.env['hr.leave.type']\
.browse(self.env.registry.populated_models['hr.leave.type'])\
.filtered(lambda lt: lt.requires_allocation == 'yes')\
.ids
return [
('holiday_status_id', populate.randomize(hr_leave_type_ids)),
('employee_id', populate.randomize(employee_ids)),
]

7
report/__init__.py Normal file
View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import holidays_summary_report
from . import hr_leave_report
from . import hr_leave_report_calendar
from . import hr_leave_employee_type_report

Some files were not shown because too many files have changed in this diff Show More