mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Merge branch 'mig/12.0/hr_holidays_accrual' into 12.0
This commit is contained in:
29
hr_holidays_accrual/README.rst
Normal file
29
hr_holidays_accrual/README.rst
Normal file
@@ -0,0 +1,29 @@
|
||||
***************************
|
||||
Hibou - HR Holidays Accrual
|
||||
***************************
|
||||
|
||||
**This Module is now Deprecated in 12.0**
|
||||
|
||||
If it comes back, it would be a new module.
|
||||
|
||||
|
||||
**Old**
|
||||
|
||||
* Adds new boolean field `grant_by_tag` to Leave Allocations.
|
||||
* Allows user to create a Leave Allocation by employee tag, then use that tag to grant the newly created leave to as many employees as desired.
|
||||
|
||||
.. image:: https://user-images.githubusercontent.com/15882954/42062226-cf3ce23e-7ae1-11e8-96dc-43268c7b904c.png
|
||||
:alt: 'Equipment Detail'
|
||||
:width: 988
|
||||
:align: left
|
||||
|
||||
|
||||
|
||||
|
||||
=======
|
||||
License
|
||||
=======
|
||||
|
||||
Please see `LICENSE <https://github.com/hibou-io/hibou-odoo-suite/blob/11.0/LICENSE>`_.
|
||||
|
||||
Copyright Hibou Corp. 2019
|
||||
1
hr_holidays_accrual/__init__.py
Executable file
1
hr_holidays_accrual/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
18
hr_holidays_accrual/__manifest__.py
Executable file
18
hr_holidays_accrual/__manifest__.py
Executable file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
'name': 'HR Holidays Accrual',
|
||||
'author': 'Hibou Corp. <hello@hibou.io>',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Human Resources',
|
||||
'sequence': 95,
|
||||
'summary': 'Grant leave allocations with tags',
|
||||
'description': """
|
||||
Create leave allocations by tag, then use tags to grant leaves to employees.
|
||||
""",
|
||||
'website': 'https://hibou.io/',
|
||||
'depends': ['hr_holidays'],
|
||||
'data': [
|
||||
'views/hr_holidays_views.xml',
|
||||
],
|
||||
'installable': False,
|
||||
'application': False,
|
||||
}
|
||||
1
hr_holidays_accrual/models/__init__.py
Normal file
1
hr_holidays_accrual/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import hr_holidays
|
||||
54
hr_holidays_accrual/models/hr_holidays.py
Normal file
54
hr_holidays_accrual/models/hr_holidays.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HRHolidays(models.Model):
|
||||
_inherit = 'hr.leave.type'
|
||||
|
||||
grant_by_tag = fields.Boolean(string="Grant by Tag")
|
||||
|
||||
def _accrue_for_employee_values(self, employee):
|
||||
return {
|
||||
'holiday_status_id': self.holiday_status_id.id,
|
||||
'number_of_days_temp': self.number_of_days_temp,
|
||||
'holiday_type': 'employee',
|
||||
'employee_id': employee.id,
|
||||
'type': 'add',
|
||||
'state': 'confirm',
|
||||
'double_validation': self.double_validation,
|
||||
'grant_by_tag': self.grant_by_tag,
|
||||
}
|
||||
|
||||
def accrue_for_employee(self, employee):
|
||||
holidays = self.env['hr.leave'].sudo()
|
||||
for leave_to_create in self:
|
||||
values = leave_to_create._accrue_for_employee_values(employee)
|
||||
if values:
|
||||
leave = holidays.create(values)
|
||||
leave.action_approve()
|
||||
|
||||
|
||||
class HREmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
holidays = self.env['hr.leave'].sudo()
|
||||
for emp in self:
|
||||
if values.get('category_ids'):
|
||||
categ_ids_command_list = values.get('category_ids')
|
||||
for categ_ids_command in categ_ids_command_list:
|
||||
ids = None
|
||||
if categ_ids_command[0] == 6:
|
||||
ids = set(categ_ids_command[2])
|
||||
ids -= set(emp.category_ids.ids)
|
||||
if categ_ids_command[0] == 4:
|
||||
id = categ_ids_command[1]
|
||||
if id not in emp.category_ids.ids:
|
||||
ids = [id]
|
||||
if ids:
|
||||
# new category ids
|
||||
leaves = holidays.search([('category_id', 'in', list(ids)),
|
||||
('grant_by_tag', '=', True)])
|
||||
leaves.accrue_for_employee(emp)
|
||||
|
||||
return super(HREmployee, self).write(values)
|
||||
1
hr_holidays_accrual/tests/__init__.py
Executable file
1
hr_holidays_accrual/tests/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
from . import test_leaves
|
||||
43
hr_holidays_accrual/tests/test_leaves.py
Normal file
43
hr_holidays_accrual/tests/test_leaves.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase
|
||||
|
||||
|
||||
class TestLeaves(TestHrHolidaysBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestLeaves, self).setUp()
|
||||
|
||||
self.categ = self.env['hr.employee.category'].create({'name': 'Test Category'})
|
||||
department = self.env['hr.department'].create({'name': 'Test Department'})
|
||||
self.employee = self.env['hr.employee'].create({'name': 'Mark', 'department_id': department.id})
|
||||
self.leave_type = self.env['hr.leave.type'].create({
|
||||
'name': 'Test Status',
|
||||
'color_name': 'red',
|
||||
})
|
||||
self.test_leave = self.env['hr.leave'].create({
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'number_of_days_temp': 5,
|
||||
'holiday_type': 'category',
|
||||
'category_id': self.categ.id,
|
||||
'type': 'add',
|
||||
'state': 'draft',
|
||||
'grant_by_tag': True,
|
||||
})
|
||||
|
||||
def test_tag_assignment(self):
|
||||
self.test_leave.action_confirm()
|
||||
self.test_leave.action_approve()
|
||||
self.assertEqual(self.employee.leaves_count, 0.0)
|
||||
self.employee.write({'category_ids': [(6, False, [self.categ.id])]})
|
||||
self.assertEqual(self.employee.leaves_count, 5.0)
|
||||
leave = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)])
|
||||
self.assertEqual(leave.holiday_status_id.id, self.leave_type.id)
|
||||
|
||||
def test_double_validation(self):
|
||||
self.test_leave.write({'double_validation': True})
|
||||
self.test_leave.action_confirm()
|
||||
self.test_leave.action_approve()
|
||||
self.test_leave.action_validate()
|
||||
self.employee.write({'category_ids': [(6, False, [self.categ.id])]})
|
||||
leave = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)])
|
||||
self.assertEqual(leave.state, 'validate1')
|
||||
self.assertEqual(leave.first_approver_id.id, self.env.uid)
|
||||
15
hr_holidays_accrual/views/hr_holidays_views.xml
Normal file
15
hr_holidays_accrual/views/hr_holidays_views.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="hr_holidays_edit_holiday_new_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.holidays.edit.holiday.new.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/group[2]" position="after">
|
||||
<group name="accrue" attrs="{'invisible': [('type', '!=', 'add')]}">
|
||||
<field name="grant_by_tag" attrs="{'invisible': [('holiday_type', '=', 'employee')]}"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
29
hr_holidays_accrual_payroll/README.rst
Normal file
29
hr_holidays_accrual_payroll/README.rst
Normal file
@@ -0,0 +1,29 @@
|
||||
*************************************
|
||||
Hibou - HR Holidays Accrual - Payroll
|
||||
*************************************
|
||||
|
||||
Accrue employee leave allocations every pay period.
|
||||
|
||||
For more information and add-ons, visit `Hibou.io <https://hibou.io/>`_.
|
||||
|
||||
=============
|
||||
Main Features
|
||||
=============
|
||||
|
||||
*New in 12.0*
|
||||
|
||||
Odoo implemented their own accruals, which we now extend. The old base module `hr_holidays_accrual` is now deprecated.
|
||||
|
||||
* Adds 'Payslip' to the interval dropdown
|
||||
* When Payslip for employee is confirmed, the accrual will be made.
|
||||
* Additionally exposes the `accrual_limit` field "Balance limit" (Why Odoo doesn't...)
|
||||
|
||||
|
||||
|
||||
=======
|
||||
License
|
||||
=======
|
||||
|
||||
Please see `LICENSE <https://github.com/hibou-io/hibou-odoo-suite/blob/11.0/LICENSE>`_.
|
||||
|
||||
Copyright Hibou Corp. 2019
|
||||
1
hr_holidays_accrual_payroll/__init__.py
Executable file
1
hr_holidays_accrual_payroll/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
21
hr_holidays_accrual_payroll/__manifest__.py
Executable file
21
hr_holidays_accrual_payroll/__manifest__.py
Executable file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
'name': 'HR Holidays Accrual - Payroll',
|
||||
'author': 'Hibou Corp. <hello@hibou.io>',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Human Resources',
|
||||
'sequence': 95,
|
||||
'summary': 'Grant leave allocations per payperiod',
|
||||
'description': """
|
||||
Automates leave allocations.
|
||||
""",
|
||||
'website': 'https://hibou.io/',
|
||||
'depends': [
|
||||
'hr_holidays',
|
||||
'hr_payroll'
|
||||
],
|
||||
'data': [
|
||||
'views/hr_holidays_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
2
hr_holidays_accrual_payroll/models/__init__.py
Normal file
2
hr_holidays_accrual_payroll/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import hr_leave_allocation
|
||||
from . import hr_payslip
|
||||
98
hr_holidays_accrual_payroll/models/hr_leave_allocation.py
Normal file
98
hr_holidays_accrual_payroll/models/hr_leave_allocation.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from datetime import datetime, time
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.resource.models.resource import HOURS_PER_DAY
|
||||
|
||||
|
||||
class HRLeaveAllocation(models.Model):
|
||||
_inherit = 'hr.leave.allocation'
|
||||
|
||||
interval_unit = fields.Selection(selection_add=[('payslip', 'Payslip')])
|
||||
|
||||
@api.model
|
||||
def payslip_update_accrual(self, payslips):
|
||||
employees = payslips.mapped('employee_id')
|
||||
holidays = self.env['hr.leave.allocation'].search([
|
||||
('employee_id', 'in', employees.ids),
|
||||
('accrual', '=', True),
|
||||
('state', '=', 'validate'),
|
||||
('holiday_type', '=', 'employee'),
|
||||
'|', ('date_to', '=', False),
|
||||
('date_to', '>', fields.Datetime.now()),
|
||||
])
|
||||
for holiday in holidays:
|
||||
days_to_give = holiday.number_per_interval
|
||||
if holiday.unit_per_interval == 'hours':
|
||||
# As we encode everything in days in the database we need to convert
|
||||
# the number of hours into days for this we use the
|
||||
# mean number of hours set on the employee's calendar
|
||||
days_to_give = days_to_give / (holiday.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
|
||||
|
||||
new_number_of_days = holiday.number_of_days + days_to_give
|
||||
if holiday.accrual_limit > 0:
|
||||
new_number_of_days = min(new_number_of_days, holiday.accrual_limit)
|
||||
holiday.number_of_days = new_number_of_days
|
||||
|
||||
|
||||
|
||||
|
||||
# This is a 'patch' because I cannot influence the domain/search here to filter out the ones by payslip.
|
||||
@api.model
|
||||
def _update_accrual(self):
|
||||
"""
|
||||
Method called by the cron task in order to increment the number_of_days when
|
||||
necessary.
|
||||
"""
|
||||
today = fields.Date.from_string(fields.Date.today())
|
||||
|
||||
holidays = self.search([('interval_unit', '!=', 'payslip'), # new domain filter
|
||||
('accrual', '=', True), ('state', '=', 'validate'), ('holiday_type', '=', 'employee'),
|
||||
'|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()),
|
||||
'|', ('nextcall', '=', False), ('nextcall', '<=', today)])
|
||||
|
||||
for holiday in holidays:
|
||||
values = {}
|
||||
|
||||
delta = relativedelta(days=0)
|
||||
|
||||
if holiday.interval_unit == 'weeks':
|
||||
delta = relativedelta(weeks=holiday.interval_number)
|
||||
if holiday.interval_unit == 'months':
|
||||
delta = relativedelta(months=holiday.interval_number)
|
||||
if holiday.interval_unit == 'years':
|
||||
delta = relativedelta(years=holiday.interval_number)
|
||||
|
||||
values['nextcall'] = (holiday.nextcall if holiday.nextcall else today) + delta
|
||||
|
||||
period_start = datetime.combine(today, time(0, 0, 0)) - delta
|
||||
period_end = datetime.combine(today, time(0, 0, 0))
|
||||
|
||||
# We have to check when the employee has been created
|
||||
# in order to not allocate him/her too much leaves
|
||||
start_date = holiday.employee_id._get_date_start_work()
|
||||
# If employee is created after the period, we cancel the computation
|
||||
if period_end <= start_date:
|
||||
holiday.write(values)
|
||||
continue
|
||||
|
||||
# If employee created during the period, taking the date at which he has been created
|
||||
if period_start <= start_date:
|
||||
period_start = start_date
|
||||
|
||||
worked = holiday.employee_id.get_work_days_data(period_start, period_end, domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')])['days']
|
||||
left = holiday.employee_id.get_leave_days_data(period_start, period_end, domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')])['days']
|
||||
prorata = worked / (left + worked) if worked else 0
|
||||
|
||||
days_to_give = holiday.number_per_interval
|
||||
if holiday.unit_per_interval == 'hours':
|
||||
# As we encode everything in days in the database we need to convert
|
||||
# the number of hours into days for this we use the
|
||||
# mean number of hours set on the employee's calendar
|
||||
days_to_give = days_to_give / (holiday.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
|
||||
|
||||
values['number_of_days'] = holiday.number_of_days + days_to_give * prorata
|
||||
if holiday.accrual_limit > 0:
|
||||
values['number_of_days'] = min(values['number_of_days'], holiday.accrual_limit)
|
||||
|
||||
holiday.write(values)
|
||||
12
hr_holidays_accrual_payroll/models/hr_payslip.py
Normal file
12
hr_holidays_accrual_payroll/models/hr_payslip.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HRPayslip(models.Model):
|
||||
_inherit = 'hr.payslip'
|
||||
|
||||
@api.multi
|
||||
def action_payslip_done(self):
|
||||
res = super(HRPayslip, self).action_payslip_done()
|
||||
if res and isinstance(res, (int, bool)):
|
||||
self.env['hr.leave.allocation'].payslip_update_accrual(self)
|
||||
return res
|
||||
1
hr_holidays_accrual_payroll/tests/__init__.py
Executable file
1
hr_holidays_accrual_payroll/tests/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
from . import test_accrual
|
||||
45
hr_holidays_accrual_payroll/tests/test_accrual.py
Normal file
45
hr_holidays_accrual_payroll/tests/test_accrual.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase
|
||||
|
||||
|
||||
class TestLeaves(TestHrHolidaysBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestLeaves, self).setUp()
|
||||
|
||||
self.categ = self.env['hr.employee.category'].create({'name': 'Test Category'})
|
||||
department = self.env['hr.department'].create({'name': 'Test Department'})
|
||||
self.employee = self.env['hr.employee'].create({'name': 'Mark', 'department_id': department.id})
|
||||
self.leave_type = self.env['hr.leave.type'].create({
|
||||
'name': 'Test Status',
|
||||
'color_name': 'red',
|
||||
})
|
||||
self.allocation = self.env['hr.leave.allocation'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'number_of_days': 0.0,
|
||||
'state': 'validate',
|
||||
'accrual': True,
|
||||
'holiday_type': 'employee',
|
||||
'number_per_interval': 0.75,
|
||||
'unit_per_interval': 'days',
|
||||
'interval_unit': 'payslip',
|
||||
'accrual_limit': 1,
|
||||
})
|
||||
|
||||
def test_payslip_accrual(self):
|
||||
payslip = self.env['hr.payslip'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'date_from': '2018-01-01',
|
||||
'date_to': '2018-01-31'
|
||||
})
|
||||
payslip.action_payslip_done()
|
||||
self.assertEqual(self.allocation.number_of_days, 0.75)
|
||||
|
||||
# Should be capped at 1 day
|
||||
payslip = self.env['hr.payslip'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'date_from': '2018-02-01',
|
||||
'date_to': '2018-02-28'
|
||||
})
|
||||
payslip.action_payslip_done()
|
||||
self.assertEqual(self.allocation.number_of_days, 1.0)
|
||||
22
hr_holidays_accrual_payroll/views/hr_holidays_views.xml
Normal file
22
hr_holidays_accrual_payroll/views/hr_holidays_views.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="hr_leave_allocation_view_form_manager_payslip" model="ir.ui.view">
|
||||
<field name="name">hr.leave.allocation.view.form.manager.payslip</field>
|
||||
<field name="model">hr.leave.allocation</field>
|
||||
<field name="inherit_id" ref="hr_holidays.hr_leave_allocation_view_form_manager"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='interval_number']" position="attributes">
|
||||
<attribute name="attrs">{'required': [('accrual', '=', True)], 'invisible': [('interval_unit', '=', 'payslip')]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='accrual']" position="after">
|
||||
<label for="accrual_limit" attrs="{'invisible': [('accrual', '=', False)]}"/>
|
||||
<div>
|
||||
<field name="accrual_limit" class="oe_inline" nolabel="1" attrs="{'invisible': [('accrual', '=', False)]}"/>
|
||||
<span class="ml8" attrs="{'invisible': [('accrual', '=', False)]}">Days</span>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user