diff --git a/hr_holidays_accrual/README.rst b/hr_holidays_accrual/README.rst new file mode 100644 index 00000000..9077cef1 --- /dev/null +++ b/hr_holidays_accrual/README.rst @@ -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 `_. + +Copyright Hibou Corp. 2019 \ No newline at end of file diff --git a/hr_holidays_accrual/__init__.py b/hr_holidays_accrual/__init__.py new file mode 100755 index 00000000..0650744f --- /dev/null +++ b/hr_holidays_accrual/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/hr_holidays_accrual/__manifest__.py b/hr_holidays_accrual/__manifest__.py new file mode 100755 index 00000000..bac2ab3c --- /dev/null +++ b/hr_holidays_accrual/__manifest__.py @@ -0,0 +1,18 @@ +{ + 'name': 'HR Holidays Accrual', + 'author': 'Hibou Corp. ', + '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, +} diff --git a/hr_holidays_accrual/models/__init__.py b/hr_holidays_accrual/models/__init__.py new file mode 100644 index 00000000..070ade81 --- /dev/null +++ b/hr_holidays_accrual/models/__init__.py @@ -0,0 +1 @@ +from . import hr_holidays diff --git a/hr_holidays_accrual/models/hr_holidays.py b/hr_holidays_accrual/models/hr_holidays.py new file mode 100644 index 00000000..606eda93 --- /dev/null +++ b/hr_holidays_accrual/models/hr_holidays.py @@ -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) diff --git a/hr_holidays_accrual/tests/__init__.py b/hr_holidays_accrual/tests/__init__.py new file mode 100755 index 00000000..76829b5b --- /dev/null +++ b/hr_holidays_accrual/tests/__init__.py @@ -0,0 +1 @@ +from . import test_leaves diff --git a/hr_holidays_accrual/tests/test_leaves.py b/hr_holidays_accrual/tests/test_leaves.py new file mode 100644 index 00000000..44e44e4c --- /dev/null +++ b/hr_holidays_accrual/tests/test_leaves.py @@ -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) diff --git a/hr_holidays_accrual/views/hr_holidays_views.xml b/hr_holidays_accrual/views/hr_holidays_views.xml new file mode 100644 index 00000000..ec1b50e2 --- /dev/null +++ b/hr_holidays_accrual/views/hr_holidays_views.xml @@ -0,0 +1,15 @@ + + + + hr.holidays.edit.holiday.new.inherit + hr.leave.type + + + + + + + + + + \ No newline at end of file diff --git a/hr_holidays_accrual_payroll/README.rst b/hr_holidays_accrual_payroll/README.rst new file mode 100644 index 00000000..29cd57c4 --- /dev/null +++ b/hr_holidays_accrual_payroll/README.rst @@ -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 `_. + +============= +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 `_. + +Copyright Hibou Corp. 2019 diff --git a/hr_holidays_accrual_payroll/__init__.py b/hr_holidays_accrual_payroll/__init__.py new file mode 100755 index 00000000..0650744f --- /dev/null +++ b/hr_holidays_accrual_payroll/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/hr_holidays_accrual_payroll/__manifest__.py b/hr_holidays_accrual_payroll/__manifest__.py new file mode 100755 index 00000000..c434a958 --- /dev/null +++ b/hr_holidays_accrual_payroll/__manifest__.py @@ -0,0 +1,21 @@ +{ + 'name': 'HR Holidays Accrual - Payroll', + 'author': 'Hibou Corp. ', + '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, +} diff --git a/hr_holidays_accrual_payroll/models/__init__.py b/hr_holidays_accrual_payroll/models/__init__.py new file mode 100644 index 00000000..8b3405a2 --- /dev/null +++ b/hr_holidays_accrual_payroll/models/__init__.py @@ -0,0 +1,2 @@ +from . import hr_leave_allocation +from . import hr_payslip diff --git a/hr_holidays_accrual_payroll/models/hr_leave_allocation.py b/hr_holidays_accrual_payroll/models/hr_leave_allocation.py new file mode 100644 index 00000000..e05c10c8 --- /dev/null +++ b/hr_holidays_accrual_payroll/models/hr_leave_allocation.py @@ -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) diff --git a/hr_holidays_accrual_payroll/models/hr_payslip.py b/hr_holidays_accrual_payroll/models/hr_payslip.py new file mode 100644 index 00000000..c046cf9f --- /dev/null +++ b/hr_holidays_accrual_payroll/models/hr_payslip.py @@ -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 diff --git a/hr_holidays_accrual_payroll/tests/__init__.py b/hr_holidays_accrual_payroll/tests/__init__.py new file mode 100755 index 00000000..6e022122 --- /dev/null +++ b/hr_holidays_accrual_payroll/tests/__init__.py @@ -0,0 +1 @@ +from . import test_accrual diff --git a/hr_holidays_accrual_payroll/tests/test_accrual.py b/hr_holidays_accrual_payroll/tests/test_accrual.py new file mode 100644 index 00000000..2a329ae6 --- /dev/null +++ b/hr_holidays_accrual_payroll/tests/test_accrual.py @@ -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) diff --git a/hr_holidays_accrual_payroll/views/hr_holidays_views.xml b/hr_holidays_accrual_payroll/views/hr_holidays_views.xml new file mode 100644 index 00000000..045d1e99 --- /dev/null +++ b/hr_holidays_accrual_payroll/views/hr_holidays_views.xml @@ -0,0 +1,22 @@ + + + + + hr.leave.allocation.view.form.manager.payslip + hr.leave.allocation + + + + {'required': [('accrual', '=', True)], 'invisible': [('interval_unit', '=', 'payslip')]} + + + + + + + \ No newline at end of file