[MOV] hr_work_entry_contract,hr_work_entry_holidays: move from enterprise to community
Before this commit, hr_work_entry_contract and hr_work_entry_holidays were defined in enterprise but those modules could be integrated with some community applications, like hr_attendance. taskid: 2222790 -- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr closes odoo/odoo#66247 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
@@ -303,12 +303,4 @@
|
||||
parent="hr.menu_human_resources_configuration"
|
||||
sequence="3"/>
|
||||
|
||||
<menuitem
|
||||
id="hr_menu_contract"
|
||||
name="All Contracts"
|
||||
action="action_hr_contract"
|
||||
parent="hr.menu_hr_employee_payroll"
|
||||
sequence="6"
|
||||
groups="hr_contract.group_hr_contract_manager"/>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
@@ -0,0 +1,29 @@
|
||||
#-*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Work Entries - Contract',
|
||||
'category': 'Human Resources/Employees',
|
||||
'sequence': 39,
|
||||
'summary': 'Manage work entries',
|
||||
'description': "",
|
||||
'installable': True,
|
||||
'depends': [
|
||||
'hr_work_entry',
|
||||
'hr_contract',
|
||||
],
|
||||
'data': [
|
||||
'security/hr_work_entry_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'wizard/create_company_global_time_off_views.xml',
|
||||
'data/hr_work_entry_data.xml',
|
||||
'views/hr_work_entry_template.xml',
|
||||
'views/hr_work_entry_views.xml',
|
||||
'views/resource_views.xml',
|
||||
'wizard/hr_work_entry_regeneration_wizard_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/hr_work_entry_demo.xml',
|
||||
],
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<!-- Work Entry Type -->
|
||||
<record id="work_entry_type_leave" model="hr.work.entry.type">
|
||||
<field name="name">Generic Time Off</field>
|
||||
<field name="code">LEAVE100</field>
|
||||
<field name="color">3</field>
|
||||
<field name="is_leave">True</field>
|
||||
</record>
|
||||
|
||||
<record id="work_entry_type_compensatory" model="hr.work.entry.type">
|
||||
<field name="name">Compensatory Time Off</field>
|
||||
<field name="code">LEAVE105</field>
|
||||
<field name="color">3</field>
|
||||
<field name="is_leave">True</field>
|
||||
</record>
|
||||
|
||||
<record id="work_entry_type_home_working" model="hr.work.entry.type">
|
||||
<field name="name">Home Working</field>
|
||||
<field name="code">WORK110</field>
|
||||
<field name="color">2</field>
|
||||
<field name="is_leave">True</field>
|
||||
</record>
|
||||
|
||||
<record id="work_entry_type_unpaid_leave" model="hr.work.entry.type">
|
||||
<field name="name">Unpaid</field>
|
||||
<field name="color">5</field>
|
||||
<field name="code">LEAVE90</field>
|
||||
<field name="is_leave">True</field>
|
||||
</record>
|
||||
|
||||
<record id="work_entry_type_sick_leave" model="hr.work.entry.type">
|
||||
<field name="name">Sick Time Off</field>
|
||||
<field name="code">LEAVE110</field>
|
||||
<field name="is_leave">True</field>
|
||||
<field name="color">5</field>
|
||||
</record>
|
||||
|
||||
<record id="work_entry_type_legal_leave" model="hr.work.entry.type">
|
||||
<field name="name" eval="'Legal Leaves '+time.strftime('%Y')"/>
|
||||
<field name="code">LEAVE120</field>
|
||||
<field name="is_leave">True</field>
|
||||
<field name="color">5</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_work_entry.work_entry_type_attendance" model="hr.work.entry.type">
|
||||
<field name="is_leave">False</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Work entries -->
|
||||
<record id="work_entry_type_extra_hours" model="hr.work.entry.type">
|
||||
<field name="name">Extra Hours</field>
|
||||
<field name="code">WORK300</field>
|
||||
<field name="color">8</field>
|
||||
</record>
|
||||
|
||||
<record id="work_entry_type_long_leave" model="hr.work.entry.type">
|
||||
<field name="name">Long Term Time Off</field>
|
||||
<field name="code">LEAVE200</field>
|
||||
<field name="is_leave">True</field>
|
||||
<field name="color">4</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,350 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * hr_work_entry_contract
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~14.1+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-01-12 07:57+0000\n"
|
||||
"PO-Revision-Date: 2021-01-12 07:57+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: code:addons/hr_work_entry_contract/models/hr_work_entry.py:0
|
||||
#, python-format
|
||||
msgid "%s does not have a contract from %s to %s."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: code:addons/hr_work_entry_contract/models/hr_work_entry.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"%s has multiple contracts from %s to %s. A work entry cannot overlap "
|
||||
"multiple contracts."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.hr_work_entry_regeneration_wizard
|
||||
msgid ""
|
||||
"<i class=\"fa fa-exclamation-triangle mr-1\" title=\"Warning\"/>You are not "
|
||||
"allowed to regenerate validated work entries"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.hr_work_entry_regeneration_wizard
|
||||
msgid "<i class=\"fa fa-info-circle mr-1\" title=\"Hint\"/>"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.create_company_global_time_off_view_form
|
||||
msgid "<span> to </span>"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.create_company_global_time_off_view_form
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_create_company_global_time_off__company_id
|
||||
msgid "Company"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:hr.work.entry.type,name:hr_work_entry_contract.work_entry_type_compensatory
|
||||
msgid "Compensatory Time Off"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.ui.menu,name:hr_work_entry_contract.menu_hr_payroll_configuration
|
||||
msgid "Configuration"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.create_company_global_time_off_view_form
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.ui.menu,name:hr_work_entry_contract.menu_work_entry_conflicts
|
||||
msgid "Conflicts"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry__contract_id
|
||||
msgid "Contract"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.actions.act_window,name:hr_work_entry_contract.action_create_company_global_time_off
|
||||
#: model:ir.model,name:hr_work_entry_contract.model_create_company_global_time_off
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.create_company_global_time_off_view_form
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.resource_calendar_view_form
|
||||
msgid "Create Company Public Time Off"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_create_company_global_time_off__create_uid
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_create_company_global_time_off__create_date
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_create_company_global_time_off__display_name
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_contract__display_name
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_employee__display_name
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry__display_name
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__display_name
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_type__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__earliest_available_date_message
|
||||
msgid "Earliest Available Date Message"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__earliest_available_date
|
||||
msgid "Earliest date"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model,name:hr_work_entry_contract.model_hr_employee
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry__employee_id
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__employee_id
|
||||
msgid "Employee"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model,name:hr_work_entry_contract.model_hr_contract
|
||||
msgid "Employee Contract"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_create_company_global_time_off__date_to
|
||||
msgid "End Date"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#. openerp-web
|
||||
#: code:addons/hr_work_entry_contract/static/src/js/work_entries_calendar.js:0
|
||||
#, python-format
|
||||
msgid "Everybody's work entries"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__date_from
|
||||
msgid "From"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_contract__date_generated_from
|
||||
msgid "Generated From"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_contract__date_generated_to
|
||||
msgid "Generated To"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:hr.work.entry.type,name:hr_work_entry_contract.work_entry_type_leave
|
||||
msgid "Generic Time Off"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model,name:hr_work_entry_contract.model_hr_work_entry
|
||||
msgid "HR Work Entry"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model,name:hr_work_entry_contract.model_hr_work_entry_type
|
||||
msgid "HR Work Entry Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:hr.work.entry.type,name:hr_work_entry_contract.work_entry_type_home_working
|
||||
msgid "Home Working"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_create_company_global_time_off__id
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_contract__id
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_employee__id
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry__id
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__id
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_type__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: code:addons/hr_work_entry_contract/wizard/hr_work_entry_regeneration_wizard.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"In order to regenerate the work entries, you need to provide the wizard with"
|
||||
" an employee_id, a date_from and a date_to. In addition to that, the time "
|
||||
"interval defined by date_from and date_to must not contain any validated "
|
||||
"work entries."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_create_company_global_time_off____last_update
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_contract____last_update
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_employee____last_update
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry____last_update
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard____last_update
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_type____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_create_company_global_time_off__write_uid
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_create_company_global_time_off__write_date
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__latest_available_date_message
|
||||
msgid "Latest Available Date Message"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__latest_available_date
|
||||
msgid "Latest date"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:hr.work.entry.type,name:hr_work_entry_contract.work_entry_type_legal_leave
|
||||
msgid "Legal Leaves 2021"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.ui.menu,name:hr_work_entry_contract.menu_hr_payroll_root
|
||||
msgid "Payroll"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.create_company_global_time_off_view_form
|
||||
msgid "Period"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_create_company_global_time_off__name
|
||||
msgid "Reason"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model,name:hr_work_entry_contract.model_hr_work_entry_regeneration_wizard
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.hr_work_entry_regeneration_wizard
|
||||
msgid "Regenerate Employee Work Entries"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#. openerp-web
|
||||
#: code:addons/hr_work_entry_contract/static/src/js/work_entries_controller_mixin.js:0
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.hr_work_entry_regeneration_wizard
|
||||
#, python-format
|
||||
msgid "Regenerate Work Entries"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__search_criteria_completed
|
||||
msgid "Search Criteria Completed"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:hr.work.entry.type,name:hr_work_entry_contract.work_entry_type_sick_leave
|
||||
msgid "Sick Time Off"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_create_company_global_time_off__date_from
|
||||
msgid "Start Date"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: code:addons/hr_work_entry_contract/wizard/hr_work_entry_regeneration_wizard.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The from date must be >= '%(earliest_available_date)s' and the to date must "
|
||||
"be <= '%(latest_available_date)s', which correspond to the generated work "
|
||||
"entries time interval."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.hr_work_entry_contract_view_form_inherit
|
||||
msgid "This work entry cannot be validated. The work entry type is undefined."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_type__is_leave
|
||||
msgid "Time Off"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__date_to
|
||||
msgid "To"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:hr.work.entry.type,name:hr_work_entry_contract.work_entry_type_unpaid_leave
|
||||
msgid "Unpaid"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__valid
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.ui.menu,name:hr_work_entry_contract.menu_hr_payroll_work_entries_root
|
||||
#: model:ir.ui.menu,name:hr_work_entry_contract.menu_hr_work_entry_confirguration
|
||||
#: model:ir.ui.menu,name:hr_work_entry_contract.menu_work_entry
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.hr_work_entry_gantt
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_contract.hr_work_entry_regeneration_wizard
|
||||
msgid "Work Entries"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_hr_work_entry_regeneration_wizard__validated_work_entry_ids
|
||||
msgid "Work Entries Within Interval"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.actions.act_window,name:hr_work_entry_contract.hr_work_entry_regeneration_wizard_action
|
||||
msgid "Work Entry Regeneration"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_contract.field_create_company_global_time_off__work_entry_type_id
|
||||
msgid "Work Entry Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.ui.menu,name:hr_work_entry_contract.menu_hr_work_entry_type_view
|
||||
msgid "Work Entry Types"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_contract
|
||||
#: model:ir.ui.menu,name:hr_work_entry_contract.menu_resource_calendar_view
|
||||
msgid "Working Times"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import hr_contract
|
||||
from . import hr_employee
|
||||
from . import hr_work_entry
|
||||
from . import hr_work_intervals
|
||||
@@ -0,0 +1,254 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from odoo import fields, models
|
||||
from odoo.addons.resource.models.resource import datetime_to_string, string_to_datetime, Intervals
|
||||
from odoo.osv import expression
|
||||
|
||||
import pytz
|
||||
|
||||
class HrContract(models.Model):
|
||||
_inherit = 'hr.contract'
|
||||
_description = 'Employee Contract'
|
||||
|
||||
date_generated_from = fields.Datetime(string='Generated From', readonly=True, required=True,
|
||||
default=lambda self: datetime.now().replace(hour=0, minute=0, second=0), copy=False)
|
||||
date_generated_to = fields.Datetime(string='Generated To', readonly=True, required=True,
|
||||
default=lambda self: datetime.now().replace(hour=0, minute=0, second=0), copy=False)
|
||||
|
||||
def _get_default_work_entry_type(self):
|
||||
return self.env.ref('hr_work_entry.work_entry_type_attendance', raise_if_not_found=False)
|
||||
|
||||
def _get_leave_work_entry_type_dates(self, leave, date_from, date_to):
|
||||
return self._get_leave_work_entry_type(leave)
|
||||
|
||||
def _get_leave_work_entry_type(self, leave):
|
||||
return leave.work_entry_type_id
|
||||
|
||||
# Is used to add more values, for example leave_id (in hr_work_entry_holidays)
|
||||
def _get_more_vals_leave_interval(self, interval, leaves):
|
||||
return []
|
||||
|
||||
def _get_interval_leave_work_entry_type(self, interval, leaves):
|
||||
# returns the work entry time related to the leave that
|
||||
# includes the whole interval.
|
||||
# Overriden in hr_work_entry_contract_holiday to select the
|
||||
# global time off first (eg: Public Holiday > Home Working)
|
||||
for leave in leaves:
|
||||
if interval[0] >= leave[0] and interval[1] <= leave[1] and leave[2]:
|
||||
interval_start = interval[0].astimezone(pytz.utc).replace(tzinfo=None)
|
||||
interval_stop = interval[1].astimezone(pytz.utc).replace(tzinfo=None)
|
||||
return self._get_leave_work_entry_type_dates(leave[2], interval_start, interval_stop)
|
||||
return self.env.ref('hr_work_entry_contract.work_entry_type_leave')
|
||||
|
||||
def _get_contract_work_entries_values(self, date_start, date_stop):
|
||||
self.ensure_one()
|
||||
contract_vals = []
|
||||
employee = self.employee_id
|
||||
calendar = self.resource_calendar_id
|
||||
resource = employee.resource_id
|
||||
tz = pytz.timezone(calendar.tz)
|
||||
start_dt = pytz.utc.localize(date_start) if not date_start.tzinfo else date_start
|
||||
end_dt = pytz.utc.localize(date_stop) if not date_stop.tzinfo else date_stop
|
||||
|
||||
attendances = calendar._attendance_intervals_batch(
|
||||
start_dt, end_dt, resources=resource, tz=tz
|
||||
)[resource.id]
|
||||
|
||||
# Other calendars: In case the employee has declared time off in another calendar
|
||||
# Example: Take a time off, then a credit time.
|
||||
# YTI TODO: This mimics the behavior of _leave_intervals_batch, while waiting to be cleaned
|
||||
# in master.
|
||||
resources_list = [self.env['resource.resource'], resource]
|
||||
resource_ids = [False, resource.id]
|
||||
leave_domain = [
|
||||
('time_type', '=', 'leave'),
|
||||
# ('calendar_id', '=', self.id), --> Get all the time offs
|
||||
('resource_id', 'in', resource_ids),
|
||||
('date_from', '<=', datetime_to_string(end_dt)),
|
||||
('date_to', '>=', datetime_to_string(start_dt)),
|
||||
('company_id', '=', self.env.company.id),
|
||||
]
|
||||
result = defaultdict(lambda: [])
|
||||
tz_dates = {}
|
||||
for leave in self.env['resource.calendar.leaves'].search(leave_domain):
|
||||
for resource in resources_list:
|
||||
if leave.resource_id.id not in [False, resource.id]:
|
||||
continue
|
||||
tz = tz if tz else pytz.timezone((resource or self).tz)
|
||||
if (tz, start_dt) in tz_dates:
|
||||
start = tz_dates[(tz, start_dt)]
|
||||
else:
|
||||
start = start_dt.astimezone(tz)
|
||||
tz_dates[(tz, start_dt)] = start
|
||||
if (tz, end_dt) in tz_dates:
|
||||
end = tz_dates[(tz, end_dt)]
|
||||
else:
|
||||
end = end_dt.astimezone(tz)
|
||||
tz_dates[(tz, end_dt)] = end
|
||||
dt0 = string_to_datetime(leave.date_from).astimezone(tz)
|
||||
dt1 = string_to_datetime(leave.date_to).astimezone(tz)
|
||||
result[resource.id].append((max(start, dt0), min(end, dt1), leave))
|
||||
mapped_leaves = {r.id: Intervals(result[r.id]) for r in resources_list}
|
||||
leaves = mapped_leaves[resource.id]
|
||||
|
||||
real_attendances = attendances - leaves
|
||||
real_leaves = attendances - real_attendances
|
||||
|
||||
# A leave period can be linked to several resource.calendar.leave
|
||||
split_leaves = []
|
||||
for leave_interval in leaves:
|
||||
if leave_interval[2] and len(leave_interval[2]) > 1:
|
||||
split_leaves += [(leave_interval[0], leave_interval[1], l) for l in leave_interval[2]]
|
||||
else:
|
||||
split_leaves += [(leave_interval[0], leave_interval[1], leave_interval[2])]
|
||||
leaves = split_leaves
|
||||
|
||||
# Attendances
|
||||
default_work_entry_type = self._get_default_work_entry_type()
|
||||
for interval in real_attendances:
|
||||
work_entry_type_id = interval[2].mapped('work_entry_type_id')[:1] or default_work_entry_type
|
||||
# All benefits generated here are using datetimes converted from the employee's timezone
|
||||
contract_vals += [{
|
||||
'name': "%s: %s" % (work_entry_type_id.name, employee.name),
|
||||
'date_start': interval[0].astimezone(pytz.utc).replace(tzinfo=None),
|
||||
'date_stop': interval[1].astimezone(pytz.utc).replace(tzinfo=None),
|
||||
'work_entry_type_id': work_entry_type_id.id,
|
||||
'employee_id': employee.id,
|
||||
'contract_id': self.id,
|
||||
'company_id': self.company_id.id,
|
||||
'state': 'draft',
|
||||
}]
|
||||
|
||||
for interval in real_leaves:
|
||||
# Could happen when a leave is configured on the interface on a day for which the
|
||||
# employee is not supposed to work, i.e. no attendance_ids on the calendar.
|
||||
# In that case, do try to generate an empty work entry, as this would raise a
|
||||
# sql constraint error
|
||||
if interval[0] == interval[1]: # if start == stop
|
||||
continue
|
||||
leave_entry_type = self._get_interval_leave_work_entry_type(interval, leaves)
|
||||
interval_start = interval[0].astimezone(pytz.utc).replace(tzinfo=None)
|
||||
interval_stop = interval[1].astimezone(pytz.utc).replace(tzinfo=None)
|
||||
contract_vals += [dict([
|
||||
('name', "%s%s" % (leave_entry_type.name + ": " if leave_entry_type else "", employee.name)),
|
||||
('date_start', interval_start),
|
||||
('date_stop', interval_stop),
|
||||
('work_entry_type_id', leave_entry_type.id),
|
||||
('employee_id', employee.id),
|
||||
('company_id', self.company_id.id),
|
||||
('state', 'draft'),
|
||||
('contract_id', self.id),
|
||||
] + self._get_more_vals_leave_interval(interval, leaves))]
|
||||
return contract_vals
|
||||
|
||||
def _get_work_entries_values(self, date_start, date_stop):
|
||||
"""
|
||||
Generate a work_entries list between date_start and date_stop for one contract.
|
||||
:return: list of dictionnary.
|
||||
"""
|
||||
vals_list = []
|
||||
for contract in self:
|
||||
contract_vals = contract._get_contract_work_entries_values(date_start, date_stop)
|
||||
|
||||
# If we generate work_entries which exceeds date_start or date_stop, we change boundaries on contract
|
||||
if contract_vals:
|
||||
date_stop_max = max([x['date_stop'] for x in contract_vals])
|
||||
if date_stop_max > contract.date_generated_to:
|
||||
contract.date_generated_to = date_stop_max
|
||||
|
||||
date_start_min = min([x['date_start'] for x in contract_vals])
|
||||
if date_start_min < contract.date_generated_from:
|
||||
contract.date_generated_from = date_start_min
|
||||
|
||||
vals_list += contract_vals
|
||||
|
||||
return vals_list
|
||||
|
||||
def _generate_work_entries(self, date_start, date_stop, force=False):
|
||||
vals_list = []
|
||||
|
||||
date_start = fields.Datetime.to_datetime(date_start)
|
||||
date_stop = datetime.combine(fields.Datetime.to_datetime(date_stop), datetime.max.time())
|
||||
|
||||
for contract in self:
|
||||
contract_start = fields.Datetime.to_datetime(contract.date_start)
|
||||
contract_stop = datetime.combine(fields.Datetime.to_datetime(contract.date_end or datetime.max.date()),
|
||||
datetime.max.time())
|
||||
date_start_work_entries = max(date_start, contract_start)
|
||||
date_stop_work_entries = min(date_stop, contract_stop)
|
||||
if force:
|
||||
vals_list.extend(contract._get_work_entries_values(date_start_work_entries, date_stop_work_entries))
|
||||
continue
|
||||
|
||||
# In case the date_generated_from == date_generated_to, move it to the date_start to
|
||||
# avoid trying to generate several months/years of history for old contracts for which
|
||||
# we've never generated the work entries.
|
||||
if contract.date_generated_from == contract.date_generated_to:
|
||||
contract.write({
|
||||
'date_generated_from': date_start,
|
||||
'date_generated_to': date_start,
|
||||
})
|
||||
# For each contract, we found each interval we must generate
|
||||
last_generated_from = min(contract.date_generated_from, contract_stop)
|
||||
if last_generated_from > date_start_work_entries:
|
||||
contract.date_generated_from = date_start_work_entries
|
||||
vals_list.extend(contract._get_work_entries_values(date_start_work_entries, last_generated_from))
|
||||
|
||||
last_generated_to = max(contract.date_generated_to, contract_start)
|
||||
if last_generated_to < date_stop_work_entries:
|
||||
contract.date_generated_to = date_stop_work_entries
|
||||
vals_list.extend(contract._get_work_entries_values(last_generated_to, date_stop_work_entries))
|
||||
|
||||
if not vals_list:
|
||||
return self.env['hr.work.entry']
|
||||
|
||||
return self.env['hr.work.entry'].create(vals_list)
|
||||
|
||||
def _remove_work_entries(self):
|
||||
''' Remove all work_entries that are outside contract period (function used after writing new start or/and end date) '''
|
||||
all_we_to_unlink = self.env['hr.work.entry']
|
||||
for contract in self:
|
||||
date_start = fields.Datetime.to_datetime(contract.date_start)
|
||||
if contract.date_generated_from < date_start:
|
||||
we_to_remove = self.env['hr.work.entry'].search([('date_stop', '<=', date_start), ('contract_id', '=', contract.id)])
|
||||
if we_to_remove:
|
||||
contract.date_generated_from = date_start
|
||||
all_we_to_unlink |= we_to_remove
|
||||
if not contract.date_end:
|
||||
continue
|
||||
date_end = datetime.combine(contract.date_end, datetime.max.time())
|
||||
if contract.date_generated_to > date_end:
|
||||
we_to_remove = self.env['hr.work.entry'].search([('date_start', '>=', date_end), ('contract_id', '=', contract.id)])
|
||||
if we_to_remove:
|
||||
contract.date_generated_to = date_end
|
||||
all_we_to_unlink |= we_to_remove
|
||||
all_we_to_unlink.unlink()
|
||||
|
||||
def _cancel_work_entries(self):
|
||||
if not self:
|
||||
return
|
||||
domain = [('state', '!=', 'validated')]
|
||||
for contract in self:
|
||||
date_start = fields.Datetime.to_datetime(contract.date_start)
|
||||
contract_domain = [
|
||||
('contract_id', '=', contract.id),
|
||||
('date_start', '>=', date_start),
|
||||
]
|
||||
if contract.date_end:
|
||||
date_end = datetime.combine(contract.date_end, datetime.max.time())
|
||||
contract_domain += [('date_stop', '<=', date_end)]
|
||||
domain = expression.AND([domain, contract_domain])
|
||||
work_entries = self.env['hr.work.entry'].search(domain)
|
||||
if work_entries:
|
||||
work_entries.unlink()
|
||||
|
||||
def write(self, vals):
|
||||
result = super(HrContract, self).write(vals)
|
||||
if vals.get('date_end') or vals.get('date_start'):
|
||||
self.sudo()._remove_work_entries()
|
||||
if vals.get('state') in ['draft', 'cancel']:
|
||||
self._cancel_work_entries()
|
||||
return result
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
_description = 'Employee'
|
||||
|
||||
def generate_work_entries(self, date_start, date_stop, force=False):
|
||||
date_start = fields.Date.to_date(date_start)
|
||||
date_stop = fields.Date.to_date(date_stop)
|
||||
|
||||
if self:
|
||||
current_contracts = self._get_contracts(date_start, date_stop, states=['open', 'close'])
|
||||
else:
|
||||
current_contracts = self._get_all_contracts(date_start, date_stop, states=['open', 'close'])
|
||||
|
||||
return bool(current_contracts._generate_work_entries(date_start, date_stop, force))
|
||||
@@ -0,0 +1,155 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import pytz
|
||||
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.hr_work_entry_contract.models.hr_work_intervals import WorkIntervals
|
||||
|
||||
|
||||
class HrWorkEntry(models.Model):
|
||||
_inherit = 'hr.work.entry'
|
||||
|
||||
contract_id = fields.Many2one('hr.contract', string="Contract", required=True)
|
||||
employee_id = fields.Many2one(domain=[('contract_ids.state', 'in', ('open', 'pending'))])
|
||||
|
||||
def _init_column(self, column_name):
|
||||
if column_name != 'contract_id':
|
||||
super()._init_column(column_name)
|
||||
else:
|
||||
self.env.cr.execute("""
|
||||
UPDATE hr_work_entry AS _hwe
|
||||
SET contract_id = result.contract_id
|
||||
FROM (
|
||||
SELECT
|
||||
hc.id AS contract_id,
|
||||
array_agg(hwe.id) AS entry_ids
|
||||
FROM
|
||||
hr_work_entry AS hwe
|
||||
LEFT JOIN
|
||||
hr_contract AS hc
|
||||
ON
|
||||
hwe.employee_id=hc.employee_id AND
|
||||
hc.state in ('open', 'close') AND
|
||||
hwe.date_start >= hc.date_start AND
|
||||
hwe.date_stop < COALESCE(hc.date_end + integer '1', '9999-12-31 23:59:59')
|
||||
WHERE
|
||||
hwe.contract_id IS NULL
|
||||
GROUP BY
|
||||
hwe.employee_id, hc.id
|
||||
) AS result
|
||||
WHERE _hwe.id = ANY(result.entry_ids)
|
||||
""")
|
||||
|
||||
def _get_duration_is_valid(self):
|
||||
return self.work_entry_type_id and self.work_entry_type_id.is_leave
|
||||
|
||||
@api.onchange('employee_id', 'date_start', 'date_stop')
|
||||
def _onchange_contract_id(self):
|
||||
vals = {
|
||||
'employee_id': self.employee_id.id,
|
||||
'date_start': self.date_start,
|
||||
'date_stop': self.date_stop,
|
||||
}
|
||||
try:
|
||||
res = self._set_current_contract(vals)
|
||||
except ValidationError:
|
||||
return
|
||||
if res.get('contract_id'):
|
||||
self.contract_id = res.get('contract_id')
|
||||
|
||||
@api.depends('date_start', 'duration')
|
||||
def _compute_date_stop(self):
|
||||
for work_entry in self:
|
||||
if work_entry._get_duration_is_valid():
|
||||
calendar = work_entry.contract_id.resource_calendar_id
|
||||
if not calendar:
|
||||
continue
|
||||
work_entry.date_stop = calendar.plan_hours(work_entry.duration, work_entry.date_start, compute_leaves=True)
|
||||
continue
|
||||
super(HrWorkEntry, work_entry)._compute_date_stop()
|
||||
|
||||
def _get_duration(self, date_start, date_stop):
|
||||
if not date_start or not date_stop:
|
||||
return 0
|
||||
if self._get_duration_is_valid():
|
||||
calendar = self.contract_id.resource_calendar_id
|
||||
if not calendar:
|
||||
return 0
|
||||
employee = self.contract_id.employee_id
|
||||
contract_data = employee._get_work_days_data_batch(
|
||||
date_start, date_stop, compute_leaves=False, calendar=calendar
|
||||
)[employee.id]
|
||||
return contract_data.get('hours', 0)
|
||||
return super()._get_duration(date_start, date_stop)
|
||||
|
||||
@api.model
|
||||
def _set_current_contract(self, vals):
|
||||
if not vals.get('contract_id') and vals.get('date_start') and vals.get('date_stop') and vals.get('employee_id'):
|
||||
contract_start = fields.Datetime.to_datetime(vals.get('date_start')).date()
|
||||
contract_end = fields.Datetime.to_datetime(vals.get('date_stop')).date()
|
||||
employee = self.env['hr.employee'].browse(vals.get('employee_id'))
|
||||
contracts = employee._get_contracts(contract_start, contract_end, states=['open', 'pending', 'close'])
|
||||
if not contracts:
|
||||
raise ValidationError(_("%s does not have a contract from %s to %s.") % (employee.name, contract_start, contract_end))
|
||||
elif len(contracts) > 1:
|
||||
raise ValidationError(_("%s has multiple contracts from %s to %s. A work entry cannot overlap multiple contracts.")
|
||||
% (employee.name, contract_start, contract_end))
|
||||
return dict(vals, contract_id=contracts[0].id)
|
||||
return vals
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
vals_list = [self._set_current_contract(vals) for vals in vals_list]
|
||||
work_entries = super().create(vals_list)
|
||||
return work_entries
|
||||
|
||||
def _check_if_error(self):
|
||||
res = super()._check_if_error()
|
||||
outside_calendar = self._mark_leaves_outside_schedule()
|
||||
return res or outside_calendar
|
||||
|
||||
def _get_leaves_entries_outside_schedule(self):
|
||||
return self.filtered(lambda w: w.work_entry_type_id.is_leave and w.state not in ('validated', 'cancelled'))
|
||||
|
||||
def _mark_leaves_outside_schedule(self):
|
||||
"""
|
||||
Check leave work entries in `self` which are completely outside
|
||||
the contract's theoretical calendar schedule. Mark them as conflicting.
|
||||
:return: leave work entries completely outside the contract's calendar
|
||||
"""
|
||||
work_entries = self._get_leaves_entries_outside_schedule()
|
||||
entries_by_calendar = defaultdict(lambda: self.env['hr.work.entry'])
|
||||
for work_entry in work_entries:
|
||||
calendar = work_entry.contract_id.resource_calendar_id
|
||||
entries_by_calendar[calendar] |= work_entry
|
||||
|
||||
outside_entries = self.env['hr.work.entry']
|
||||
for calendar, entries in entries_by_calendar.items():
|
||||
datetime_start = min(entries.mapped('date_start'))
|
||||
datetime_stop = max(entries.mapped('date_stop'))
|
||||
|
||||
calendar_intervals = calendar._attendance_intervals_batch(pytz.utc.localize(datetime_start), pytz.utc.localize(datetime_stop))[False]
|
||||
entries_intervals = entries._to_intervals()
|
||||
overlapping_entries = self._from_intervals(entries_intervals & calendar_intervals)
|
||||
outside_entries |= entries - overlapping_entries
|
||||
outside_entries.write({'state': 'conflict'})
|
||||
return bool(outside_entries)
|
||||
|
||||
def _to_intervals(self):
|
||||
return WorkIntervals((w.date_start.replace(tzinfo=pytz.utc), w.date_stop.replace(tzinfo=pytz.utc), w) for w in self)
|
||||
|
||||
@api.model
|
||||
def _from_intervals(self, intervals):
|
||||
return self.browse(chain.from_iterable(recs.ids for start, end, recs in intervals))
|
||||
|
||||
|
||||
class HrWorkEntryType(models.Model):
|
||||
_inherit = 'hr.work.entry.type'
|
||||
_description = 'HR Work Entry Type'
|
||||
|
||||
is_leave = fields.Boolean(default=False, string="Time Off")
|
||||
@@ -0,0 +1,97 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from itertools import chain
|
||||
|
||||
|
||||
def _boundaries(intervals, opening, closing):
|
||||
""" Iterate on the boundaries of intervals. """
|
||||
for start, stop, recs in intervals:
|
||||
if start < stop:
|
||||
yield (start, opening, recs)
|
||||
yield (stop, closing, recs)
|
||||
|
||||
|
||||
class WorkIntervals(object):
|
||||
"""
|
||||
This class is a modified copy of the ``Intervals`` class in the resource module.
|
||||
A generic solution to handle intervals should probably be developped the day a similar
|
||||
class is needed elsewhere.
|
||||
|
||||
This implementation differs from the resource implementation in its management
|
||||
of two continuous intervals. Here, continuous intervals are not merged together
|
||||
while they are merged in resource.
|
||||
e.g.:
|
||||
In resource: (1, 4, rec1) and (4, 10, rec2) are merged into (1, 10, rec1 | rec2)
|
||||
Here: they remain two different intervals.
|
||||
To implement this behaviour, the main implementation change is the way boundaries are sorted.
|
||||
"""
|
||||
def __init__(self, intervals=()):
|
||||
self._items = []
|
||||
if intervals:
|
||||
# normalize the representation of intervals
|
||||
append = self._items.append
|
||||
starts = []
|
||||
recses = []
|
||||
for value, flag, recs in sorted(_boundaries(sorted(intervals), 'start', 'stop'), key=lambda i: i[0]):
|
||||
if flag == 'start':
|
||||
starts.append(value)
|
||||
recses.append(recs)
|
||||
else:
|
||||
start = starts.pop()
|
||||
if not starts:
|
||||
append((start, value, recses[0].union(*recses)))
|
||||
recses.clear()
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._items)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._items)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._items)
|
||||
|
||||
def __reversed__(self):
|
||||
return reversed(self._items)
|
||||
|
||||
def __or__(self, other):
|
||||
""" Return the union of two sets of intervals. """
|
||||
return WorkIntervals(chain(self._items, other._items))
|
||||
|
||||
def __and__(self, other):
|
||||
""" Return the intersection of two sets of intervals. """
|
||||
return self._merge(other, False)
|
||||
|
||||
def __sub__(self, other):
|
||||
""" Return the difference of two sets of intervals. """
|
||||
return self._merge(other, True)
|
||||
|
||||
def _merge(self, other, difference):
|
||||
""" Return the difference or intersection of two sets of intervals. """
|
||||
result = WorkIntervals()
|
||||
append = result._items.append
|
||||
|
||||
# using 'self' and 'other' below forces normalization
|
||||
bounds1 = _boundaries(sorted(self), 'start', 'stop')
|
||||
bounds2 = _boundaries(sorted(other), 'switch', 'switch')
|
||||
|
||||
start = None # set by start/stop
|
||||
recs1 = None # set by start
|
||||
enabled = difference # changed by switch
|
||||
for value, flag, recs in sorted(chain(bounds1, bounds2), key=lambda i: i[0]):
|
||||
if flag == 'start':
|
||||
start = value
|
||||
recs1 = recs
|
||||
elif flag == 'stop':
|
||||
if enabled and start < value:
|
||||
append((start, value, recs1))
|
||||
start = None
|
||||
else:
|
||||
if not enabled and start is not None:
|
||||
start = value
|
||||
if enabled and start is not None and start < value:
|
||||
append((start, value, recs1))
|
||||
enabled = not enabled
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="ir_rule_hr_work_entry_multi_company" model="ir.rule">
|
||||
<field name="name">HR Work Entry Contract: Multi Company</field>
|
||||
<field name="model_id" ref="model_hr_work_entry"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_hr_work_entry_regeneration_wizard,access_hr_work_entry_regeneration_wizard,model_hr_work_entry_regeneration_wizard,hr.group_hr_manager,1,1,1,1
|
||||
access_create_company_global_time_off,access_create_company_global_time_off,model_create_company_global_time_off,hr_contract.group_hr_contract_manager,1,1,1,0
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70">
|
||||
<defs>
|
||||
<path id="icon-a" d="M4,5.35309892e-14 C36.4160122,9.87060235e-15 58.0836068,-3.97961823e-14 65,5.07020818e-14 C69,6.733808e-14 70,1 70,5 C70,43.0488877 70,62.4235458 70,65 C70,69 69,70 65,70 C61,70 9,70 4,70 C1,70 7.10542736e-15,69 7.10542736e-15,65 C7.25721566e-15,62.4676575 3.83358709e-14,41.8005206 3.60818146e-14,5 C-1.13686838e-13,1 1,5.75716207e-14 4,5.35309892e-14 Z"/>
|
||||
<linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#CD7690"/>
|
||||
<stop offset="100%" stop-color="#CA5377"/>
|
||||
</linearGradient>
|
||||
<path id="icon-d" d="M29.5,16.9166667 C34.5338704,16.9166667 38.6145833,20.9973796 38.6145833,26.03125 C38.6145833,31.0651204 34.5338704,35.1458333 29.5,35.1458333 C24.4661296,35.1458333 20.3854167,31.0651204 20.3854167,26.03125 C20.3854167,20.9973796 24.4661296,16.9166667 29.5,16.9166667 Z M38.3969283,46.0833333 L17.6510417,46.0833333 C16.1408691,46.0833333 14.9166667,44.8591309 14.9166667,43.3489583 L14.9166667,41.2386475 C14.9166667,38.7291748 16.6245687,36.5417887 19.0590739,35.9331624 L23.1215007,34.91757 C26.4370443,37.3024007 31.6104248,37.9874756 35.8784993,34.91757 L38.085401,35.4692877 C38.5061499,38.7235298 38.4706769,43.0476895 38.3969283,46.0833333 Z M58.75,30.5 C59.9921875,30.5 61,31.6757812 61,33.125 L61,52.375 C61,53.8242187 59.9921875,55 58.75,55 L42.25,55 C41.0078125,55 40,53.8242187 40,52.375 L40,33.125 C40,31.6757812 41.0078125,30.5 42.25,30.5 L44.5,30.5 L44.5,27.65625 C44.5,27.2953125 44.753125,27 45.0625,27 L46.9375,27 C47.246875,27 47.5,27.2953125 47.5,27.65625 L47.5,30.5 L53.5,30.5 L53.5,27.65625 C53.5,27.2953125 53.753125,27 54.0625,27 L55.9375,27 C56.246875,27 56.5,27.2953125 56.5,27.65625 L56.5,30.5 L58.75,30.5 Z M58.46875,52.375 C58.6234375,52.375 58.75,52.2105322 58.75,52.009516 L58.75,33.8571429 L42.25,33.8571429 L42.25,52.009516 C42.25,52.2105322 42.3765625,52.375 42.53125,52.375 L58.46875,52.375 Z M49.1056813,41.3851318 C49.1056813,42.9223389 54.4224866,42.5321777 54.4224578,45.454248 C54.4224578,46.8539063 53.2582943,48.0494873 51.3671328,48.298877 L51.3671328,49.4570312 C51.3671328,49.6188232 51.2125191,49.75 51.0218203,49.75 L49.8707786,49.75 C49.6800798,49.75 49.5254661,49.6188232 49.5254661,49.4570312 L49.5254661,48.2798584 C48.3887549,48.1158447 47.3758383,47.6706543 46.6838033,47.1086426 C46.5578505,47.0063477 46.5419661,46.8402588 46.6463369,46.7217529 L47.520812,45.7289063 C47.6381319,45.5957275 47.8617793,45.5725586 48.0145801,45.6766602 C48.7315639,46.1651123 49.6576057,46.5528076 50.5424978,46.5528076 C51.5730542,46.5528076 52.0424202,46.0319336 52.0424202,45.5479736 C52.0424202,44.1172119 46.7256148,44.427832 46.7256148,41.4118896 C46.7256148,40.1247314 47.8384419,39.0840332 49.5254949,38.7595703 L49.5254949,37.5429688 C49.5254949,37.3811768 49.6801086,37.25 49.8708074,37.25 L51.0218491,37.25 C51.2125479,37.25 51.3671616,37.3811768 51.3671616,37.5429688 L51.3671616,38.6962646 C52.2924264,38.7869873 53.2953866,39.0949219 53.9940977,39.6192627 C54.1141225,39.7093262 54.1444237,39.8580811 54.068196,39.9776611 L53.3904626,41.0408691 C53.2913292,41.1964111 53.0539268,41.2421875 52.8847237,41.1389404 C52.241723,40.7466064 51.4535473,40.4472168 50.6844788,40.4472168 C49.7254021,40.4472168 49.1056813,40.8153809 49.1056813,41.3851318 Z"/>
|
||||
<path id="icon-e" d="M29.5,14.9166667 C34.5338704,14.9166667 38.6145833,18.9973796 38.6145833,24.03125 C38.6145833,29.0651204 34.5338704,33.1458333 29.5,33.1458333 C24.4661296,33.1458333 20.3854167,29.0651204 20.3854167,24.03125 C20.3854167,18.9973796 24.4661296,14.9166667 29.5,14.9166667 Z M38.3969283,44.0833333 L17.6510417,44.0833333 C16.1408691,44.0833333 14.9166667,42.8591309 14.9166667,41.3489583 L14.9166667,39.2386475 C14.9166667,36.7291748 16.6245687,34.5417887 19.0590739,33.9331624 L23.1215007,32.91757 C26.4370443,35.3024007 31.6104248,35.9874756 35.8784993,32.91757 L38.085401,33.4692877 C38.5061499,36.7235298 38.4706769,41.0476895 38.3969283,44.0833333 Z M58.75,28.5 C59.9921875,28.5 61,29.6757812 61,31.125 L61,50.375 C61,51.8242187 59.9921875,53 58.75,53 L42.25,53 C41.0078125,53 40,51.8242187 40,50.375 L40,31.125 C40,29.6757812 41.0078125,28.5 42.25,28.5 L44.5,28.5 L44.5,25.65625 C44.5,25.2953125 44.753125,25 45.0625,25 L46.9375,25 C47.246875,25 47.5,25.2953125 47.5,25.65625 L47.5,28.5 L53.5,28.5 L53.5,25.65625 C53.5,25.2953125 53.753125,25 54.0625,25 L55.9375,25 C56.246875,25 56.5,25.2953125 56.5,25.65625 L56.5,28.5 L58.75,28.5 Z M58.46875,50.375 C58.6234375,50.375 58.75,50.2105322 58.75,50.009516 L58.75,31.8571429 L42.25,31.8571429 L42.25,50.009516 C42.25,50.2105322 42.3765625,50.375 42.53125,50.375 L58.46875,50.375 Z M49.1056813,39.3851318 C49.1056813,40.9223389 54.4224866,40.5321777 54.4224578,43.454248 C54.4224578,44.8539063 53.2582943,46.0494873 51.3671328,46.298877 L51.3671328,47.4570312 C51.3671328,47.6188232 51.2125191,47.75 51.0218203,47.75 L49.8707786,47.75 C49.6800798,47.75 49.5254661,47.6188232 49.5254661,47.4570312 L49.5254661,46.2798584 C48.3887549,46.1158447 47.3758383,45.6706543 46.6838033,45.1086426 C46.5578505,45.0063477 46.5419661,44.8402588 46.6463369,44.7217529 L47.520812,43.7289063 C47.6381319,43.5957275 47.8617793,43.5725586 48.0145801,43.6766602 C48.7315639,44.1651123 49.6576057,44.5528076 50.5424978,44.5528076 C51.5730542,44.5528076 52.0424202,44.0319336 52.0424202,43.5479736 C52.0424202,42.1172119 46.7256148,42.427832 46.7256148,39.4118896 C46.7256148,38.1247314 47.8384419,37.0840332 49.5254949,36.7595703 L49.5254949,35.5429688 C49.5254949,35.3811768 49.6801086,35.25 49.8708074,35.25 L51.0218491,35.25 C51.2125479,35.25 51.3671616,35.3811768 51.3671616,35.5429688 L51.3671616,36.6962646 C52.2924264,36.7869873 53.2953866,37.0949219 53.9940977,37.6192627 C54.1141225,37.7093262 54.1444237,37.8580811 54.068196,37.9776611 L53.3904626,39.0408691 C53.2913292,39.1964111 53.0539268,39.2421875 52.8847237,39.1389404 C52.241723,38.7466064 51.4535473,38.4472168 50.6844788,38.4472168 C49.7254021,38.4472168 49.1056813,38.8153809 49.1056813,39.3851318 Z"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<mask id="icon-b" fill="#fff">
|
||||
<use xlink:href="#icon-a"/>
|
||||
</mask>
|
||||
<g mask="url(#icon-b)">
|
||||
<rect width="70" height="70" fill="url(#icon-c)"/>
|
||||
<path fill="#FFF" fill-opacity=".383" d="M4,1.8 L65,1.8 C67.6666667,1.8 69.3333333,1.13333333 70,-0.2 C70,2.46666667 70,3.46666667 70,2.8 L1.10547097e-14,2.8 C-1.65952376e-14,3.46666667 -2.9161925e-14,2.46666667 -2.66453526e-14,-0.2 C0.666666667,1.13333333 2,1.8 4,1.8 Z" transform="matrix(1 0 0 -1 0 2.8)"/>
|
||||
<path fill="#393939" d="M45.4871795,51 L4,51 C2,51 -7.10542736e-15,50.851312 0,46.8367347 L2.08199737e-16,23.7312137 L22,0 L37,1.04081633 L36,15.6122449 L40.5053241,10.5061222 L41.4366949,9.83683131 L44.9722671,6.08098476 L49,12.4897959 L53.8072717,6.21274166 L56,11.4489796 L59,11.4489796 L60.3332838,33.1194301 L45.4871795,51 Z" opacity=".324" transform="translate(0 19)"/>
|
||||
<path fill="#000" fill-opacity=".383" d="M4,4 L65,4 C67.6666667,4 69.3333333,3 70,1 C70,3.66666667 70,5 70,5 L1.77635684e-15,5 C1.77635684e-15,5 1.77635684e-15,3.66666667 1.77635684e-15,1 C0.666666667,3 2,4 4,4 Z" transform="translate(0 65)"/>
|
||||
<use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#icon-d"/>
|
||||
<use fill="#FFF" fill-rule="nonzero" xlink:href="#icon-e"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,76 @@
|
||||
odoo.define('hr_work_entry_contract.work_entries_calendar', function(require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var WorkEntryControllerMixin = require('hr_work_entry_contract.WorkEntryControllerMixin');
|
||||
var CalendarController = require("web.CalendarController");
|
||||
var CalendarModel = require('web.CalendarModel');
|
||||
var CalendarRenderer = require('web.CalendarRenderer');
|
||||
var CalendarView = require('web.CalendarView');
|
||||
var viewRegistry = require('web.view_registry');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
var WorkEntryCalendarController = CalendarController.extend(WorkEntryControllerMixin, {
|
||||
events: _.extend({}, WorkEntryControllerMixin.events, CalendarController.prototype.events),
|
||||
|
||||
// Returns the records from the model
|
||||
_fetchRecords: function () {
|
||||
var self = this;
|
||||
var records = _.filter(this.model.data.data, function (data) {
|
||||
// Filter records that are not inside the current month
|
||||
// (because in calendar view some days of prev. and next month are visible)
|
||||
return data.record.date_start.isBefore(self.lastDay) && data.record.date_stop.isAfter(self.firstDay);
|
||||
});
|
||||
return _.pluck(records, 'record');
|
||||
},
|
||||
_fetchFirstDay: function () {
|
||||
return this.model.data.target_date.clone().startOf('month');
|
||||
},
|
||||
_fetchLastDay: function () {
|
||||
return this.model.data.target_date.clone().endOf('month');
|
||||
},
|
||||
_displayWarning: function ($warning) {
|
||||
this.$('.o_calendar_container').before($warning);
|
||||
},
|
||||
});
|
||||
|
||||
var WorkEntryCalendarModel = CalendarModel.extend({
|
||||
/**
|
||||
* Display everybody's work entries if no employee filter exists
|
||||
* @private
|
||||
* @override
|
||||
* @param {any} filter
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_loadFilter: function (filter) {
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
var filters = filter.filters;
|
||||
var all_filter = filters[filters.length - 1];
|
||||
|
||||
if (all_filter) {
|
||||
all_filter.label = _t("Everybody's work entries");
|
||||
|
||||
if (filter.write_model && filter.filters.length <= 1 && all_filter.active === undefined) {
|
||||
filter.all = true;
|
||||
all_filter.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var WorkEntryCalendarView = CalendarView.extend({
|
||||
config: _.extend({}, CalendarView.prototype.config, {
|
||||
Controller: WorkEntryCalendarController,
|
||||
Model: WorkEntryCalendarModel,
|
||||
Renderer: CalendarRenderer,
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add('work_entries_calendar', WorkEntryCalendarView);
|
||||
|
||||
return WorkEntryCalendarController;
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
odoo.define('hr_work_entry_contract.WorkEntryControllerMixin', function(require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var time = require('web.time');
|
||||
|
||||
var _t = core._t;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
/*
|
||||
This mixin implements the behaviours necessary to generate and validate work entries and Payslips
|
||||
It is intended to be used in a Controller and requires four methods to be defined on your Controller
|
||||
|
||||
1. _fetchRecords
|
||||
Which should return a list of records containing at least the state and id fields
|
||||
|
||||
2. _fetchFirstDay
|
||||
Which should return the first day for which we will generate the work entries, it should be a Moment instance
|
||||
(Typically the first day of the current month)
|
||||
|
||||
3. _fetchLastDay
|
||||
Same as _fetchFirstDay except that this is the last day of the period
|
||||
|
||||
4. _displayWarning
|
||||
Which should insert in the DOM the warning rendered template received as argument.
|
||||
|
||||
This mixin is responsible for rendering the buttons in the control panel and adds the two following methods
|
||||
|
||||
1. _generateWorkEntries
|
||||
*/
|
||||
|
||||
var WorkEntryControllerMixin = {
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_update: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self.firstDay = self._fetchFirstDay().toDate();
|
||||
self.lastDay = self._fetchLastDay().toDate();
|
||||
var now = moment();
|
||||
if (self.firstDay > now) return Promise.resolve();
|
||||
return self._generateWorkEntries();
|
||||
});
|
||||
},
|
||||
|
||||
updateButtons: function() {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if(!this.$buttons) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$buttons.find('.btn-regenerate-work-entries').on('click', this._onRegenerateWorkEntries.bind(this));
|
||||
},
|
||||
|
||||
renderButtons: function($node) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if(this.$buttons) {
|
||||
this.$buttons.append(this._renderWorkEntryButtons());
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
Private
|
||||
*/
|
||||
_renderWorkEntryButtons: function() {
|
||||
return $('<span>').append(QWeb.render('hr_work_entry.work_entry_button', {
|
||||
button_text: _t("Regenerate Work Entries"),
|
||||
event_class: 'btn-regenerate-work-entries',
|
||||
}));
|
||||
},
|
||||
|
||||
_generateWorkEntries: function () {
|
||||
var self = this;
|
||||
return this._rpc({
|
||||
model: 'hr.employee',
|
||||
method: 'generate_work_entries',
|
||||
args: [[], time.date_to_str(this.firstDay), time.date_to_str(this.lastDay)],
|
||||
}).then(function (new_work_entries) {
|
||||
if (new_work_entries) {
|
||||
self.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_regenerateWorkEntries: function () {
|
||||
this.do_action('hr_work_entry_contract.hr_work_entry_regeneration_wizard_action', {
|
||||
additional_context: {
|
||||
date_start: time.date_to_str(this.firstDay),
|
||||
date_end: time.date_to_str(this.lastDay),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_onRegenerateWorkEntries: function (e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
this._regenerateWorkEntries();
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
return WorkEntryControllerMixin;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_work_entry
|
||||
from . import test_work_intervals
|
||||
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from odoo.fields import Date
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestWorkEntryBase(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestWorkEntryBase, cls).setUpClass()
|
||||
|
||||
cls.env.user.tz = 'Europe/Brussels'
|
||||
cls.env.ref('resource.resource_calendar_std').tz = 'Europe/Brussels'
|
||||
|
||||
cls.dep_rd = cls.env['hr.department'].create({
|
||||
'name': 'Research & Development - Test',
|
||||
})
|
||||
|
||||
# I create a new employee "Richard"
|
||||
cls.richard_emp = cls.env['hr.employee'].create({
|
||||
'name': 'Richard',
|
||||
'gender': 'male',
|
||||
'birthday': '1984-05-01',
|
||||
'country_id': cls.env.ref('base.be').id,
|
||||
'department_id': cls.dep_rd.id,
|
||||
})
|
||||
|
||||
# I create a contract for "Richard"
|
||||
cls.env['hr.contract'].create({
|
||||
'date_end': Date.today() + relativedelta(years=2),
|
||||
'date_start': Date.to_date('2018-01-01'),
|
||||
'name': 'Contract for Richard',
|
||||
'wage': 5000.0,
|
||||
'employee_id': cls.richard_emp.id,
|
||||
})
|
||||
|
||||
cls.work_entry_type = cls.env['hr.work.entry.type'].create({
|
||||
'name': 'Extra attendance',
|
||||
'is_leave': False,
|
||||
'code': 'WORKTEST200',
|
||||
})
|
||||
|
||||
cls.work_entry_type_unpaid = cls.env['hr.work.entry.type'].create({
|
||||
'name': 'Unpaid Leave',
|
||||
'is_leave': True,
|
||||
'code': 'LEAVETEST300',
|
||||
})
|
||||
|
||||
cls.work_entry_type_leave = cls.env['hr.work.entry.type'].create({
|
||||
'name': 'Leave',
|
||||
'is_leave': True,
|
||||
'code': 'LEAVETEST100'
|
||||
})
|
||||
|
||||
def create_work_entry(self, start, stop, work_entry_type=None):
|
||||
work_entry_type = work_entry_type or self.work_entry_type
|
||||
return self.env['hr.work.entry'].create({
|
||||
'contract_id': self.richard_emp.contract_ids[0].id,
|
||||
'name': "Work entry %s-%s" % (start, stop),
|
||||
'date_start': start,
|
||||
'date_stop': stop,
|
||||
'employee_id': self.richard_emp.id,
|
||||
'work_entry_type_id': work_entry_type.id,
|
||||
})
|
||||
@@ -0,0 +1,182 @@
|
||||
# # -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from psycopg2 import IntegrityError
|
||||
import pytz
|
||||
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.addons.hr_work_entry_contract.tests.common import TestWorkEntryBase
|
||||
|
||||
|
||||
@tagged('work_entry')
|
||||
class TestWorkEntry(TestWorkEntryBase):
|
||||
def setUp(self):
|
||||
super(TestWorkEntry, self).setUp()
|
||||
self.tz = pytz.timezone(self.richard_emp.tz)
|
||||
self.start = datetime(2015, 11, 1, 1, 0, 0)
|
||||
self.end = datetime(2015, 11, 30, 23, 59, 59)
|
||||
self.resource_calendar_id = self.env['resource.calendar'].create({'name': 'My Calendar'})
|
||||
contract = self.env['hr.contract'].create({
|
||||
'date_start': self.start.date() - relativedelta(days=5),
|
||||
'name': 'dodo',
|
||||
'resource_calendar_id': self.resource_calendar_id.id,
|
||||
'wage': 1000,
|
||||
'employee_id': self.richard_emp.id,
|
||||
'state': 'open',
|
||||
'date_generated_from': self.end.date() + relativedelta(days=5),
|
||||
})
|
||||
self.richard_emp.resource_calendar_id = self.resource_calendar_id
|
||||
self.richard_emp.contract_id = contract
|
||||
|
||||
def test_no_duplicate(self):
|
||||
self.richard_emp.generate_work_entries(self.start, self.end)
|
||||
pou1 = self.env['hr.work.entry'].search_count([])
|
||||
self.richard_emp.generate_work_entries(self.start, self.end)
|
||||
pou2 = self.env['hr.work.entry'].search_count([])
|
||||
self.assertEqual(pou1, pou2, "Work entries should not be duplicated")
|
||||
|
||||
def test_work_entry(self):
|
||||
self.richard_emp.generate_work_entries(self.start, self.end)
|
||||
attendance_nb = len(self.resource_calendar_id._attendance_intervals_batch(self.start.replace(tzinfo=pytz.utc), self.end.replace(tzinfo=pytz.utc))[False])
|
||||
work_entry_nb = self.env['hr.work.entry'].search_count([
|
||||
('employee_id', '=', self.richard_emp.id),
|
||||
('date_start', '>=', self.start),
|
||||
('date_stop', '<=', self.end)])
|
||||
self.assertEqual(attendance_nb, work_entry_nb, "One work_entry should be generated for each calendar attendance")
|
||||
|
||||
def test_approve_multiple_day_work_entry(self):
|
||||
start = datetime(2015, 11, 1, 9, 0, 0)
|
||||
end = datetime(2015, 11, 3, 18, 0, 0)
|
||||
|
||||
work_entry = self.env['hr.work.entry'].create({
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
'work_entry_type_id': self.work_entry_type.id,
|
||||
})
|
||||
work_entry.action_validate()
|
||||
work_entries = self.env['hr.work.entry'].search([('employee_id', '=', self.richard_emp.id)])
|
||||
self.assertTrue(all((b.state == 'validated' for b in work_entries)), "Work entries should be approved")
|
||||
|
||||
def test_validate_conflict_work_entry(self):
|
||||
start = datetime(2015, 11, 1, 9, 0, 0)
|
||||
end = datetime(2015, 11, 1, 13, 0, 0)
|
||||
work_entry1 = self.env['hr.work.entry'].create({
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'work_entry_type_id': self.env.ref('hr_work_entry.work_entry_type_attendance').id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': start,
|
||||
'date_stop': end + relativedelta(hours=5),
|
||||
})
|
||||
self.env['hr.work.entry'].create({
|
||||
'name': '2',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'work_entry_type_id': self.env.ref('hr_work_entry.work_entry_type_attendance').id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': start + relativedelta(hours=3),
|
||||
'date_stop': end,
|
||||
})
|
||||
self.assertFalse(work_entry1.action_validate(), "It should not validate work_entries conflicting with others")
|
||||
self.assertEqual(work_entry1.state, 'conflict')
|
||||
self.assertNotEqual(work_entry1.state, 'validated')
|
||||
|
||||
def test_validate_undefined_work_entry(self):
|
||||
work_entry1 = self.env['hr.work.entry'].create({
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': self.start,
|
||||
'date_stop': self.end,
|
||||
})
|
||||
self.assertFalse(work_entry1.action_validate(), "It should not validate work_entries without a type")
|
||||
self.assertEqual(work_entry1.state, 'conflict', "It should change to conflict state")
|
||||
work_entry1.work_entry_type_id = self.work_entry_type
|
||||
self.assertTrue(work_entry1.action_validate(), "It should validate work_entries")
|
||||
|
||||
def test_outside_calendar(self):
|
||||
""" Test leave work entries outside schedule are conflicting """
|
||||
# Outside but not a leave
|
||||
work_entry_1 = self.create_work_entry(datetime(2018, 10, 10, 3, 0), datetime(2018, 10, 10, 4, 0))
|
||||
# Outside and a leave
|
||||
work_entry_2 = self.create_work_entry(datetime(2018, 10, 10, 1, 0), datetime(2018, 10, 10, 2, 0), work_entry_type=self.work_entry_type_leave)
|
||||
# Overlapping and a leave
|
||||
work_entry_3 = self.create_work_entry(datetime(2018, 10, 10, 7, 0), datetime(2018, 10, 10, 10, 0), work_entry_type=self.work_entry_type_leave)
|
||||
# Overlapping and not a leave
|
||||
work_entry_4 = self.create_work_entry(datetime(2018, 10, 10, 11, 0), datetime(2018, 10, 10, 13, 0))
|
||||
(work_entry_1 | work_entry_2 | work_entry_3 | work_entry_4)._mark_leaves_outside_schedule()
|
||||
self.assertEqual(work_entry_2.state, 'conflict', "It should conflict")
|
||||
self.assertNotEqual(work_entry_1.state, 'conflict', "It should not conflict")
|
||||
self.assertNotEqual(work_entry_3.state, 'conflict', "It should not conflict")
|
||||
self.assertNotEqual(work_entry_4.state, 'conflict', "It should not conflict")
|
||||
|
||||
def test_write_conflict(self):
|
||||
""" Test updating work entries dates recomputes conflicts """
|
||||
work_entry_1 = self.create_work_entry(datetime(2018, 10, 10, 9, 0), datetime(2018, 10, 10, 12, 0))
|
||||
work_entry_2 = self.create_work_entry(datetime(2018, 10, 10, 12, 0), datetime(2018, 10, 10, 18, 0))
|
||||
self.assertNotEqual(work_entry_1.state, 'conflict', "It should not conflict")
|
||||
self.assertNotEqual(work_entry_2.state, 'conflict', "It should not conflict")
|
||||
work_entry_1.date_stop = datetime(2018, 10, 10, 14, 0)
|
||||
self.assertEqual(work_entry_1.state, 'conflict', "It should conflict")
|
||||
self.assertEqual(work_entry_2.state, 'conflict', "It should conflict")
|
||||
|
||||
work_entry_1.date_stop = datetime(2018, 10, 10, 12, 0) # cancel conflict
|
||||
self.assertNotEqual(work_entry_1.state, 'conflict', "It should no longer conflict")
|
||||
self.assertNotEqual(work_entry_2.state, 'conflict', "It should no longer conflict")
|
||||
|
||||
def test_write_move(self):
|
||||
""" Test completely moving a work entry recomputes conflicts """
|
||||
work_entry_1 = self.create_work_entry(datetime(2018, 10, 10, 9, 0), datetime(2018, 10, 10, 12, 0))
|
||||
work_entry_2 = self.create_work_entry(datetime(2018, 10, 18, 9, 0), datetime(2018, 10, 18, 12, 0))
|
||||
work_entry_3 = self.create_work_entry(datetime(2018, 10, 18, 10, 0), datetime(2018, 10, 18, 12, 0))
|
||||
work_entry_2.write({
|
||||
'date_start': datetime(2018, 10, 10, 9, 0),
|
||||
'date_stop': datetime(2018, 10, 10, 10, 0),
|
||||
})
|
||||
self.assertEqual(work_entry_1.state, 'conflict')
|
||||
self.assertEqual(work_entry_2.state, 'conflict')
|
||||
self.assertNotEqual(work_entry_3.state, 'conflict')
|
||||
|
||||
def test_create_conflict(self):
|
||||
""" Test creating a work entry recomputes conflicts """
|
||||
work_entry_1 = self.create_work_entry(datetime(2018, 10, 10, 9, 0), datetime(2018, 10, 10, 12, 0))
|
||||
self.assertNotEqual(work_entry_1.state, 'conflict', "It should not conflict")
|
||||
work_entry_2 = self.create_work_entry(datetime(2018, 10, 10, 10, 0), datetime(2018, 10, 10, 18, 0))
|
||||
self.assertEqual(work_entry_1.state, 'conflict', "It should conflict")
|
||||
self.assertEqual(work_entry_2.state, 'conflict', "It should conflict")
|
||||
|
||||
def test_unarchive_conflict(self):
|
||||
""" Test archive/unarchive a work entry recomputes conflicts """
|
||||
work_entry_1 = self.create_work_entry(datetime(2018, 10, 10, 9, 0), datetime(2018, 10, 10, 12, 0))
|
||||
work_entry_2 = self.create_work_entry(datetime(2018, 10, 10, 10, 0), datetime(2018, 10, 10, 18, 0))
|
||||
work_entry_2.active = False
|
||||
self.assertNotEqual(work_entry_1.state, 'conflict', "It should not conflict")
|
||||
self.assertEqual(work_entry_2.state, 'cancelled', "It should be cancelled")
|
||||
work_entry_2.active = True
|
||||
self.assertEqual(work_entry_1.state, 'conflict', "It should conflict")
|
||||
self.assertEqual(work_entry_2.state, 'conflict', "It should conflict")
|
||||
|
||||
def test_validated_no_conflict(self):
|
||||
""" Test validating a work entry removes the conflict """
|
||||
work_entry_1 = self.create_work_entry(datetime(2018, 10, 10, 9, 0), datetime(2018, 10, 10, 12, 0))
|
||||
work_entry_1.state = 'validated'
|
||||
work_entry_2 = self.create_work_entry(datetime(2018, 10, 10, 10, 0), datetime(2018, 10, 10, 18, 0))
|
||||
self.assertEqual(work_entry_1.state, 'conflict', "It should not conflict")
|
||||
self.assertEqual(work_entry_2.state, 'conflict', "It should conflict")
|
||||
|
||||
def test_no_overlap_sql_constraint(self):
|
||||
""" Test _work_entries_no_validated_conflict sql constrains """
|
||||
work_entry_1 = self.create_work_entry(datetime(2018, 10, 10, 9, 0), datetime(2018, 10, 10, 12, 0))
|
||||
work_entry_1.state = 'validated'
|
||||
work_entry_2 = self.create_work_entry(datetime(2018, 10, 10, 8, 0), datetime(2018, 10, 10, 10, 0))
|
||||
self.assertEqual(work_entry_1.state, 'conflict')
|
||||
|
||||
with mute_logger('odoo.sql_db'):
|
||||
with self.assertRaises(IntegrityError):
|
||||
with self.cr.savepoint():
|
||||
(work_entry_1 + work_entry_2).write({'state': 'validated'})
|
||||
@@ -0,0 +1,70 @@
|
||||
from odoo.addons.hr_work_entry_contract.models.hr_work_intervals import WorkIntervals
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestIntervals(TransactionCase):
|
||||
|
||||
def ints(self, pairs):
|
||||
recs = self.env['base']
|
||||
return [(a, b, recs) for a, b in pairs]
|
||||
|
||||
def test_union(self):
|
||||
def check(a, b):
|
||||
a, b = self.ints(a), self.ints(b)
|
||||
self.assertEqual(list(WorkIntervals(a)), b)
|
||||
|
||||
check([(1, 2), (3, 4)], [(1, 2), (3, 4)])
|
||||
check([(1, 2), (2, 4)], [(1, 2), (2, 4)])
|
||||
check([(1, 3), (2, 4)], [(1, 4)])
|
||||
check([(1, 4), (2, 3)], [(1, 4)])
|
||||
check([(1, 4), (1, 4)], [(1, 4)])
|
||||
check([(3, 4), (1, 2)], [(1, 2), (3, 4)])
|
||||
check([(2, 4), (1, 2)], [(1, 2), (2, 4)])
|
||||
check([(2, 4), (1, 3)], [(1, 4)])
|
||||
check([(2, 3), (1, 4)], [(1, 4)])
|
||||
|
||||
def test_intersection(self):
|
||||
def check(a, b, c):
|
||||
a, b, c = self.ints(a), self.ints(b), self.ints(c)
|
||||
self.assertEqual(list(WorkIntervals(a) & WorkIntervals(b)), c)
|
||||
|
||||
check([(10, 20)], [(5, 8)], [])
|
||||
check([(10, 20)], [(5, 10)], [])
|
||||
check([(10, 20)], [(5, 15)], [(10, 15)])
|
||||
check([(10, 20)], [(5, 20)], [(10, 20)])
|
||||
check([(10, 20)], [(5, 25)], [(10, 20)])
|
||||
check([(10, 20)], [(10, 15)], [(10, 15)])
|
||||
check([(10, 20)], [(10, 20)], [(10, 20)])
|
||||
check([(10, 20)], [(10, 25)], [(10, 20)])
|
||||
check([(10, 20)], [(15, 18)], [(15, 18)])
|
||||
check([(10, 20)], [(15, 20)], [(15, 20)])
|
||||
check([(10, 20)], [(15, 25)], [(15, 20)])
|
||||
check([(10, 20)], [(20, 25)], [])
|
||||
check(
|
||||
[(0, 5), (10, 15), (20, 25), (30, 35)],
|
||||
[(6, 7), (9, 12), (13, 17), (22, 23), (24, 40)],
|
||||
[(10, 12), (13, 15), (22, 23), (24, 25), (30, 35)],
|
||||
)
|
||||
|
||||
def test_difference(self):
|
||||
def check(a, b, c):
|
||||
a, b, c = self.ints(a), self.ints(b), self.ints(c)
|
||||
self.assertEqual(list(WorkIntervals(a) - WorkIntervals(b)), c)
|
||||
|
||||
check([(10, 20)], [(5, 8)], [(10, 20)])
|
||||
check([(10, 20)], [(5, 10)], [(10, 20)])
|
||||
check([(10, 20)], [(5, 15)], [(15, 20)])
|
||||
check([(10, 20)], [(5, 20)], [])
|
||||
check([(10, 20)], [(5, 25)], [])
|
||||
check([(10, 20)], [(10, 15)], [(15, 20)])
|
||||
check([(10, 20)], [(10, 20)], [])
|
||||
check([(10, 20)], [(10, 25)], [])
|
||||
check([(10, 20)], [(15, 18)], [(10, 15), (18, 20)])
|
||||
check([(10, 20)], [(15, 20)], [(10, 15)])
|
||||
check([(10, 20)], [(15, 25)], [(10, 15)])
|
||||
check([(10, 20)], [(20, 25)], [(10, 20)])
|
||||
check(
|
||||
[(0, 5), (10, 15), (20, 25), (30, 35)],
|
||||
[(6, 7), (9, 12), (13, 17), (22, 23), (24, 40)],
|
||||
[(0, 5), (12, 13), (20, 22), (23, 24)],
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="assets_backend" inherit_id="web.assets_backend">
|
||||
<xpath expr="script[last()]" position="after">
|
||||
<script type="text/javascript" src="/hr_work_entry_contract/static/src/js/work_entries_controller_mixin.js"></script>
|
||||
<script type="text/javascript" src="/hr_work_entry_contract/static/src/js/work_entries_calendar.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_work_entry_contract_view_calendar_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.work.entry.contract.view.calendar.inherit</field>
|
||||
<field name="model">hr.work.entry</field>
|
||||
<field name="inherit_id" ref="hr_work_entry.hr_work_entry_view_calendar"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//calendar" position="attributes">
|
||||
<attribute name="js_class">work_entries_calendar</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_work_entry_contract_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.work.entry.contract.view.form.inherit</field>
|
||||
<field name="model">hr.work.entry</field>
|
||||
<field name="inherit_id" ref="hr_work_entry.hr_work_entry_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet" position="before">
|
||||
<div attrs="{'invisible': [('state', '!=', 'conflict')]}">
|
||||
<div class="alert alert-warning mb-0" role="alert" attrs="{'invisible': ['!', ('work_entry_type_id', '=', False)]}" name="work_entry_undefined">
|
||||
This work entry cannot be validated. The work entry type is undefined.
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_work_entry_contract_type_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.work.entry.type.contract.view.form.inherit</field>
|
||||
<field name="model">hr.work.entry.type</field>
|
||||
<field name="inherit_id" ref="hr_work_entry.hr_work_entry_type_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='identification']" position="after">
|
||||
<group>
|
||||
<field name="is_leave"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Calendar View -->
|
||||
<record id="resource_calendar_view_form" model="ir.ui.view">
|
||||
<field name="name">resource.calendar.view.form.inherit</field>
|
||||
<field name="model">resource.calendar</field>
|
||||
<field name="inherit_id" ref="resource.resource_calendar_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet" position="before">
|
||||
<header>
|
||||
<button name="%(action_create_company_global_time_off)d" string="Create Company Public Time Off" type="action" class="oe_highlight"/>
|
||||
</header>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import hr_work_entry_regeneration_wizard
|
||||
from . import create_company_global_time_off
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrPayrollIndex(models.TransientModel):
|
||||
_name = 'create.company.global.time.off'
|
||||
_description = 'Create Company Public Time Off'
|
||||
|
||||
name = fields.Char('Reason', required=True)
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.company, required=True)
|
||||
date_from = fields.Datetime('Start Date', required=True)
|
||||
date_to = fields.Datetime('End Date', required=True)
|
||||
work_entry_type_id = fields.Many2one('hr.work.entry.type', 'Work Entry Type', required=True)
|
||||
|
||||
def action_confirm(self):
|
||||
self.ensure_one()
|
||||
self.env['resource.calendar'].search([('company_id', '=', self.company_id.id)]).write({
|
||||
'leave_ids': [(0, 0, {
|
||||
'name': self.name,
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
'time_type': 'leave',
|
||||
'work_entry_type_id': self.work_entry_type_id.id,
|
||||
})]
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="create_company_global_time_off_view_form" model="ir.ui.view">
|
||||
<field name="name">create.company.global.time.off.view.form</field>
|
||||
<field name="model">create.company.global.time.off</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create Company Public Time Off">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<label for="date_from" string="Period"/>
|
||||
<div>
|
||||
<field name="date_from" class="oe_inline"/>
|
||||
<span> to </span>
|
||||
<field name="date_to" class="oe_inline"/>
|
||||
</div>
|
||||
<field name="company_id"/>
|
||||
<field name="work_entry_type_id"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Confirm" name="action_confirm" type="object" class="oe_highlight"/>
|
||||
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_create_company_global_time_off" model="ir.actions.act_window">
|
||||
<field name="name">Create Company Public Time Off</field>
|
||||
<field name="res_model">create.company.global.time.off</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="create_company_global_time_off_view_form"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,97 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrWorkEntryRegenerationWizard(models.TransientModel):
|
||||
_name = 'hr.work.entry.regeneration.wizard'
|
||||
_description = 'Regenerate Employee Work Entries'
|
||||
|
||||
earliest_available_date = fields.Date('Earliest date', compute='_compute_earliest_available_date')
|
||||
earliest_available_date_message = fields.Char(readonly=True, store=False, default='')
|
||||
latest_available_date = fields.Date('Latest date', compute='_compute_latest_available_date')
|
||||
latest_available_date_message = fields.Char(readonly=True, store=False, default='')
|
||||
date_from = fields.Date('From', required=True, default=lambda self: self.env.context.get('date_start'))
|
||||
date_to = fields.Date('To', required=True, default=lambda self: self.env.context.get('date_end'))
|
||||
employee_id = fields.Many2one('hr.employee', 'Employee', required=True)
|
||||
validated_work_entry_ids = fields.Many2many('hr.work.entry', string='Work Entries Within Interval',
|
||||
compute='_compute_validated_work_entry_ids')
|
||||
search_criteria_completed = fields.Boolean(compute='_compute_search_criteria_completed')
|
||||
valid = fields.Boolean(compute='_compute_valid')
|
||||
|
||||
@api.depends('employee_id')
|
||||
def _compute_earliest_available_date(self):
|
||||
for wizard in self:
|
||||
dates = wizard.mapped('employee_id.contract_ids.date_generated_from')
|
||||
wizard.earliest_available_date = min(dates) if dates else None
|
||||
|
||||
@api.depends('employee_id')
|
||||
def _compute_latest_available_date(self):
|
||||
for wizard in self:
|
||||
dates = wizard.mapped('employee_id.contract_ids.date_generated_to')
|
||||
wizard.latest_available_date = max(dates) if dates else None
|
||||
|
||||
@api.depends('date_from', 'date_to', 'employee_id')
|
||||
def _compute_validated_work_entry_ids(self):
|
||||
for wizard in self:
|
||||
validated_work_entry_ids = self.env['hr.work.entry']
|
||||
if wizard.search_criteria_completed:
|
||||
search_domain = [('employee_id', '=', self.employee_id.id),
|
||||
('date_start', '>=', self.date_from),
|
||||
('date_stop', '<=', self.date_to),
|
||||
('state', '=', 'validated')]
|
||||
validated_work_entry_ids = self.env['hr.work.entry'].search(search_domain, order="date_start")
|
||||
wizard.validated_work_entry_ids = validated_work_entry_ids
|
||||
|
||||
@api.depends('validated_work_entry_ids')
|
||||
def _compute_valid(self):
|
||||
for wizard in self:
|
||||
wizard.valid = wizard.search_criteria_completed and len(wizard.validated_work_entry_ids) == 0
|
||||
|
||||
@api.depends('date_from', 'date_to', 'employee_id')
|
||||
def _compute_search_criteria_completed(self):
|
||||
for wizard in self:
|
||||
wizard.search_criteria_completed = wizard.date_from and wizard.date_to and wizard.employee_id and wizard.earliest_available_date and wizard.latest_available_date
|
||||
|
||||
@api.onchange('date_from', 'date_to', 'employee_id')
|
||||
def _check_dates(self):
|
||||
for wizard in self:
|
||||
wizard.earliest_available_date_message = ''
|
||||
wizard.latest_available_date_message = ''
|
||||
if wizard.search_criteria_completed:
|
||||
if wizard.date_from > wizard.date_to:
|
||||
date_from = wizard.date_from
|
||||
wizard.date_from = wizard.date_to
|
||||
wizard.date_to = date_from
|
||||
if wizard.earliest_available_date and wizard.date_from < wizard.earliest_available_date:
|
||||
wizard.date_from = wizard.earliest_available_date
|
||||
wizard.earliest_available_date_message = 'The earliest available date is {date}' \
|
||||
.format(date=self._date_to_string(wizard.earliest_available_date))
|
||||
if wizard.latest_available_date and wizard.date_to > wizard.latest_available_date:
|
||||
wizard.date_to = wizard.latest_available_date
|
||||
wizard.latest_available_date_message = 'The latest available date is {date}' \
|
||||
.format(date=self._date_to_string(wizard.latest_available_date))
|
||||
|
||||
@api.model
|
||||
def _date_to_string(self, date):
|
||||
if not date:
|
||||
return ''
|
||||
user_date_format = self.env['res.lang']._lang_get(self.env.user.lang).date_format
|
||||
return date.strftime(user_date_format)
|
||||
|
||||
def regenerate_work_entries(self):
|
||||
self.ensure_one()
|
||||
if not self.valid:
|
||||
raise ValidationError(_("In order to regenerate the work entries, you need to provide the wizard with an employee_id, a date_from and a date_to. In addition to that, the time interval defined by date_from and date_to must not contain any validated work entries."))
|
||||
|
||||
if self.date_from < self.earliest_available_date or self.date_to > self.latest_available_date:
|
||||
raise ValidationError(_("The from date must be >= '%(earliest_available_date)s' and the to date must be <= '%(latest_available_date)s', which correspond to the generated work entries time interval.", earliest_available_date=self._date_to_string(self.earliest_available_date), latest_available_date=self._date_to_string(self.latest_available_date)))
|
||||
work_entries = self.env['hr.work.entry'].search([('employee_id', '=', self.employee_id.id),
|
||||
('date_stop', '>=', self.date_from),
|
||||
('date_start', '<=', self.date_to)])
|
||||
work_entries.write({'active': False})
|
||||
self.employee_id.generate_work_entries(self.date_from, self.date_to, True)
|
||||
action = self.env["ir.actions.actions"]._for_xml_id('hr_work_entry.hr_work_entry_action')
|
||||
return action
|
||||
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_work_entry_regeneration_wizard" model="ir.ui.view">
|
||||
<field name="name">hr_work_entry_regeneration_wizard</field>
|
||||
<field name="model">hr.work.entry.regeneration.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Regenerate Employee Work Entries">
|
||||
<group>
|
||||
<group>
|
||||
<field name="employee_id"/>
|
||||
<label for="date_from"></label>
|
||||
<div name="date_from">
|
||||
<div class="text-info" attrs="{'invisible': [('earliest_available_date_message', '=', '')]}">
|
||||
<i class="fa fa-info-circle mr-1" title="Hint"/>
|
||||
<field name="earliest_available_date_message" nolabel="1"/>
|
||||
</div >
|
||||
<field name="date_from"/>
|
||||
</div>
|
||||
<label for="date_to"></label>
|
||||
<div name="date_to">
|
||||
<div class="text-info" attrs="{'invisible': [('latest_available_date_message', '=', '')]}">
|
||||
<i class="fa fa-info-circle mr-1" title="Hint"/>
|
||||
<field name="latest_available_date_message" nolabel="1"/>
|
||||
</div>
|
||||
<field name="date_to"/>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<field name="search_criteria_completed" invisible="1"/>
|
||||
<field name="valid" invisible="1"/>
|
||||
<div attrs="{'invisible': ['|', ('search_criteria_completed', '=', False), ('valid', '=', True)]}">
|
||||
<div class="text-danger"><i class="fa fa-exclamation-triangle mr-1" title="Warning"/>You are not allowed to regenerate validated work entries</div>
|
||||
<field name="validated_work_entry_ids" widget="many2many" nolabel="1">
|
||||
<tree string="Work Entries"
|
||||
default_order = "date_start"
|
||||
|
||||
editable="bottom"
|
||||
no_open="1" decoration-danger="state == 'validated'">
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_stop"/>
|
||||
<field name="work_entry_type_id"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</div>
|
||||
<footer>
|
||||
<button name="regenerate_work_entries"
|
||||
string="Regenerate Work Entries"
|
||||
type="object" class="oe_highlight"
|
||||
attrs="{'invisible': ['|', ('search_criteria_completed', '=', False), ('valid', '=', False)]}"/>
|
||||
<button name="regenerate_work_entries_disabled"
|
||||
string="Regenerate Work Entries"
|
||||
disabled="1"
|
||||
attrs="{'invisible': [('search_criteria_completed', '=', True), ('valid', '=', True)]}"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_work_entry_regeneration_wizard_action" model="ir.actions.act_window">
|
||||
<field name="name">Work Entry Regeneration</field>
|
||||
<field name="res_model">hr.work.entry.regeneration.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
|
||||
def _validate_existing_work_entry(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
env['hr.work.entry'].search([])._check_if_error()
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
{
|
||||
'name': 'Time Off in Payslips',
|
||||
'version': '1.0',
|
||||
'category': 'Human Resources/Payroll',
|
||||
'sequence': 95,
|
||||
'summary': 'Manage Time Off in Payslips',
|
||||
'description': """
|
||||
Manage Time Off in Payslips
|
||||
============================
|
||||
|
||||
This application allows you to integrate time off in payslips.
|
||||
""",
|
||||
'depends': ['hr_holidays', 'hr_work_entry_contract'],
|
||||
'data': [
|
||||
'views/hr_leave_views.xml',
|
||||
],
|
||||
'demo': ['data/hr_payroll_holidays_demo.xml'],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': True,
|
||||
'post_init_hook': '_validate_existing_work_entry',
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="hr_holidays.holiday_status_comp" model="hr.leave.type">
|
||||
<field name="work_entry_type_id" ref="hr_work_entry_contract.work_entry_type_compensatory"></field>
|
||||
</record>
|
||||
|
||||
<record id="hr_holidays.holiday_status_unpaid" model="hr.leave.type">
|
||||
<field name="work_entry_type_id" ref="hr_work_entry_contract.work_entry_type_unpaid_leave"></field>
|
||||
</record>
|
||||
|
||||
<record id="hr_holidays.holiday_status_sl" model="hr.leave.type">
|
||||
<field name="work_entry_type_id" ref="hr_work_entry_contract.work_entry_type_sick_leave"></field>
|
||||
</record>
|
||||
|
||||
<record id="hr_holidays.holiday_status_cl" model="hr.leave.type">
|
||||
<field name="work_entry_type_id" ref="hr_work_entry_contract.work_entry_type_legal_leave"></field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- add work_entry type to leave type -->
|
||||
<record id="hr_holidays.holiday_status_unpaid" model="hr.leave.type">
|
||||
<field name="work_entry_type_id" ref="hr_work_entry_contract.work_entry_type_unpaid_leave"></field>
|
||||
</record>
|
||||
|
||||
<record id="hr_holidays.holiday_status_comp" model="hr.leave.type">
|
||||
<field name="work_entry_type_id" ref="hr_work_entry_contract.work_entry_type_leave"></field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,154 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * hr_work_entry_holidays
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~14.2+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-18 14:36+0000\n"
|
||||
"PO-Revision-Date: 2021-02-18 14:36+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model_terms:ir.actions.act_window,help:hr_work_entry_holidays.hr_leave_work_entry_action
|
||||
msgid ""
|
||||
"A great way to keep track on employee’s PTOs, sick days, and approval "
|
||||
"status."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: code:addons/hr_work_entry_holidays/models/hr_leave.py:0
|
||||
#, python-format
|
||||
msgid "A leave cannot be set across multiple contracts."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.actions.act_window,name:hr_work_entry_holidays.hr_leave_work_entry_action
|
||||
msgid "All Time Off"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_holidays.payroll_hr_work_entry_view_form_inherit
|
||||
msgid "Approve Time Off"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_contract__display_name
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_leave__display_name
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_leave_type__display_name
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_work_entry__display_name
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_work_entry_type__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.model,name:hr_work_entry_holidays.model_hr_contract
|
||||
msgid "Employee Contract"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.model,name:hr_work_entry_holidays.model_hr_work_entry
|
||||
msgid "HR Work Entry"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.model,name:hr_work_entry_holidays.model_hr_work_entry_type
|
||||
msgid "HR Work Entry Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_contract__id
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_leave__id
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_leave_type__id
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_work_entry__id
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_work_entry_type__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_contract____last_update
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_leave____last_update
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_leave_type____last_update
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_work_entry____last_update
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_work_entry_type____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model_terms:ir.actions.act_window,help:hr_work_entry_holidays.hr_leave_work_entry_action
|
||||
msgid "Meet the time off dashboard."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_holidays.payroll_hr_work_entry_view_form_inherit
|
||||
msgid "Refuse Time Off"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_work_entry__leave_state
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.model.fields,help:hr_work_entry_holidays.field_hr_work_entry__leave_state
|
||||
msgid ""
|
||||
"The status is set to 'To Submit', when a time off request is created.\n"
|
||||
"The status is 'To Approve', when time off request is confirmed by user.\n"
|
||||
"The status is 'Refused', when time off request is refused by manager.\n"
|
||||
"The status is 'Approved', when time off request is approved by manager."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: code:addons/hr_work_entry_holidays/models/hr_leave.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"There is no employee set on the time off. Please make sure you're logged in "
|
||||
"the correct company."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_holidays.payroll_hr_work_entry_view_form_inherit_contract
|
||||
msgid ""
|
||||
"This work entry cannot be validated. It is conflicting with at least one work entry. <br/>\n"
|
||||
" Two work entries of the same employee cannot overlap at the same time."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_holidays.payroll_hr_work_entry_view_form_inherit_contract
|
||||
msgid ""
|
||||
"This work entry cannot be validated. There is a leave to approve (or refuse)"
|
||||
" at the same time."
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.model,name:hr_work_entry_holidays.model_hr_leave
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_work_entry__leave_id
|
||||
msgid "Time Off"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.model,name:hr_work_entry_holidays.model_hr_leave_type
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_work_entry_type__leave_type_ids
|
||||
msgid "Time Off Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.ui.menu,name:hr_work_entry_holidays.menu_work_entry_leave_to_approve
|
||||
msgid "Time Off to Report"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model_terms:ir.ui.view,arch_db:hr_work_entry_holidays.work_entry_type_leave_form_inherit
|
||||
msgid "Work Entries"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_work_entry_holidays
|
||||
#: model:ir.model.fields,field_description:hr_work_entry_holidays.field_hr_leave_type__work_entry_type_id
|
||||
msgid "Work Entry Type"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import hr_contract
|
||||
from . import hr_leave
|
||||
from . import hr_work_entry
|
||||
@@ -0,0 +1,73 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import pytz
|
||||
|
||||
from datetime import date
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class HrContract(models.Model):
|
||||
_inherit = 'hr.contract'
|
||||
_description = 'Employee Contract'
|
||||
|
||||
@api.constrains('date_start', 'date_end', 'state')
|
||||
def _check_contracts(self):
|
||||
self._get_leaves()._check_contracts()
|
||||
|
||||
def _get_leaves(self):
|
||||
return self.env['hr.leave'].search([
|
||||
('employee_id', 'in', self.mapped('employee_id.id')),
|
||||
('date_from', '<=', max([end or date.max for end in self.mapped('date_end')])),
|
||||
('date_to', '>=', min(self.mapped('date_start'))),
|
||||
])
|
||||
|
||||
# override to add work_entry_type from leave
|
||||
def _get_leave_work_entry_type(self, leave):
|
||||
if leave.holiday_id:
|
||||
return leave.holiday_id.holiday_status_id.work_entry_type_id
|
||||
else:
|
||||
return leave.work_entry_type_id
|
||||
|
||||
# YTI TODO: Master remove the method (deprecated)
|
||||
def _get_more_vals_leave(self, leave):
|
||||
return [('leave_id', leave.holiday_id and leave.holiday_id.id)]
|
||||
|
||||
def _get_more_vals_leave_interval(self, interval, leaves):
|
||||
result = super()._get_more_vals_leave_interval(interval, leaves)
|
||||
for leave in leaves:
|
||||
if interval[0] >= leave[0] and interval[1] <= leave[1]:
|
||||
result.append(('leave_id', leave[2].holiday_id.id))
|
||||
return result
|
||||
|
||||
def _get_bypassing_work_entry_type(self):
|
||||
return self.env['hr.work.entry.type']
|
||||
|
||||
def _get_interval_leave_work_entry_type(self, interval, leaves):
|
||||
# returns the work entry time related to the leave that
|
||||
# includes the whole interval.
|
||||
# Overriden in hr_work_entry_contract_holiday to select the
|
||||
# global time off first (eg: Public Holiday > Home Working)
|
||||
interval_start = interval[0].astimezone(pytz.utc).replace(tzinfo=None)
|
||||
interval_stop = interval[1].astimezone(pytz.utc).replace(tzinfo=None)
|
||||
including_rcleaves = [l[2] for l in leaves if l[2] and interval_start >= l[2].date_from and interval_stop <= l[2].date_to]
|
||||
including_global_rcleaves = [l for l in including_rcleaves if not l.holiday_id]
|
||||
including_holiday_rcleaves = [l for l in including_rcleaves if l.holiday_id]
|
||||
rc_leave = False
|
||||
|
||||
# Example: In CP200: Long term sick > Public Holidays (which is global)
|
||||
bypassing_work_entry_types = self._get_bypassing_work_entry_type()
|
||||
if bypassing_work_entry_types:
|
||||
bypassing_rc_leave = [l for l in including_holiday_rcleaves if l.holiday_id.holiday_status_id.work_entry_type_id in bypassing_work_entry_types]
|
||||
else:
|
||||
bypassing_rc_leave = []
|
||||
|
||||
if bypassing_rc_leave:
|
||||
rc_leave = bypassing_rc_leave[0]
|
||||
elif including_global_rcleaves:
|
||||
rc_leave = including_global_rcleaves[0]
|
||||
elif including_holiday_rcleaves:
|
||||
rc_leave = including_holiday_rcleaves[0]
|
||||
if rc_leave:
|
||||
return self._get_leave_work_entry_type_dates(rc_leave, interval_start, interval_stop)
|
||||
return self.env.ref('hr_work_entry_contract.work_entry_type_leave')
|
||||
@@ -0,0 +1,210 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, date
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrLeaveType(models.Model):
|
||||
_inherit = 'hr.leave.type'
|
||||
|
||||
work_entry_type_id = fields.Many2one('hr.work.entry.type', string='Work Entry Type')
|
||||
|
||||
|
||||
class HrLeave(models.Model):
|
||||
_inherit = 'hr.leave'
|
||||
|
||||
def _create_resource_leave(self):
|
||||
"""
|
||||
Add a resource leave in calendars of contracts running at the same period.
|
||||
This is needed in order to compute the correct number of hours/days of the leave
|
||||
according to the contract's calender.
|
||||
"""
|
||||
resource_leaves = super(HrLeave, self)._create_resource_leave()
|
||||
for resource_leave in resource_leaves:
|
||||
resource_leave.work_entry_type_id = resource_leave.holiday_id.holiday_status_id.work_entry_type_id.id
|
||||
|
||||
resource_leave_values = []
|
||||
|
||||
for leave in self.filtered(lambda l: l.employee_id):
|
||||
|
||||
contract = leave.employee_id.sudo()._get_contracts(leave.date_from, leave.date_to, states=['open'])
|
||||
if contract and contract.resource_calendar_id != leave.employee_id.resource_calendar_id:
|
||||
resource_leave_values += [{
|
||||
'name': leave.name,
|
||||
'holiday_id': leave.id,
|
||||
'resource_id': leave.employee_id.resource_id.id,
|
||||
'work_entry_type_id': leave.holiday_status_id.work_entry_type_id.id,
|
||||
'time_type': leave.holiday_status_id.time_type,
|
||||
'date_from': max(leave.date_from, datetime.combine(contract.date_start, datetime.min.time())),
|
||||
'date_to': min(leave.date_to, datetime.combine(contract.date_end or date.max, datetime.max.time())),
|
||||
'calendar_id': contract.resource_calendar_id.id,
|
||||
}]
|
||||
|
||||
return resource_leaves | self.env['resource.calendar.leaves'].create(resource_leave_values)
|
||||
|
||||
@api.constrains('date_from', 'date_to')
|
||||
def _check_contracts(self):
|
||||
"""
|
||||
A leave cannot be set across multiple contracts.
|
||||
Note: a leave can be across multiple contracts despite this constraint.
|
||||
It happens if a leave is correctly created (not accross multiple contracts) but
|
||||
contracts are later modifed/created in the middle of the leave.
|
||||
"""
|
||||
for holiday in self.filtered('employee_id'):
|
||||
domain = [
|
||||
('employee_id', '=', holiday.employee_id.id),
|
||||
('date_start', '<=', holiday.date_to),
|
||||
'|',
|
||||
('state', 'not in', ['draft', 'cancel']),
|
||||
'&',
|
||||
('state', '=', 'draft'),
|
||||
('kanban_state', '=', 'done'),
|
||||
'|',
|
||||
('date_end', '>=', holiday.date_from),
|
||||
'&',
|
||||
('date_end', '=', False),
|
||||
('state', '!=', 'close')
|
||||
]
|
||||
nbr_contracts = self.env['hr.contract'].sudo().search_count(domain)
|
||||
if nbr_contracts > 1:
|
||||
contracts = self.env['hr.contract'].sudo().search(domain)
|
||||
raise ValidationError(_('A leave cannot be set across multiple contracts.') + '\n' + ', '.join(contracts.mapped('name')))
|
||||
|
||||
def _cancel_work_entry_conflict(self):
|
||||
"""
|
||||
Creates a leave work entry for each hr.leave in self.
|
||||
Check overlapping work entries with self.
|
||||
Work entries completely included in a leave are archived.
|
||||
e.g.:
|
||||
|----- work entry ----|---- work entry ----|
|
||||
|------------------- hr.leave ---------------|
|
||||
||
|
||||
vv
|
||||
|----* work entry ****|
|
||||
|************ work entry leave --------------|
|
||||
"""
|
||||
if not self:
|
||||
return
|
||||
|
||||
# 1. Create a work entry for each leave
|
||||
work_entries_vals_list = []
|
||||
for leave in self:
|
||||
contracts = leave.employee_id.sudo()._get_contracts(leave.date_from, leave.date_to, states=['open', 'close'])
|
||||
for contract in contracts:
|
||||
# Generate only if it has aleady been generated
|
||||
if leave.date_to >= contract.date_generated_from and leave.date_from <= contract.date_generated_to:
|
||||
work_entries_vals_list += contracts._get_work_entries_values(leave.date_from, leave.date_to)
|
||||
|
||||
new_leave_work_entries = self.env['hr.work.entry'].create(work_entries_vals_list)
|
||||
|
||||
if new_leave_work_entries:
|
||||
# 2. Fetch overlapping work entries, grouped by employees
|
||||
start = min(self.mapped('date_from'), default=False)
|
||||
stop = max(self.mapped('date_to'), default=False)
|
||||
work_entry_groups = self.env['hr.work.entry'].read_group([
|
||||
('date_start', '<', stop),
|
||||
('date_stop', '>', start),
|
||||
('employee_id', 'in', self.employee_id.ids),
|
||||
], ['work_entry_ids:array_agg(id)', 'employee_id'], ['employee_id', 'date_start', 'date_stop'], lazy=False)
|
||||
work_entries_by_employee = defaultdict(lambda: self.env['hr.work.entry'])
|
||||
for group in work_entry_groups:
|
||||
employee_id = group.get('employee_id')[0]
|
||||
work_entries_by_employee[employee_id] |= self.env['hr.work.entry'].browse(group.get('work_entry_ids'))
|
||||
|
||||
# 3. Archive work entries included in leaves
|
||||
included = self.env['hr.work.entry']
|
||||
overlappping = self.env['hr.work.entry']
|
||||
for work_entries in work_entries_by_employee.values():
|
||||
# Work entries for this employee
|
||||
new_employee_work_entries = work_entries & new_leave_work_entries
|
||||
previous_employee_work_entries = work_entries - new_leave_work_entries
|
||||
|
||||
# Build intervals from work entries
|
||||
leave_intervals = new_employee_work_entries._to_intervals()
|
||||
conflicts_intervals = previous_employee_work_entries._to_intervals()
|
||||
|
||||
# Compute intervals completely outside any leave
|
||||
# Intervals are outside, but associated records are overlapping.
|
||||
outside_intervals = conflicts_intervals - leave_intervals
|
||||
|
||||
overlappping |= self.env['hr.work.entry']._from_intervals(outside_intervals)
|
||||
included |= previous_employee_work_entries - overlappping
|
||||
overlappping.write({'leave_id': False})
|
||||
included.write({'active': False})
|
||||
|
||||
def write(self, vals):
|
||||
if not self:
|
||||
return True
|
||||
skip_check = not bool({'employee_id', 'state', 'date_from', 'date_to'} & vals.keys())
|
||||
|
||||
start = min(self.mapped('date_from') + [fields.Datetime.from_string(vals.get('date_from', False)) or datetime.max])
|
||||
stop = max(self.mapped('date_to') + [fields.Datetime.from_string(vals.get('date_to', False)) or datetime.min])
|
||||
with self.env['hr.work.entry']._error_checking(start=start, stop=stop, skip=skip_check):
|
||||
return super().write(vals)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
start_dates = [v.get('date_from') for v in vals_list if v.get('date_from')]
|
||||
stop_dates = [v.get('date_to') for v in vals_list if v.get('date_to')]
|
||||
if any(vals.get('holiday_type', 'employee') == 'employee' and not vals.get('employee_id', False) for vals in vals_list):
|
||||
raise ValidationError(_("There is no employee set on the time off. Please make sure you're logged in the correct company."))
|
||||
with self.env['hr.work.entry']._error_checking(start=min(start_dates, default=False), stop=max(stop_dates, default=False)):
|
||||
return super().create(vals_list)
|
||||
|
||||
def action_confirm(self):
|
||||
start = min(self.mapped('date_from'), default=False)
|
||||
stop = max(self.mapped('date_to'), default=False)
|
||||
with self.env['hr.work.entry']._error_checking(start=start, stop=stop):
|
||||
return super().action_confirm()
|
||||
|
||||
def action_validate(self):
|
||||
super(HrLeave, self).action_validate()
|
||||
self.sudo()._cancel_work_entry_conflict() # delete preexisting conflicting work_entries
|
||||
return True
|
||||
|
||||
def action_refuse(self):
|
||||
"""
|
||||
Override to archive linked work entries and recreate attendance work entries
|
||||
where the refused leave was.
|
||||
"""
|
||||
res = super(HrLeave, self).action_refuse()
|
||||
work_entries = self.env['hr.work.entry'].sudo().search([('leave_id', 'in', self.ids)])
|
||||
|
||||
work_entries.write({'active': False})
|
||||
# Re-create attendance work entries
|
||||
vals_list = []
|
||||
for work_entry in work_entries:
|
||||
vals_list += work_entry.contract_id._get_work_entries_values(work_entry.date_start, work_entry.date_stop)
|
||||
self.env['hr.work.entry'].create(vals_list)
|
||||
return res
|
||||
|
||||
def _get_number_of_days(self, date_from, date_to, employee_id):
|
||||
""" If an employee is currently working full time but asks for time off next month
|
||||
where he has a new contract working only 3 days/week. This should be taken into
|
||||
account when computing the number of days for the leave (2 weeks leave = 6 days).
|
||||
Override this method to get number of days according to the contract's calendar
|
||||
at the time of the leave.
|
||||
"""
|
||||
days = super(HrLeave, self)._get_number_of_days(date_from, date_to, employee_id)
|
||||
if employee_id:
|
||||
employee = self.env['hr.employee'].browse(employee_id)
|
||||
# Use sudo otherwise base users can't compute number of days
|
||||
contracts = employee.sudo()._get_contracts(date_from, date_to, states=['open'])
|
||||
contracts |= employee.sudo()._get_incoming_contracts(date_from, date_to)
|
||||
calendar = contracts[:1].resource_calendar_id if contracts else None # Note: if len(contracts)>1, the leave creation will crash because of unicity constaint
|
||||
return employee._get_work_days_data_batch(date_from, date_to, calendar=calendar)[employee.id]
|
||||
|
||||
return days
|
||||
|
||||
def _get_calendar(self):
|
||||
self.ensure_one()
|
||||
if self.date_from and self.date_to:
|
||||
contracts = self.employee_id.sudo()._get_contracts(self.date_from, self.date_to, states=['open'])
|
||||
contracts |= self.employee_id.sudo()._get_incoming_contracts(self.date_from, self.date_to)
|
||||
contract_calendar = contracts[:1].resource_calendar_id if contracts else None
|
||||
return contract_calendar or self.employee_id.resource_calendar_id or self.env.company.resource_calendar_id
|
||||
return super()._get_calendar()
|
||||
@@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrWorkEntry(models.Model):
|
||||
_inherit = 'hr.work.entry'
|
||||
|
||||
leave_id = fields.Many2one('hr.leave', string='Time Off')
|
||||
leave_state = fields.Selection(related='leave_id.state')
|
||||
|
||||
def _get_duration(self, date_start, date_stop):
|
||||
if not date_start or not date_stop:
|
||||
return 0
|
||||
if not self.work_entry_type_id and self.leave_id:
|
||||
calendar = self.contract_id.resource_calendar_id
|
||||
employee = self.contract_id.employee_id
|
||||
contract_data = employee._get_work_days_data_batch(
|
||||
date_start, date_stop, compute_leaves=False, calendar=calendar)[employee.id]
|
||||
return contract_data.get('hours', 0)
|
||||
return super()._get_duration(date_start, date_stop)
|
||||
|
||||
def write(self, vals):
|
||||
if 'state' in vals and vals['state'] == 'cancelled':
|
||||
self.mapped('leave_id').filtered(lambda l: l.state != 'refuse').action_refuse()
|
||||
return super().write(vals)
|
||||
|
||||
def _reset_conflicting_state(self):
|
||||
super()._reset_conflicting_state()
|
||||
attendances = self.filtered(lambda w: w.work_entry_type_id and not w.work_entry_type_id.is_leave)
|
||||
attendances.write({'leave_id': False})
|
||||
|
||||
def _check_if_error(self):
|
||||
res = super()._check_if_error()
|
||||
conflict_with_leaves = self._compute_conflicts_leaves_to_approve()
|
||||
return res or conflict_with_leaves
|
||||
|
||||
def _compute_conflicts_leaves_to_approve(self):
|
||||
if not self:
|
||||
return False
|
||||
|
||||
self.flush(['date_start', 'date_stop', 'employee_id'])
|
||||
self.env['hr.leave'].flush(['date_from', 'date_to', 'state', 'employee_id'])
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
b.id AS work_entry_id,
|
||||
l.id AS leave_id
|
||||
FROM hr_work_entry b
|
||||
INNER JOIN hr_leave l ON b.employee_id = l.employee_id
|
||||
WHERE
|
||||
b.active = TRUE AND
|
||||
b.id IN %s AND
|
||||
l.date_from < b.date_stop AND
|
||||
l.date_to > b.date_start AND
|
||||
l.state IN ('confirm', 'validate1');
|
||||
"""
|
||||
self.env.cr.execute(query, [tuple(self.ids)])
|
||||
conflicts = self.env.cr.dictfetchall()
|
||||
for res in conflicts:
|
||||
self.browse(res.get('work_entry_id')).write({
|
||||
'state': 'conflict',
|
||||
'leave_id': res.get('leave_id')
|
||||
})
|
||||
return bool(conflicts)
|
||||
|
||||
def action_approve_leave(self):
|
||||
self.ensure_one()
|
||||
if self.leave_id:
|
||||
# Already confirmed once
|
||||
if self.leave_id.state == 'validate1':
|
||||
self.leave_id.action_validate()
|
||||
# Still in confirmed state
|
||||
else:
|
||||
self.leave_id.action_approve()
|
||||
# If double validation, still have to validate it again
|
||||
if self.leave_id.validation_type == 'both':
|
||||
self.leave_id.action_validate()
|
||||
|
||||
def action_refuse_leave(self):
|
||||
self.ensure_one()
|
||||
leave_sudo = self.leave_id.sudo()
|
||||
if leave_sudo:
|
||||
leave_sudo.action_refuse()
|
||||
|
||||
|
||||
class HrWorkEntryType(models.Model):
|
||||
_inherit = 'hr.work.entry.type'
|
||||
_description = 'HR Work Entry Type'
|
||||
|
||||
leave_type_ids = fields.One2many('hr.leave.type', 'work_entry_type_id', string='Time Off Type')
|
||||
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_leave
|
||||
from . import test_multi_contract
|
||||
from . import test_payslip_holidays_computation
|
||||
from . import test_performance
|
||||
from . import test_work_entry
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo.fields import Datetime
|
||||
from odoo.addons.hr_work_entry_contract.tests.common import TestWorkEntryBase
|
||||
|
||||
|
||||
class TestWorkEntryHolidaysBase(TestWorkEntryBase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestWorkEntryHolidaysBase, cls).setUpClass()
|
||||
|
||||
cls.leave_type = cls.env['hr.leave.type'].create({
|
||||
'name': 'Legal Leaves',
|
||||
'time_type': 'leave',
|
||||
'allocation_type': 'no',
|
||||
'validity_start': False,
|
||||
'work_entry_type_id': cls.work_entry_type_leave.id
|
||||
})
|
||||
|
||||
# I create a new employee "Jules"
|
||||
cls.jules_emp = cls.env['hr.employee'].create({
|
||||
'name': 'Jules',
|
||||
'gender': 'male',
|
||||
'birthday': '1984-05-01',
|
||||
'country_id': cls.env.ref('base.be').id,
|
||||
'department_id': cls.dep_rd.id,
|
||||
})
|
||||
|
||||
cls.calendar_35h = cls.env['resource.calendar'].create({
|
||||
'name': '35h calendar',
|
||||
'attendance_ids': [
|
||||
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Monday Evening', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Tuesday Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Wednesday Evening', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Thursday Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Friday Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'})
|
||||
]
|
||||
})
|
||||
cls.calendar_35h._onchange_hours_per_day() # update hours/day
|
||||
cls.calendar_40h = cls.env['resource.calendar'].create({'name': 'Default calendar'})
|
||||
|
||||
# This contract ends at the 15th of the month
|
||||
cls.contract_cdd = cls.env['hr.contract'].create({ # Fixed term contract
|
||||
'date_end': datetime.strptime('2015-11-15', '%Y-%m-%d'),
|
||||
'date_start': datetime.strptime('2015-01-01', '%Y-%m-%d'),
|
||||
'name': 'First CDD Contract for Jules',
|
||||
'resource_calendar_id': cls.calendar_40h.id,
|
||||
'wage': 5000.0,
|
||||
'employee_id': cls.jules_emp.id,
|
||||
'state': 'open',
|
||||
'kanban_state': 'blocked',
|
||||
'date_generated_from': datetime.strptime('2015-11-16', '%Y-%m-%d'),
|
||||
'date_generated_to': datetime.strptime('2015-11-16', '%Y-%m-%d'),
|
||||
})
|
||||
|
||||
# This contract starts the next day
|
||||
cls.contract_cdi = cls.env['hr.contract'].create({
|
||||
'date_start': datetime.strptime('2015-11-16', '%Y-%m-%d'),
|
||||
'name': 'Contract for Jules',
|
||||
'resource_calendar_id': cls.calendar_35h.id,
|
||||
'wage': 5000.0,
|
||||
'employee_id': cls.jules_emp.id,
|
||||
'state': 'open',
|
||||
'kanban_state': 'normal',
|
||||
'date_generated_from': datetime.strptime('2015-11-15', '%Y-%m-%d'),
|
||||
'date_generated_to': datetime.strptime('2015-11-15', '%Y-%m-%d'),
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def create_leave(cls, date_from=None, date_to=None):
|
||||
date_from = date_from or Datetime.today()
|
||||
date_to = date_to or Datetime.today() + relativedelta(days=1)
|
||||
return cls.env['hr.leave'].create({
|
||||
'name': 'Holiday !!!',
|
||||
'employee_id': cls.richard_emp.id,
|
||||
'holiday_status_id': cls.leave_type.id,
|
||||
'date_to': date_to,
|
||||
'date_from': date_from,
|
||||
'number_of_days': 1,
|
||||
})
|
||||
@@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo.addons.hr_work_entry_holidays.tests.common import TestWorkEntryHolidaysBase
|
||||
|
||||
|
||||
class TestWorkEntryLeave(TestWorkEntryHolidaysBase):
|
||||
|
||||
def test_resource_leave_has_work_entry_type(self):
|
||||
leave = self.create_leave()
|
||||
|
||||
resource_leave = leave._create_resource_leave()
|
||||
self.assertEqual(resource_leave.work_entry_type_id, self.leave_type.work_entry_type_id, "it should have the corresponding work_entry type")
|
||||
|
||||
def test_resource_leave_in_contract_calendar(self):
|
||||
other_calendar = self.env['resource.calendar'].create({'name': 'New calendar'})
|
||||
contract = self.richard_emp.contract_ids[0]
|
||||
contract.resource_calendar_id = other_calendar
|
||||
contract.state = 'open' # this set richard's calendar to New calendar
|
||||
leave = self.create_leave()
|
||||
|
||||
resource_leave = leave._create_resource_leave()
|
||||
self.assertEqual(len(resource_leave), 1, "it should have created only one resource leave")
|
||||
self.assertEqual(resource_leave.work_entry_type_id, self.leave_type.work_entry_type_id, "it should have the corresponding work_entry type")
|
||||
|
||||
def test_resource_leave_different_calendars(self):
|
||||
other_calendar = self.env['resource.calendar'].create({'name': 'New calendar'})
|
||||
contract = self.richard_emp.contract_ids[0]
|
||||
contract.resource_calendar_id = other_calendar
|
||||
contract.state = 'open' # this set richard's calendar to New calendar
|
||||
|
||||
# set another calendar
|
||||
self.richard_emp.resource_calendar_id = self.env['resource.calendar'].create({'name': 'Other calendar'})
|
||||
|
||||
leave = self.create_leave()
|
||||
resource_leave = leave._create_resource_leave()
|
||||
self.assertEqual(len(resource_leave), 2, "it should have created one resource leave per calendar")
|
||||
self.assertEqual(resource_leave.mapped('work_entry_type_id'), self.leave_type.work_entry_type_id, "they should have the corresponding work_entry type")
|
||||
|
||||
def test_create_mark_conflicting_work_entries(self):
|
||||
work_entry = self.create_work_entry(datetime(2019, 10, 10, 9, 0), datetime(2019, 10, 10, 12, 0))
|
||||
self.assertNotEqual(work_entry.state, 'conflict', "It should not be conflicting")
|
||||
leave = self.create_leave(datetime(2019, 10, 10, 9, 0), datetime(2019, 10, 10, 18, 0))
|
||||
self.assertEqual(work_entry.state, 'conflict', "It should be conflicting")
|
||||
self.assertEqual(work_entry.leave_id, leave, "It should be linked to conflicting leave")
|
||||
|
||||
def test_write_mark_conflicting_work_entries(self):
|
||||
leave = self.create_leave(datetime(2019, 10, 10, 9, 0), datetime(2019, 10, 10, 12, 0))
|
||||
work_entry = self.create_work_entry(datetime(2019, 10, 9, 9, 0), datetime(2019, 10, 10, 9, 0)) # the day before
|
||||
self.assertNotEqual(work_entry.state, 'conflict', "It should not be conflicting")
|
||||
leave.date_from = datetime(2019, 10, 9, 9, 0) # now it conflicts
|
||||
self.assertEqual(work_entry.state, 'conflict', "It should be conflicting")
|
||||
self.assertEqual(work_entry.leave_id, leave, "It should be linked to conflicting leave")
|
||||
|
||||
def test_validate_leave_with_overlap(self):
|
||||
contract = self.richard_emp.contract_ids[:1]
|
||||
contract.state = 'open'
|
||||
contract.date_generated_from = datetime(2019, 10, 10, 9, 0)
|
||||
contract.date_generated_to = datetime(2019, 10, 10, 9, 0)
|
||||
leave = self.create_leave(datetime(2019, 10, 10, 9, 0), datetime(2019, 10, 12, 18, 0))
|
||||
work_entry_1 = self.create_work_entry(datetime(2019, 10, 8, 9, 0), datetime(2019, 10, 11, 9, 0)) # overlaps
|
||||
work_entry_2 = self.create_work_entry(datetime(2019, 10, 11, 9, 0), datetime(2019, 10, 11, 10, 0)) # included
|
||||
adjacent_work_entry = self.create_work_entry(datetime(2019, 10, 12, 18, 0), datetime(2019, 10, 13, 18, 0)) # after and don't overlap
|
||||
leave.action_validate()
|
||||
self.assertNotEqual(adjacent_work_entry.state, 'conflict', "It should not conflict")
|
||||
self.assertFalse(work_entry_2.active, "It should have been archived")
|
||||
self.assertEqual(work_entry_1.state, 'conflict', "It should conflict")
|
||||
self.assertFalse(work_entry_1.leave_id, "It should not be linked to the leave")
|
||||
|
||||
leave_work_entry = self.env['hr.work.entry'].search([('leave_id', '=', leave.id)]) - work_entry_1
|
||||
self.assertTrue(leave_work_entry.work_entry_type_id.is_leave, "It should have created a leave work entry")
|
||||
self.assertEqual(leave_work_entry[:1].state, 'conflict', "The leave work entry should conflict")
|
||||
|
||||
def test_conflict_move_work_entry(self):
|
||||
leave = self.create_leave(datetime(2019, 10, 10, 9, 0), datetime(2019, 10, 12, 18, 0))
|
||||
work_entry = self.create_work_entry(datetime(2019, 10, 8, 9, 0), datetime(2019, 10, 11, 9, 0)) # overlaps
|
||||
self.assertEqual(work_entry.state, 'conflict', "It should be conflicting")
|
||||
self.assertEqual(work_entry.leave_id, leave, "It should be linked to conflicting leave")
|
||||
work_entry.date_stop = datetime(2019, 10, 9, 9, 0) # no longer overlaps
|
||||
self.assertNotEqual(work_entry.state, 'conflict', "It should not be conflicting")
|
||||
self.assertFalse(work_entry.leave_id, "It should not be linked to any leave")
|
||||
|
||||
def test_validate_leave_without_overlap(self):
|
||||
contract = self.richard_emp.contract_ids[:1]
|
||||
contract.state = 'open'
|
||||
contract.date_generated_from = datetime(2019, 10, 10, 9, 0)
|
||||
contract.date_generated_to = datetime(2019, 10, 10, 9, 0)
|
||||
leave = self.create_leave(datetime(2019, 10, 10, 9, 0), datetime(2019, 10, 12, 18, 0))
|
||||
work_entry = self.create_work_entry(datetime(2019, 10, 11, 9, 0), datetime(2019, 10, 11, 10, 0)) # included
|
||||
leave.action_validate()
|
||||
self.assertFalse(work_entry[:1].active, "It should have been archived")
|
||||
|
||||
leave_work_entry = self.env['hr.work.entry'].search([('leave_id', '=', leave.id)])
|
||||
self.assertTrue(leave_work_entry.work_entry_type_id.is_leave, "It should have created a leave work entry")
|
||||
self.assertNotEqual(leave_work_entry[:1].state, 'conflict', "The leave work entry should not conflict")
|
||||
|
||||
def test_refuse_leave(self):
|
||||
leave = self.create_leave(datetime(2019, 10, 10, 9, 0), datetime(2019, 10, 10, 18, 0))
|
||||
work_entries = self.richard_emp.contract_id._generate_work_entries(datetime(2019, 10, 10, 9, 0), datetime(2019, 10, 10, 18, 0))
|
||||
adjacent_work_entry = self.create_work_entry(datetime(2019, 10, 7, 9, 0), datetime(2019, 10, 10, 9, 0))
|
||||
self.assertTrue(all(work_entries.mapped(lambda w: w.state == 'conflict')), "Attendance work entries should all conflict with the leave")
|
||||
self.assertNotEqual(adjacent_work_entry.state, 'conflict', "Non overlapping work entry should not conflict")
|
||||
leave.action_refuse()
|
||||
self.assertTrue(all(work_entries.mapped(lambda w: w.state != 'conflict')), "Attendance work entries should no longer conflict")
|
||||
self.assertNotEqual(adjacent_work_entry.state, 'conflict', "Non overlapping work entry should not conflict")
|
||||
|
||||
def test_refuse_approved_leave(self):
|
||||
start = datetime(2019, 10, 10, 6, 0)
|
||||
end = datetime(2019, 10, 10, 18, 0)
|
||||
|
||||
# Setup contract generation state
|
||||
contract = self.richard_emp.contract_ids[:1]
|
||||
contract.state = 'open'
|
||||
contract.date_generated_from = start - relativedelta(hours=1)
|
||||
contract.date_generated_to = start - relativedelta(hours=1)
|
||||
|
||||
leave = self.create_leave(start, end)
|
||||
leave.action_validate()
|
||||
work_entries = self.env['hr.work.entry'].search([('employee_id', '=', self.richard_emp.id), ('date_start', '<=', end), ('date_stop', '>=', start)])
|
||||
leave_work_entry = self.richard_emp.contract_ids._generate_work_entries(start, end)
|
||||
self.assertEqual(leave_work_entry[:1].leave_id, leave)
|
||||
leave.action_refuse()
|
||||
work_entries = self.env['hr.work.entry'].search([('employee_id', '=', self.richard_emp.id), ('date_start', '>=', start), ('date_stop', '<=', end)])
|
||||
self.assertFalse(leave_work_entry[:1].filtered('leave_id').active)
|
||||
self.assertEqual(len(work_entries), 2, "Attendance work entries should have been re-created (morning and afternoon)")
|
||||
self.assertTrue(all(work_entries.mapped(lambda w: w.state != 'conflict')), "Attendance work entries should not conflict")
|
||||
|
||||
def test_archived_work_entry_conflict(self):
|
||||
self.create_leave(datetime(2019, 10, 10, 9, 0), datetime(2019, 10, 10, 18, 0))
|
||||
work_entry = self.create_work_entry(datetime(2019, 10, 10, 9, 0), datetime(2019, 10, 10, 18, 0))
|
||||
self.assertTrue(work_entry.active)
|
||||
self.assertEqual(work_entry.state, 'conflict', "Attendance work entries should conflict with the leave")
|
||||
work_entry.toggle_active()
|
||||
self.assertEqual(work_entry.state, 'cancelled', "Attendance work entries should be cancelled and not conflict")
|
||||
self.assertFalse(work_entry.active)
|
||||
@@ -0,0 +1,120 @@
|
||||
# # -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.hr_work_entry_holidays.tests.common import TestWorkEntryHolidaysBase
|
||||
|
||||
|
||||
class TestWorkEntryHolidaysMultiContract(TestWorkEntryHolidaysBase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.leave_type = self.env['hr.leave.type'].create({
|
||||
'name': 'Legal Leaves',
|
||||
'time_type': 'leave',
|
||||
'allocation_type': 'no',
|
||||
'validity_start': False,
|
||||
'work_entry_type_id': self.work_entry_type_leave.id
|
||||
})
|
||||
|
||||
def create_leave(self, start, end):
|
||||
work_days_data = self.jules_emp._get_work_days_data_batch(start, end)
|
||||
return self.env['hr.leave'].create({
|
||||
'name': 'Doctor Appointment',
|
||||
'employee_id': self.jules_emp.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': start,
|
||||
'date_to': end,
|
||||
'number_of_days': work_days_data[self.jules_emp.id]['days'],
|
||||
})
|
||||
|
||||
def test_multi_contract_holiday(self):
|
||||
# Leave during second contract
|
||||
leave = self.create_leave(datetime(2015, 11, 17, 7, 0), datetime(2015, 11, 20, 18, 0))
|
||||
leave.action_approve()
|
||||
start = datetime.strptime('2015-11-01', '%Y-%m-%d')
|
||||
end_generate = datetime(2015, 11, 30, 23, 59, 59)
|
||||
work_entries = self.jules_emp.contract_ids._generate_work_entries(start, end_generate)
|
||||
work_entries.action_validate()
|
||||
work_entries = work_entries.filtered(lambda we: we.contract_id == self.contract_cdi)
|
||||
|
||||
work = work_entries.filtered(lambda line: line.work_entry_type_id == self.env.ref('hr_work_entry.work_entry_type_attendance'))
|
||||
leave = work_entries.filtered(lambda line: line.work_entry_type_id == self.work_entry_type_leave)
|
||||
self.assertEqual(sum(work.mapped('duration')), 49, "It should be 49 hours of work this month for this contract")
|
||||
self.assertEqual(sum(leave.mapped('duration')), 28, "It should be 28 hours of leave this month for this contract")
|
||||
|
||||
def test_move_contract_in_leave(self):
|
||||
# test move contract dates such that a leave is accross two contracts
|
||||
start = datetime.strptime('2015-11-05 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-12-15 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.contract_cdi.write({'date_start': datetime.strptime('2015-12-30', '%Y-%m-%d').date()})
|
||||
# begins during contract, ends after contract
|
||||
leave = self.create_leave(start, end)
|
||||
leave.action_approve()
|
||||
# move contract in the middle of the leave
|
||||
with self.assertRaises(ValidationError):
|
||||
self.contract_cdi.date_start = datetime.strptime('2015-11-17', '%Y-%m-%d').date()
|
||||
|
||||
def test_create_contract_in_leave(self):
|
||||
# test create contract such that a leave is accross two contracts
|
||||
start = datetime.strptime('2015-11-05 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-12-15 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.contract_cdi.date_start = datetime.strptime('2015-12-30', '%Y-%m-%d').date() # remove this contract to be able to create the leave
|
||||
# begins during contract, ends after contract
|
||||
leave = self.create_leave(start, end)
|
||||
leave.action_approve()
|
||||
# move contract in the middle of the leave
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['hr.contract'].create({
|
||||
'date_start': datetime.strptime('2015-11-30', '%Y-%m-%d').date(),
|
||||
'name': 'Contract for Richard',
|
||||
'resource_calendar_id': self.calendar_40h.id,
|
||||
'wage': 5000.0,
|
||||
'employee_id': self.jules_emp.id,
|
||||
'state': 'open',
|
||||
'date_generated_from': datetime.strptime('2015-11-30', '%Y-%m-%d'),
|
||||
'date_generated_to': datetime.strptime('2015-11-30', '%Y-%m-%d'),
|
||||
})
|
||||
|
||||
def test_leave_outside_contract(self):
|
||||
# Leave outside contract => should not raise
|
||||
start = datetime.strptime('2014-10-18 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2014-10-20 09:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.create_leave(start, end)
|
||||
|
||||
# begins before contract, ends during contract => should not raise
|
||||
start = datetime.strptime('2014-10-25 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-01-15 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.create_leave(start, end)
|
||||
|
||||
# begins during contract, ends after contract => should not raise
|
||||
self.contract_cdi.date_end = datetime.strptime('2015-11-30', '%Y-%m-%d').date()
|
||||
start = datetime.strptime('2015-11-25 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-12-5 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.create_leave(start, end)
|
||||
|
||||
def test_no_leave_overlapping_contracts(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
# Overlap two contracts
|
||||
start = datetime.strptime('2015-11-12 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-11-17 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.create_leave(start, end)
|
||||
|
||||
# Leave inside fixed term contract => should not raise
|
||||
start = datetime.strptime('2015-11-04 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-11-07 09:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.create_leave(start, end)
|
||||
|
||||
# Leave inside contract (no end) => should not raise
|
||||
start = datetime.strptime('2015-11-18 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-11-20 09:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.create_leave(start, end)
|
||||
|
||||
def test_leave_request_next_contracts(self):
|
||||
start = datetime.strptime('2015-11-23 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-11-24 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
leave = self.create_leave(start, end)
|
||||
|
||||
leave._compute_number_of_hours_display()
|
||||
self.assertEqual(leave.number_of_hours_display, 14, "It should count hours according to the future contract.")
|
||||
@@ -0,0 +1,40 @@
|
||||
# # -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, date
|
||||
|
||||
from odoo.addons.hr_work_entry_holidays.tests.common import TestWorkEntryHolidaysBase
|
||||
|
||||
|
||||
class TestPayslipHolidaysComputation(TestWorkEntryHolidaysBase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.leave_type = self.env['hr.leave.type'].create({
|
||||
'name': 'Legal Leaves',
|
||||
'time_type': 'leave',
|
||||
'allocation_type': 'no',
|
||||
'validity_start': False,
|
||||
'work_entry_type_id': self.work_entry_type_leave.id
|
||||
})
|
||||
|
||||
def test_work_data(self):
|
||||
start = datetime(2015, 11, 8, 8, 0)
|
||||
end = datetime(2015, 11, 10, 22, 0)
|
||||
work_days_data = self.jules_emp._get_work_days_data_batch(start, end)
|
||||
leave = self.env['hr.leave'].create({
|
||||
'name': 'Doctor Appointment',
|
||||
'employee_id': self.jules_emp.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': start,
|
||||
'date_to': end,
|
||||
'number_of_days': work_days_data[self.jules_emp.id]['days'],
|
||||
})
|
||||
leave.action_approve()
|
||||
|
||||
work_entries = self.jules_emp.contract_ids._generate_work_entries(date(2015, 11, 10), date(2015, 11, 21))
|
||||
work_entries.action_validate()
|
||||
work_entries = work_entries.filtered(lambda we: we.work_entry_type_id in self.env.ref('hr_work_entry.work_entry_type_attendance'))
|
||||
sum_hours = sum(work_entries.mapped('duration'))
|
||||
self.assertEqual(sum_hours, 59, 'It should count 59 attendance hours') # 24h first contract + 35h second contract
|
||||
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import date, datetime
|
||||
|
||||
from odoo.addons.hr_work_entry_holidays.tests.common import TestWorkEntryHolidaysBase
|
||||
from odoo.tests.common import users, warmup, tagged
|
||||
|
||||
|
||||
class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestWorkEntryHolidaysPerformance, cls).setUpClass()
|
||||
cls.jack = cls.env['hr.employee'].create({'name': 'Jack'})
|
||||
cls.employees = cls.richard_emp | cls.jack
|
||||
|
||||
cls.env['hr.contract'].create([{
|
||||
'date_start': date(2018, 1, 1),
|
||||
'date_end': date(2018, 2, 1),
|
||||
'name': 'Contract for %s' % employee.name,
|
||||
'wage': 5000.0,
|
||||
'state': 'open',
|
||||
'employee_id': employee.id,
|
||||
'date_generated_from': datetime(2018, 1, 1, 0, 0),
|
||||
'date_generated_to': datetime(2018, 1, 1, 0, 0),
|
||||
} for employee in cls.employees])
|
||||
|
||||
@users('__system__', 'admin')
|
||||
@warmup
|
||||
def test_performance_leave_validate(self):
|
||||
self.richard_emp.generate_work_entries(date(2018, 1, 1), date(2018, 1, 2))
|
||||
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
|
||||
|
||||
with self.assertQueryCount(__system__=90, admin=99):
|
||||
leave.action_validate()
|
||||
leave.action_refuse()
|
||||
|
||||
@users('__system__', 'admin')
|
||||
@warmup
|
||||
def test_performance_leave_write(self):
|
||||
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
|
||||
|
||||
with self.assertQueryCount(__system__=24, admin=24):
|
||||
leave.date_to = datetime(2018, 1, 1, 19, 0)
|
||||
leave.action_refuse()
|
||||
|
||||
@users('__system__', 'admin')
|
||||
@warmup
|
||||
def test_performance_leave_create(self):
|
||||
with self.assertQueryCount(__system__=28, admin=59):
|
||||
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
|
||||
leave.action_refuse()
|
||||
|
||||
@users('__system__', 'admin')
|
||||
@warmup
|
||||
def test_performance_leave_confirm(self):
|
||||
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
|
||||
leave.action_draft()
|
||||
with self.assertQueryCount(__system__=22, admin=55):
|
||||
leave.action_confirm()
|
||||
leave.state = 'cancel'
|
||||
|
||||
|
||||
@tagged('work_entry_perf')
|
||||
class TestWorkEntryHolidaysPerformancesBigData(TestWorkEntryHolidaysBase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestWorkEntryHolidaysPerformancesBigData, cls).setUpClass()
|
||||
cls.company = cls.env['res.company'].create({'name': 'A company'})
|
||||
|
||||
cls.paid_time_off = cls.env['hr.leave.type'].create({
|
||||
'name': 'Paid Time Off',
|
||||
'request_unit': 'day',
|
||||
'leave_validation_type': 'both',
|
||||
'company_id': cls.company.id,
|
||||
})
|
||||
|
||||
cls.employees = cls.env['hr.employee'].create([{
|
||||
'name': 'Employee %s' % i,
|
||||
'company_id': cls.company.id
|
||||
} for i in range(100)])
|
||||
|
||||
cls.contracts = cls.env['hr.contract'].create([{
|
||||
'date_start': date(2018, 1, 1),
|
||||
'date_end': False,
|
||||
'name': 'Contract for %s' % employee.name,
|
||||
'wage': 5000.0,
|
||||
'state': 'open',
|
||||
'employee_id': employee.id,
|
||||
'date_generated_from': datetime(2018, 1, 1, 0, 0),
|
||||
'date_generated_to': datetime(2018, 1, 1, 0, 0),
|
||||
} for employee in cls.employees])
|
||||
|
||||
cls.leaves = cls.env['hr.leave'].create([{
|
||||
'name': 'Holiday - %s' % employee.name,
|
||||
'employee_id': employee.id,
|
||||
'holiday_status_id': cls.paid_time_off.id,
|
||||
'request_date_from': date(2020, 8, 3),
|
||||
'request_date_to': date(2020, 8, 7),
|
||||
'number_of_days': 5,
|
||||
} for employee in cls.employees])
|
||||
cls.leaves._compute_date_from_to()
|
||||
cls.leaves.action_approve()
|
||||
cls.leaves.action_validate()
|
||||
|
||||
|
||||
def test_work_entries_generation_perf(self):
|
||||
# Test Case 7: Try to generate work entries for
|
||||
# a hundred employees over a month
|
||||
with self.assertQueryCount(__system__=11522, admin=11522):
|
||||
work_entries = self.contracts._generate_work_entries(date(2020, 7, 1), date(2020, 8, 31))
|
||||
|
||||
# Original work entries to generate when we don't adapt date_generated_from and
|
||||
# date_generated_to when they are equal for old contracts: 138300
|
||||
self.assertEqual(len(work_entries), 8800)
|
||||
@@ -0,0 +1,192 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import pytz
|
||||
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.fields import Date, Datetime
|
||||
from odoo.addons.hr_work_entry_holidays.tests.common import TestWorkEntryHolidaysBase
|
||||
|
||||
|
||||
@tagged('work_entry')
|
||||
class TestWorkeEntryHolidaysWorkEntry(TestWorkEntryHolidaysBase):
|
||||
def setUp(self):
|
||||
super(TestWorkeEntryHolidaysWorkEntry, self).setUp()
|
||||
self.tz = pytz.timezone(self.richard_emp.tz)
|
||||
self.start = datetime(2015, 11, 1, 1, 0, 0)
|
||||
self.end = datetime(2015, 11, 30, 23, 59, 59)
|
||||
self.resource_calendar_id = self.env['resource.calendar'].create({'name': 'Zboub'})
|
||||
contract = self.env['hr.contract'].create({
|
||||
'date_start': self.start.date() - relativedelta(days=5),
|
||||
'name': 'dodo',
|
||||
'resource_calendar_id': self.resource_calendar_id.id,
|
||||
'wage': 1000,
|
||||
'employee_id': self.richard_emp.id,
|
||||
'state': 'open',
|
||||
'date_generated_from': self.end.date() + relativedelta(days=5),
|
||||
})
|
||||
self.richard_emp.resource_calendar_id = self.resource_calendar_id
|
||||
self.richard_emp.contract_id = contract
|
||||
|
||||
def test_validate_non_approved_leave_work_entry(self):
|
||||
work_entry1 = self.env['hr.work.entry'].create({
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'work_entry_type_id': self.work_entry_type_leave.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': self.start,
|
||||
'date_stop': self.end,
|
||||
})
|
||||
self.env['hr.leave'].create({
|
||||
'name': 'Doctor Appointment',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': self.start - relativedelta(days=1),
|
||||
'date_to': self.start + relativedelta(days=1),
|
||||
'number_of_days': 2,
|
||||
})
|
||||
self.assertFalse(work_entry1.action_validate(), "It should not validate work_entries conflicting with non approved leaves")
|
||||
self.assertEqual(work_entry1.state, 'conflict')
|
||||
|
||||
def test_refuse_leave_work_entry(self):
|
||||
start = datetime(2015, 11, 1, 9, 0, 0)
|
||||
end = datetime(2015, 11, 3, 13, 0, 0)
|
||||
leave = self.env['hr.leave'].create({
|
||||
'name': 'Doctor Appointment',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': start,
|
||||
'date_to': start + relativedelta(days=1),
|
||||
'number_of_days': 2,
|
||||
})
|
||||
work_entry = self.env['hr.work.entry'].create({
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'work_entry_type_id': self.work_entry_type.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
'leave_id': leave.id
|
||||
})
|
||||
work_entry.action_validate()
|
||||
self.assertEqual(work_entry.state, 'conflict', "It should have an error (conflicting leave to approve")
|
||||
leave.action_refuse()
|
||||
self.assertNotEqual(work_entry.state, 'conflict', "It should not have an error")
|
||||
|
||||
def test_time_week_leave_work_entry(self):
|
||||
# /!\ this is a week day => it exists an calendar attendance at this time
|
||||
start = datetime(2015, 11, 2, 10, 0, 0)
|
||||
end = datetime(2015, 11, 2, 17, 0, 0)
|
||||
work_days_data = self.jules_emp._get_work_days_data_batch(start, end)
|
||||
leave = self.env['hr.leave'].create({
|
||||
'name': '1leave',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': start,
|
||||
'date_to': end,
|
||||
'number_of_days': work_days_data[self.jules_emp.id]['days'],
|
||||
})
|
||||
leave.action_validate()
|
||||
|
||||
work_entries = self.richard_emp.contract_id._generate_work_entries(self.start, self.end)
|
||||
work_entries.action_validate()
|
||||
leave_work_entry = work_entries.filtered(lambda we: we.work_entry_type_id in self.work_entry_type_leave)
|
||||
sum_hours = sum(leave_work_entry.mapped('duration'))
|
||||
|
||||
self.assertEqual(sum_hours, 5.0, "It should equal the number of hours richard should have worked")
|
||||
|
||||
def test_contract_on_another_company(self):
|
||||
""" Test that the work entry generation still work if
|
||||
the contract is not on the same company than
|
||||
the employee (Internal Use Case)
|
||||
So when generating the work entries in Belgium,
|
||||
there is an issue when accessing to the time off
|
||||
in Hong Kong.
|
||||
"""
|
||||
company = self.env['res.company'].create({'name': 'Another Company'})
|
||||
|
||||
employee = self.env['hr.employee'].create({
|
||||
'name': 'New Employee',
|
||||
'company_id': company.id,
|
||||
})
|
||||
|
||||
self.env['hr.contract'].create({
|
||||
'name': 'Employee Contract',
|
||||
'employee_id': employee.id,
|
||||
'date_start': Date.from_string('2015-01-01'),
|
||||
'state': 'open',
|
||||
'company_id': self.env.ref('base.main_company').id,
|
||||
'wage': 4000,
|
||||
})
|
||||
|
||||
leave_type = self.env['hr.leave.type'].create({
|
||||
'name': 'Sick',
|
||||
'request_unit': 'hour',
|
||||
'leave_validation_type': 'both',
|
||||
'allocation_type': 'no',
|
||||
'company_id': company.id,
|
||||
})
|
||||
leave1 = self.env['hr.leave'].create({
|
||||
'name': 'Sick 1 week during christmas snif',
|
||||
'employee_id': employee.id,
|
||||
'holiday_status_id': leave_type.id,
|
||||
'date_from': Datetime.from_string('2019-12-23 06:00:00'),
|
||||
'date_to': Datetime.from_string('2019-12-27 20:00:00'),
|
||||
'number_of_days': 5,
|
||||
})
|
||||
leave1.action_approve()
|
||||
leave1.action_validate()
|
||||
|
||||
# The work entries generation shouldn't raise an error
|
||||
|
||||
user = self.env['res.users'].create({
|
||||
'name': 'Classic User',
|
||||
'login': 'Classic User',
|
||||
'company_id': self.env.ref('base.main_company').id,
|
||||
'company_ids': self.env.ref('base.main_company').ids,
|
||||
'groups_id': [(6, 0, [self.env.ref('hr_contract.group_hr_contract_manager').id, self.env.ref('base.group_user').id])],
|
||||
})
|
||||
self.env['hr.employee'].with_user(user).generate_work_entries('2019-12-01', '2019-12-31')
|
||||
|
||||
def test_work_entries_generation_if_parent_leave_zero_hours(self):
|
||||
# Test case: The employee has a parental leave at 0 hours per week
|
||||
# The employee has a leave during that period
|
||||
|
||||
employee = self.env['hr.employee'].create({'name': 'My employee'})
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'Parental 0h',
|
||||
'attendance_ids': False,
|
||||
})
|
||||
employee.resource_calendar_id = calendar
|
||||
contract = self.env['hr.contract'].create({
|
||||
'date_start': self.start.date() - relativedelta(years=1),
|
||||
'name': 'Contract - Parental 0h',
|
||||
'resource_calendar_id': calendar.id,
|
||||
'wage': 1000,
|
||||
'employee_id': employee.id,
|
||||
'state': 'open',
|
||||
})
|
||||
|
||||
leave_type = self.env['hr.leave.type'].create({
|
||||
'name': 'Sick',
|
||||
'request_unit': 'hour',
|
||||
'leave_validation_type': 'both',
|
||||
'allocation_type': 'no',
|
||||
})
|
||||
|
||||
leave = self.env['hr.leave'].create({
|
||||
'name': "Sick 1 that doesn't make sense, but it's the prod so YOLO",
|
||||
'employee_id': employee.id,
|
||||
'holiday_status_id': leave_type.id,
|
||||
'request_date_from': date(2020, 9, 4),
|
||||
'request_date_to': date(2020, 9, 4),
|
||||
'number_of_days': 1,
|
||||
})
|
||||
leave.action_approve()
|
||||
leave.action_validate()
|
||||
|
||||
work_entries = contract._generate_work_entries(date(2020, 7, 1), date(2020, 9, 30))
|
||||
|
||||
self.assertEqual(len(work_entries), 0)
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version='1.0' encoding='UTF-8' ?>
|
||||
<odoo>
|
||||
<!-- Work entry type on hr leave -->
|
||||
<record id="work_entry_type_leave_form_inherit" model="ir.ui.view">
|
||||
<field name="name">work_entry.type.leave.form.inherit</field>
|
||||
<field name="model">hr.leave.type</field>
|
||||
<field name="inherit_id" ref="hr_holidays.edit_holiday_status_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='calendar']" position="after">
|
||||
<group name="work_entry" string="Work Entries">
|
||||
<field name="work_entry_type_id"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_leave_view_tree_inherit_work_entry" model="ir.ui.view">
|
||||
<field name="name">hr.holidays.view.tree.inherit.work.entry</field>
|
||||
<field name="model">hr.leave</field>
|
||||
<field name="inherit_id" ref="hr_holidays.hr_leave_view_tree"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//tree" position="attributes">
|
||||
<attribute name="default_order">date_from</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_leave_view_search_manager_inherit_work_entry" model="ir.ui.view">
|
||||
<field name="name">hr.holidays.view.search.manager.work.entry</field>
|
||||
<field name="model">hr.leave</field>
|
||||
<field name="inherit_id" ref="hr_holidays.hr_leave_view_search_manager"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='gray']" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user