Init
This commit is contained in:
parent
b458d7e298
commit
9c5b162283
18
__init__.py
Normal file
18
__init__.py
Normal 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
87
__manifest__.py
Normal 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
2
controllers/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*
|
||||
from . import main
|
48
controllers/main.py
Normal file
48
controllers/main.py
Normal 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
315
data/hr_holidays_data.xml
Normal 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
468
data/hr_holidays_demo.xml
Normal 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
25
data/ir_cron_data.xml
Normal 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>
|
24
data/mail_activity_type_data.xml
Normal file
24
data/mail_activity_type_data.xml
Normal 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>
|
33
data/mail_message_subtype_data.xml
Normal file
33
data/mail_message_subtype_data.xml
Normal 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>
|
18
data/report_paperformat.xml
Normal file
18
data/report_paperformat.xml
Normal 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
4351
i18n/af.po
Normal file
File diff suppressed because it is too large
Load Diff
4347
i18n/am.po
Normal file
4347
i18n/am.po
Normal file
File diff suppressed because it is too large
Load Diff
4932
i18n/ar.po
Normal file
4932
i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
4396
i18n/az.po
Normal file
4396
i18n/az.po
Normal file
File diff suppressed because it is too large
Load Diff
4811
i18n/bg.po
Normal file
4811
i18n/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
4352
i18n/bs.po
Normal file
4352
i18n/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
4948
i18n/ca.po
Normal file
4948
i18n/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
4904
i18n/cs.po
Normal file
4904
i18n/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
4910
i18n/da.po
Normal file
4910
i18n/da.po
Normal file
File diff suppressed because it is too large
Load Diff
5030
i18n/de.po
Normal file
5030
i18n/de.po
Normal file
File diff suppressed because it is too large
Load Diff
4357
i18n/el.po
Normal file
4357
i18n/el.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/en_GB.po
Normal file
4350
i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load Diff
5010
i18n/es.po
Normal file
5010
i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
5019
i18n/es_419.po
Normal file
5019
i18n/es_419.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/es_BO.po
Normal file
4350
i18n/es_BO.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/es_CL.po
Normal file
4350
i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/es_CO.po
Normal file
4350
i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/es_CR.po
Normal file
4350
i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/es_DO.po
Normal file
4350
i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/es_EC.po
Normal file
4350
i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/es_PE.po
Normal file
4350
i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/es_PY.po
Normal file
4350
i18n/es_PY.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/es_VE.po
Normal file
4350
i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load Diff
4921
i18n/et.po
Normal file
4921
i18n/et.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/eu.po
Normal file
4350
i18n/eu.po
Normal file
File diff suppressed because it is too large
Load Diff
4866
i18n/fa.po
Normal file
4866
i18n/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
4941
i18n/fi.po
Normal file
4941
i18n/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/fo.po
Normal file
4350
i18n/fo.po
Normal file
File diff suppressed because it is too large
Load Diff
5011
i18n/fr.po
Normal file
5011
i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
4349
i18n/fr_BE.po
Normal file
4349
i18n/fr_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/fr_CA.po
Normal file
4350
i18n/fr_CA.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/gl.po
Normal file
4350
i18n/gl.po
Normal file
File diff suppressed because it is too large
Load Diff
4356
i18n/gu.po
Normal file
4356
i18n/gu.po
Normal file
File diff suppressed because it is too large
Load Diff
4812
i18n/he.po
Normal file
4812
i18n/he.po
Normal file
File diff suppressed because it is too large
Load Diff
4349
i18n/hi.po
Normal file
4349
i18n/hi.po
Normal file
File diff suppressed because it is too large
Load Diff
4391
i18n/hr.po
Normal file
4391
i18n/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
4781
i18n/hr_holidays.pot
Normal file
4781
i18n/hr_holidays.pot
Normal file
File diff suppressed because it is too large
Load Diff
4835
i18n/hu.po
Normal file
4835
i18n/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
4975
i18n/id.po
Normal file
4975
i18n/id.po
Normal file
File diff suppressed because it is too large
Load Diff
4351
i18n/is.po
Normal file
4351
i18n/is.po
Normal file
File diff suppressed because it is too large
Load Diff
5016
i18n/it.po
Normal file
5016
i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
4858
i18n/ja.po
Normal file
4858
i18n/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/ka.po
Normal file
4350
i18n/ka.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/kab.po
Normal file
4350
i18n/kab.po
Normal file
File diff suppressed because it is too large
Load Diff
4352
i18n/km.po
Normal file
4352
i18n/km.po
Normal file
File diff suppressed because it is too large
Load Diff
4870
i18n/ko.po
Normal file
4870
i18n/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
4351
i18n/lb.po
Normal file
4351
i18n/lb.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/lo.po
Normal file
4350
i18n/lo.po
Normal file
File diff suppressed because it is too large
Load Diff
4823
i18n/lt.po
Normal file
4823
i18n/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
4816
i18n/lv.po
Normal file
4816
i18n/lv.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/mk.po
Normal file
4350
i18n/mk.po
Normal file
File diff suppressed because it is too large
Load Diff
4401
i18n/mn.po
Normal file
4401
i18n/mn.po
Normal file
File diff suppressed because it is too large
Load Diff
4362
i18n/nb.po
Normal file
4362
i18n/nb.po
Normal file
File diff suppressed because it is too large
Load Diff
4347
i18n/ne.po
Normal file
4347
i18n/ne.po
Normal file
File diff suppressed because it is too large
Load Diff
5000
i18n/nl.po
Normal file
5000
i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
4933
i18n/pl.po
Normal file
4933
i18n/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
4803
i18n/pt.po
Normal file
4803
i18n/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
4990
i18n/pt_BR.po
Normal file
4990
i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load Diff
4404
i18n/ro.po
Normal file
4404
i18n/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
5005
i18n/ru.po
Normal file
5005
i18n/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
4875
i18n/sk.po
Normal file
4875
i18n/sk.po
Normal file
File diff suppressed because it is too large
Load Diff
4810
i18n/sl.po
Normal file
4810
i18n/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
4350
i18n/sq.po
Normal file
4350
i18n/sq.po
Normal file
File diff suppressed because it is too large
Load Diff
4913
i18n/sr.po
Normal file
4913
i18n/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
4354
i18n/sr@latin.po
Normal file
4354
i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load Diff
4858
i18n/sv.po
Normal file
4858
i18n/sv.po
Normal file
File diff suppressed because it is too large
Load Diff
4928
i18n/th.po
Normal file
4928
i18n/th.po
Normal file
File diff suppressed because it is too large
Load Diff
4907
i18n/tr.po
Normal file
4907
i18n/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
4984
i18n/uk.po
Normal file
4984
i18n/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
4965
i18n/vi.po
Normal file
4965
i18n/vi.po
Normal file
File diff suppressed because it is too large
Load Diff
4859
i18n/zh_CN.po
Normal file
4859
i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
4859
i18n/zh_TW.po
Normal file
4859
i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load Diff
16
models/__init__.py
Normal file
16
models/__init__.py
Normal 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
76
models/hr_department.py
Normal 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
582
models/hr_employee.py
Normal 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)
|
13
models/hr_employee_base.py
Normal file
13
models/hr_employee_base.py
Normal 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
1771
models/hr_leave.py
Normal file
File diff suppressed because it is too large
Load Diff
143
models/hr_leave_accrual_plan.py
Normal file
143
models/hr_leave_accrual_plan.py
Normal 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)
|
305
models/hr_leave_accrual_plan_level.py
Normal file
305
models/hr_leave_accrual_plan_level.py
Normal 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
|
917
models/hr_leave_allocation.py
Normal file
917
models/hr_leave_allocation.py
Normal 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)
|
24
models/hr_leave_mandatory_day.py
Normal file
24
models/hr_leave_mandatory_day.py
Normal 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
502
models/hr_leave_type.py
Normal 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
|
50
models/mail_message_subtype.py
Normal file
50
models/mail_message_subtype.py
Normal 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
42
models/res_partner.py
Normal 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
82
models/res_users.py
Normal 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
169
models/resource.py
Normal 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
4
populate/__init__.py
Normal 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
67
populate/hr_leave.py
Normal 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',
|
||||
])),
|
||||
]
|
24
populate/hr_leave_allocation.py
Normal file
24
populate/hr_leave_allocation.py
Normal 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
7
report/__init__.py
Normal 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
Loading…
x
Reference in New Issue
Block a user