[MOV] hr_work_entry_holidays: move to community.

Before this commit, the module dependencies: hr_holidays and hr_work_entry were already defined in community.
As they could be integrated with some community applications, like hr_attendance or hr_holidays, it is more coherent to move them to community.

Taskid: 2222790
This commit is contained in:
Arnaud Joset
2021-02-26 08:42:24 +01:00
parent c6998b3b14
commit baa4dde1bb
18 changed files with 1427 additions and 0 deletions
+11
View File
@@ -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,28 @@
# -*- 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',
'views/hr_work_entry_views.xml',
'data/hr_payroll_holidays_data.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 requests a leave 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 api, 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,88 @@
# -*- 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'),
})
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(cls):
super(TestWorkEntryHolidaysMultiContract, cls).setUp()
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
})
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(cls):
super(TestPayslipHolidaysComputation, cls).setUp()
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
})
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,118 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
from odoo import fields
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,
})
contract = 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,68 @@
<?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>
<record id="hr_leave_work_entry_action" model="ir.actions.act_window">
<field name="name">All Time Off</field>
<field name="res_model">hr.leave</field>
<field name="view_mode">tree,kanban,form,calendar,activity,gantt</field>
<field name="search_view_id" ref="hr_leave_view_search_manager_inherit_work_entry"/>
<field name="view_id" ref="hr_leave_view_tree_inherit_work_entry"/>
<field name="context">{
'search_default_gray':1,
'hide_employee_name': 1
}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Meet the time off dashboard.
</p><p>
A great way to keep track on employee’s PTOs, sick days, and approval status.
</p>
</field>
</record>
<menuitem
id="menu_work_entry_leave_to_approve"
name="Time Off to Report"
action="hr_leave_work_entry_action"
parent="hr_work_entry_contract.menu_hr_payroll_work_entries_root"
sequence="75"
groups="hr_holidays.group_hr_holidays_user"/>
</odoo>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payroll_hr_work_entry_view_form_inherit" model="ir.ui.view">
<field name="name">payroll.hr.work.entry.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="//field[@name='state']" position="before">
<field name="leave_state" invisible="1"/>
<button string="Refuse Time Off" name="action_refuse_leave" type="object" attrs="{'invisible': ['|', ('state', '!=', 'conflict'), ('leave_id', '=', False)]}"/>
<button string="Approve Time Off" name="action_approve_leave" type="object" attrs="{'invisible': ['|', '|', ('state', '!=', 'conflict'), ('leave_id', '=', False), ('leave_state', '=', 'validate')]}"/>
</xpath>
<xpath expr="//field[@name='work_entry_type_id']" position="after">
<field name="leave_id" attrs="{'invisible': [('leave_id', '=', False)]}"/>
<field name="contract_id" invisible="1"/>
</xpath>
</field>
</record>
<record id="payroll_hr_work_entry_view_form_inherit_contract" model="ir.ui.view">
<field name="name">payroll.hr.work.entry.view.form.inherit.contract</field>
<field name="model">hr.work.entry</field>
<field name="inherit_id" ref="hr_work_entry_contract.hr_work_entry_contract_view_form_inherit"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='work_entry_undefined']" position="after">
<div attrs="{'invisible': [('work_entry_type_id', '=', False)]}">
<div class="alert alert-warning mb-0" role="alert" attrs="{'invisible': [('leave_id', '=', False)]}">
This work entry cannot be validated. There is a leave to approve (or refuse) at the same time.
</div>
<div class="alert alert-warning mb-0" role="alert" attrs="{'invisible': ['!', ('leave_id', '=', False)]}">
This work entry cannot be validated. It is conflicting with at least one work entry. <br/>
Two work entries of the same employee cannot overlap at the same time.
</div>
</div>
</xpath>
</field>
</record>
<record id="payroll_leave_hr_work_entry_type_view_form_inherit" model="ir.ui.view">
<field name="name">payroll.leave.hr.work.entry.type.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="//field[@name='sequence']" position="after">
<field name="leave_type_ids" widget="many2many_tags" attrs="{'invisible': [('is_leave', '=', False)]}"/>
</xpath>
</field>
</record>
</odoo>