diff --git a/addons/hr_holidays/__manifest__.py b/addons/hr_holidays/__manifest__.py index db8472955f9..b2911b88e2b 100644 --- a/addons/hr_holidays/__manifest__.py +++ b/addons/hr_holidays/__manifest__.py @@ -38,6 +38,7 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p 'views/hr_leave_views.xml', 'views/hr_leave_type_views.xml', 'views/hr_leave_allocation_views.xml', + 'views/hr_leave_accrual_views.xml', 'views/mail_activity_views.xml', 'wizard/hr_holidays_summary_employees_views.xml', @@ -47,6 +48,7 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p 'report/hr_holidays_reports.xml', 'report/hr_leave_reports.xml', 'report/hr_leave_report_calendar.xml', + 'report/hr_leave_employee_type_report.xml', 'views/hr_views.xml', 'views/hr_holidays_views.xml', @@ -64,10 +66,12 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p ], 'web.assets_backend': [ 'hr_holidays/static/src/js/time_off_calendar.js', + 'hr_holidays/static/src/js/radio_image.js', 'hr_holidays/static/src/js/leave_stats_widget.js', 'hr_holidays/static/src/models/*/*.js', 'hr_holidays/static/src/scss/time_off.scss', 'hr_holidays/static/src/components/*/*.scss', + 'hr_holidays/static/src/scss/accrual_plan_level.scss', ], 'web.tests_assets': [ 'hr_holidays/static/tests/helpers/**/*', diff --git a/addons/hr_holidays/data/hr_holidays_data.xml b/addons/hr_holidays/data/hr_holidays_data.xml index f97277117d9..db3c4a29f86 100644 --- a/addons/hr_holidays/data/hr_holidays_data.xml +++ b/addons/hr_holidays/data/hr_holidays_data.xml @@ -1,55 +1,308 @@ + + + Annual_Time_Off.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Annual_Time_Off.svg + + + Annual_Time_Off_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Annual_Time_Off_2.svg + + + Annual_Time_Off_3.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Annual_Time_Off_3.svg + + + Compensatory_Time_Off.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Compensatory_Time_Off.svg + + + Compensatory_Time_Off_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Compensatory_Time_Off_2.svg + + + Compensatory_Time_Off_3.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Compensatory_Time_Off_3.svg + + + Credit_Time.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Credit_Time.svg + + + Credit_Time_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Credit_Time_2.svg + + + Extra_Time_Off.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Extra_Time_Off.svg + + + Extra_Time_Off_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Extra_Time_Off_2.svg + + + Maternity_Time_Off.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Maternity_Time_Off.svg + + + Maternity_Time_Off_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Maternity_Time_Off_2.svg + + + Maternity_Time_Off_3.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Maternity_Time_Off_3.svg + + + Paid_Time_Off.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Paid_Time_Off.svg + + + Paid_Time_Off_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Paid_Time_Off_2.svg + + + Paid_Time_Off_3.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Paid_Time_Off_3.svg + + + Parental_Time_Off.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Parental_Time_Off.svg + + + Parental_Time_Off_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Parental_Time_Off_2.svg + + + Recovery_Bank_Holiday.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Recovery_Bank_Holiday.svg + + + Recovery_Bank_Holiday_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Recovery_Bank_Holiday_2.svg + + + Sick_Time_Off.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Sick_Time_Off.svg + + + Sick_Time_Off_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Sick_Time_Off_2.svg + + + Small_Unemployement.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Small_Unemployement.svg + + + Small_Unemployement_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Small_Unemployement_2.svg + + + Small_Unemployement_3.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Small_Unemployement_3.svg + + + Training_Time_Off.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Training_Time_Off.svg + + + Training_Time_Off_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Training_Time_Off_2.svg + + + Unpaid_Time_Off.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Unpaid_Time_Off.svg + + + Unpaid_Time_Off_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Unpaid_Time_Off_2.svg + + + Work_Accident_Time_Off.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Work_Accident_Time_Off.svg + + + Work_Accident_Time_Off_2.svg + hr.leave.type + icon_id + + url + /hr_holidays/static/src/img/icons/Work_Accident_Time_Off_2.svg + + Paid Time Off - fixed + yes + no both - both - black + officer - + + 2 Sick Time Off - no + no red - True + + 3 Compensatory Days - fixed_allocation + yes + yes manager - manager + officer hour - lavender - + + 4 Unpaid - no + no both - both - brown + officer hour - + + 5 diff --git a/addons/hr_holidays/data/hr_holidays_demo.xml b/addons/hr_holidays/data/hr_holidays_demo.xml index e9f10534abe..4318519232b 100644 --- a/addons/hr_holidays/data/hr_holidays_demo.xml +++ b/addons/hr_holidays/data/hr_holidays_demo.xml @@ -9,11 +9,11 @@ Parental Leaves - fixed + yes + no brown both - both - + officer @@ -25,6 +25,8 @@ 20 validate + + @@ -33,8 +35,10 @@ 7 confirm + + - + @@ -42,7 +46,10 @@ Summer Vacation 7 + confirm + + @@ -51,30 +58,32 @@ 12 validate + + Trip with Family - - - - + + + + Doctor Appointment - - - - + + + + confirm - + @@ -103,6 +112,8 @@ 20 validate + + @@ -111,6 +122,8 @@ 10 validate + + @@ -119,6 +132,8 @@ 12 validate + + @@ -131,21 +146,21 @@ - + Dentist appointment - - - - + + + + confirm - + @@ -157,13 +172,18 @@ 20 validate + + Summer Vacation 7 + confirm + + @@ -176,17 +196,17 @@ - + Trip - - - - + + + + @@ -198,6 +218,8 @@ 20 validate + + @@ -205,8 +227,11 @@ 7 + confirm + + - + @@ -221,7 +246,7 @@ confirm - + @@ -235,7 +260,7 @@ confirm - + @@ -247,6 +272,8 @@ 20 validate + + @@ -254,6 +281,9 @@ 7 + confirm + + @@ -264,6 +294,8 @@ 20 validate + + @@ -271,8 +303,11 @@ 5 + confirm + + - + @@ -298,7 +333,7 @@ confirm - + @@ -310,13 +345,18 @@ 20 confirm + + Summer Vacation 5 + confirm + + diff --git a/addons/hr_holidays/models/__init__.py b/addons/hr_holidays/models/__init__.py index 368a7bc4bf1..1ab720a3edb 100644 --- a/addons/hr_holidays/models/__init__.py +++ b/addons/hr_holidays/models/__init__.py @@ -7,6 +7,8 @@ from . import hr_department from . import hr_leave from . import hr_leave_allocation from . import hr_leave_type +from . import hr_leave_accrual_plan_level +from . import hr_leave_accrual_plan from . import mail_message_subtype from . import res_partner from . import res_users diff --git a/addons/hr_holidays/models/hr_employee.py b/addons/hr_holidays/models/hr_employee.py index 228d1c8104b..36adb4a7aee 100644 --- a/addons/hr_holidays/models/hr_employee.py +++ b/addons/hr_holidays/models/hr_employee.py @@ -32,6 +32,7 @@ class HrEmployeeBase(models.AbstractModel): leave_date_to = fields.Date('To Date', compute='_compute_leave_status') leaves_count = fields.Float('Number of Time Off', compute='_compute_remaining_leaves') allocation_count = fields.Float('Total number of days allocated.', compute='_compute_allocation_count') + allocations_count = fields.Integer('Total number of allocations', compute="_compute_allocation_count") allocation_used_count = fields.Float('Total number of days off used', compute='_compute_total_allocation_used') show_leaves = fields.Boolean('Able to see Remaining Time Off', compute='_compute_show_leaves') is_absent = fields.Boolean('Absent Today', compute='_compute_leave_status', search='_search_absent_employee') @@ -40,9 +41,6 @@ class HrEmployeeBase(models.AbstractModel): hr_icon_display = fields.Selection(selection_add=[('presence_holiday_absent', 'On leave'), ('presence_holiday_present', 'Present but on leave')]) - def _get_date_start_work(self): - return self.create_date - def _get_remaining_leaves(self): """ Helper to compute the remaining leaves for the current employees :returns dict where the key is the employee id, and the value is the remain leaves @@ -64,7 +62,7 @@ class HrEmployeeBase(models.AbstractModel): join hr_leave_type s ON (s.id=h.holiday_status_id) WHERE s.active = true AND h.state='validate' AND - (s.allocation_type='fixed' OR s.allocation_type='fixed_allocation') AND + s.requires_allocation='yes' AND h.employee_id in %s GROUP BY h.employee_id""", (tuple(self.ids),)) return dict((row['employee_id'], row['days']) for row in self._cr.dictfetchall()) @@ -84,10 +82,12 @@ class HrEmployeeBase(models.AbstractModel): ('holiday_status_id.active', '=', True), ('state', '=', 'validate'), ], ['number_of_days:sum', 'employee_id'], ['employee_id']) - rg_results = dict((d['employee_id'][0], d['number_of_days']) for d in data) + rg_results = dict((d['employee_id'][0], {"employee_id_count": d['employee_id_count'], "number_of_days": d['number_of_days']}) for d in data) for employee in self: - employee.allocation_count = float_round(rg_results.get(employee.id, 0.0), precision_digits=2) + result = rg_results.get(employee.id) + employee.allocation_count = float_round(result['number_of_days'], precision_digits=2) if result else 0.0 employee.allocation_display = "%g" % employee.allocation_count + employee.allocations_count = result['employee_id_count'] if result else 0.0 def _compute_total_allocation_used(self): for employee in self: diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index a52417077d7..11f417456a6 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -5,12 +5,12 @@ import logging -from collections import namedtuple +from collections import namedtuple, defaultdict from datetime import datetime, timedelta, time from pytz import timezone, UTC -from odoo import api, fields, models, tools +from odoo import api, fields, models, tools, SUPERUSER_ID from odoo.addons.base.models.res_partner import _tz_get from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY from odoo.exceptions import AccessError, UserError, ValidationError @@ -72,7 +72,7 @@ class HolidaysRequest(models.Model): defaults = self._default_get_request_parameters(defaults) if 'holiday_status_id' in fields_list and not defaults.get('holiday_status_id'): - lt = self.env['hr.leave.type'].search([('valid', '=', True), ('allocation_type', 'in', ['no', 'fixed_allocation'])], limit=1) + lt = self.env['hr.leave.type'].search(['|', ('requires_allocation', '=', 'no'), ('has_valid_allocation', '=', True)], limit=1) if lt: defaults['holiday_status_id'] = lt.id @@ -112,7 +112,6 @@ class HolidaysRequest(models.Model): private_name = fields.Char('Time Off Description', groups='hr_holidays.group_hr_holidays_user') state = fields.Selection([ ('draft', 'To Submit'), - ('cancel', 'Cancelled'), # YTI This state seems to be unused. To remove ('confirm', 'To Approve'), ('refuse', 'Refused'), ('validate1', 'Second Approval'), @@ -122,7 +121,6 @@ class HolidaysRequest(models.Model): "\nThe status is 'To Approve', when time off request is confirmed by user." + "\nThe status is 'Refused', when time off request is refused by manager." + "\nThe status is 'Approved', when time off request is approved by manager.") - payslip_status = fields.Boolean('Reported in last payslips', help='Green this button when the time off has been taken into account in the payslip.', copy=False) report_note = fields.Text('HR Comments', copy=False, groups="hr_holidays.group_hr_holidays_manager") user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, compute_sudo=True, store=True, default=lambda self: self.env.uid, readonly=True) manager_id = fields.Many2one('hr.employee', compute='_compute_from_employee_id', store=True, readonly=False) @@ -130,14 +128,18 @@ class HolidaysRequest(models.Model): holiday_status_id = fields.Many2one( "hr.leave.type", compute='_compute_from_employee_id', store=True, string="Time Off Type", required=True, readonly=False, states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]}, - domain=[('valid', '=', True)]) + domain=['|', ('requires_allocation', '=', 'no'), ('has_valid_allocation', '=', True)]) + holiday_allocation_id = fields.Many2one( + 'hr.leave.allocation', compute='_compute_from_holiday_status_id', string="Allocation", store=True, readonly=False) + color = fields.Integer("Color", related='holiday_status_id.color') validation_type = fields.Selection(string='Validation Type', related='holiday_status_id.leave_validation_type', readonly=False) # HR data employee_id = fields.Many2one( - 'hr.employee', compute='_compute_from_holiday_type', store=True, string='Employee', index=True, readonly=False, ondelete="restrict", + 'hr.employee', compute='_compute_from_employee_ids', store=True, string='Employee', index=True, readonly=False, ondelete="restrict", states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]}, tracking=True) + employee_company_id = fields.Many2one(related='employee_id.company_id', readonly=True, store=True) active_employee = fields.Boolean(related='employee_id.active', readonly=True) tz_mismatch = fields.Boolean(compute='_compute_tz_mismatch') tz = fields.Selection(_tz_get, compute='_compute_tz') @@ -176,6 +178,12 @@ class HolidaysRequest(models.Model): string='Allocation Mode', readonly=True, required=True, default='employee', states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help='By Employee: Allocation/Request for individual Employee, By Employee Tag: Allocation/Request for group of employees in category') + employee_ids = fields.Many2many( + 'hr.employee', compute='_compute_from_holiday_type', store=True, string='Employees', readonly=False, + states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]}) + multi_employee = fields.Boolean( + compute='_compute_from_employee_ids', store=True, + help='Holds whether this allocation concerns more than 1 employee') category_id = fields.Many2one( 'hr.employee.category', compute='_compute_from_holiday_type', store=True, string='Employee Tag', states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help='Category of Employee') @@ -196,6 +204,7 @@ class HolidaysRequest(models.Model): supported_attachment_ids = fields.Many2many( 'ir.attachment', string="Attach File", compute='_compute_supported_attachment_ids', inverse='_inverse_supported_attachment_ids') + supported_attachment_ids_count = fields.Integer(compute='_compute_supported_attachment_ids') # UX fields leave_type_request_unit = fields.Selection(related='holiday_status_id.request_unit', readonly=True) leave_type_support_document = fields.Boolean(related="holiday_status_id.support_document") @@ -263,10 +272,11 @@ class HolidaysRequest(models.Model): request_unit_custom = fields.Boolean('Days-long custom hours', compute='_compute_request_unit_custom', store=True, readonly=False) # view is_hatched = fields.Boolean('Hatched', compute='_compute_is_hatched') + is_striked = fields.Boolean('Striked', compute='_compute_is_hatched') _sql_constraints = [ ('type_value', - "CHECK((holiday_type='employee' AND employee_id IS NOT NULL) or " + "CHECK((holiday_type='employee' AND (employee_id IS NOT NULL OR multi_employee IS TRUE)) or " "(holiday_type='company' AND mode_company_id IS NOT NULL) or " "(holiday_type='category' AND category_id IS NOT NULL) or " "(holiday_type='department' AND department_id IS NOT NULL) )", @@ -313,8 +323,46 @@ class HolidaysRequest(models.Model): @api.depends('holiday_status_id') def _compute_state(self): - for holiday in self: - holiday.state = 'confirm' if holiday.validation_type != 'no_validation' else 'draft' + for leave in self: + leave.state = 'confirm' if leave.validation_type != 'no_validation' else 'draft' + + @api.depends('holiday_status_id.requires_allocation', 'validation_type', 'employee_id', 'date_from', 'date_to') + def _compute_from_holiday_status_id(self): + invalid_self = self.filtered(lambda leave: not leave.date_to or not leave.date_from) + if invalid_self: + invalid_self.update({'holiday_allocation_id': False}) + self = self - invalid_self + if not self: + return + allocations = self.env['hr.leave.allocation'].search_read( + [ + ('holiday_status_id', 'in', self.holiday_status_id.ids), + ('employee_id', 'in', self.employee_id.ids), + '|', + ('date_to', '>=', min(self.mapped('date_to'))), + '&', + ('date_to', '=', False), + ('date_from', '<=', max(self.mapped('date_from'))), + ], ['id', 'date_from', 'date_to', 'holiday_status_id', 'employee_id'], order="date_to, id" + ) + allocations_dict = defaultdict(lambda: []) + for allocation in allocations: + allocations_dict[(allocation['holiday_status_id'][0], allocation['employee_id'][0])].append(allocation) + + for leave in self: + if leave.holiday_status_id.requires_allocation == 'yes' and leave.date_from and leave.date_to: + found_allocation = False + date_to = leave.date_to.replace(tzinfo=UTC).astimezone(timezone(leave.tz)).date() + date_from = leave.date_from.replace(tzinfo=UTC).astimezone(timezone(leave.tz)).date() + for allocation in allocations_dict[(leave.holiday_status_id.id, leave.employee_id.id)]: + date_to_check = allocation['date_to'] > date_to if allocation['date_to'] else False + date_from_check = allocation['date_to'] is False and allocation['date_from'] <= date_from + if (date_to_check or date_from_check): + found_allocation = allocation['id'] + break + leave.holiday_allocation_id = self.env['hr.leave.allocation'].browse(found_allocation) if found_allocation else False + else: + leave.holiday_allocation_id = False @api.depends('request_date_from_period', 'request_hour_from', 'request_hour_to', 'request_date_from', 'request_date_to', 'request_unit_half', 'request_unit_hours', 'request_unit_custom', 'employee_id') @@ -409,28 +457,37 @@ class HolidaysRequest(models.Model): if holiday.holiday_status_id or holiday.request_unit_half or holiday.request_unit_hours: holiday.request_unit_custom = False + @api.depends('employee_ids') + def _compute_from_employee_ids(self): + for holiday in self: + if len(holiday.employee_ids) == 1: + holiday.employee_id = holiday.employee_ids[0]._origin + else: + holiday.employee_id = False + holiday.multi_employee = (len(holiday.employee_ids) > 1) + @api.depends('holiday_type') def _compute_from_holiday_type(self): for holiday in self: if holiday.holiday_type == 'employee': - if not holiday.employee_id: - holiday.employee_id = self.env.user.employee_id + if not holiday.employee_ids: + holiday.employee_ids = self.env.user.employee_id holiday.mode_company_id = False holiday.category_id = False elif holiday.holiday_type == 'company': - holiday.employee_id = False + holiday.employee_ids = False if not holiday.mode_company_id: holiday.mode_company_id = self.env.company.id holiday.category_id = False elif holiday.holiday_type == 'department': - holiday.employee_id = False + holiday.employee_ids = False holiday.mode_company_id = False holiday.category_id = False elif holiday.holiday_type == 'category': - holiday.employee_id = False + holiday.employee_ids = False holiday.mode_company_id = False else: - holiday.employee_id = self.env.context.get('default_employee_id') or self.env.user.employee_id + holiday.employee_ids = self.env.context.get('default_employee_id') or self.env.user.employee_id @api.depends('employee_id') def _compute_from_employee_id(self): @@ -559,12 +616,14 @@ class HolidaysRequest(models.Model): @api.depends('state') def _compute_is_hatched(self): for holiday in self: + holiday.is_striked = holiday.state == 'refuse' holiday.is_hatched = holiday.state not in ['refuse', 'validate'] @api.depends('leave_type_support_document', 'attachment_ids') def _compute_supported_attachment_ids(self): for holiday in self: holiday.supported_attachment_ids = holiday.attachment_ids + holiday.supported_attachment_ids_count = len(holiday.attachment_ids.ids) def _inverse_supported_attachment_ids(self): for holiday in self: @@ -590,7 +649,7 @@ class HolidaysRequest(models.Model): def _check_holidays(self): mapped_days = self.mapped('holiday_status_id').get_employees_days(self.mapped('employee_id').ids) for holiday in self: - if holiday.holiday_type != 'employee' or not holiday.employee_id or holiday.holiday_status_id.allocation_type == 'no': + if holiday.holiday_type != 'employee' or not holiday.employee_id or holiday.holiday_status_id.requires_allocation == 'no': continue leave_days = mapped_days[holiday.employee_id.id][holiday.holiday_status_id.id] if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1 or float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1: @@ -664,8 +723,10 @@ class HolidaysRequest(models.Model): target = leave.department_id.name elif leave.holiday_type == 'category': target = leave.category_id.name - else: + elif leave.employee_id: target = leave.employee_id.name + else: + target = ', '.join(leave.employee_ids.mapped('name')) if leave.leave_type_request_unit == 'hour': if self.env.context.get('hide_employee_name') and 'employee_id' in self.env.context.get('group_by', []): res.append(( @@ -717,34 +778,43 @@ class HolidaysRequest(models.Model): if employee.user_id: self.message_subscribe(partner_ids=employee.user_id.partner_id.ids) - @api.constrains('holiday_status_id', 'date_to', 'date_from') + @api.constrains('holiday_allocation_id') + def _check_allocation_id(self): + for leave in self: + if leave.holiday_status_id.requires_allocation == 'yes' and not leave.holiday_allocation_id: + raise ValidationError(_( + 'Could not find an allocation of type %(leave_type)s for the requested time period.', + leave_type=leave.holiday_status_id.display_name, + )) + + @api.constrains('holiday_allocation_id', 'date_to', 'date_from') def _check_leave_type_validity(self): for leave in self: - vstart = leave.holiday_status_id.validity_start - vstop = leave.holiday_status_id.validity_stop + vstart = leave.holiday_allocation_id.date_from + vstop = leave.holiday_allocation_id.date_to dfrom = leave.date_from dto = leave.date_to - if leave.holiday_status_id.validity_start and leave.holiday_status_id.validity_stop: + if vstart and vstop: if dfrom and dto and (dfrom.date() < vstart or dto.date() > vstop): raise ValidationError(_( '%(leave_type)s are only valid between %(start)s and %(end)s', leave_type=leave.holiday_status_id.display_name, - start=leave.holiday_status_id.validity_start, - end=leave.holiday_status_id.validity_stop + start=vstart, + end=vstop )) - elif leave.holiday_status_id.validity_start: + elif vstart: if dfrom and (dfrom.date() < vstart): raise ValidationError(_( '%(leave_type)s are only valid starting from %(date)s', leave_type=leave.holiday_status_id.display_name, - date=leave.holiday_status_id.validity_start + date=vstart )) - elif leave.holiday_status_id.validity_stop: + elif vstop: if dto and (dto.date() > vstop): raise ValidationError(_( '%(leave_type)s are only valid until %(date)s', leave_type=leave.holiday_status_id.display_name, - date=leave.holiday_status_id.validity_stop + date=vstop )) def _check_double_validation_rules(self, employees, state): @@ -788,6 +858,8 @@ class HolidaysRequest(models.Model): holidays = super(HolidaysRequest, self.with_context(mail_create_nosubscribe=True)).create(vals_list) + holidays.filtered(lambda holiday: not holiday.holiday_allocation_id).with_user(SUPERUSER_ID)._compute_from_holiday_status_id() + for holiday in holidays: if not self._context.get('leave_fast_create'): # Everything that is done here must be done using sudo because we might @@ -1019,94 +1091,107 @@ class HolidaysRequest(models.Model): raise UserError(_('Time off request must be confirmed in order to approve it.')) self.write({'state': 'validate'}) - self.filtered(lambda holiday: holiday.validation_type == 'both').write({'second_approver_id': current_employee.id}) - self.filtered(lambda holiday: holiday.validation_type != 'both').write({'first_approver_id': current_employee.id}) - for holiday in self.filtered(lambda holiday: holiday.holiday_type != 'employee'): - if holiday.holiday_type == 'category': - employees = holiday.category_id.employee_ids - elif holiday.holiday_type == 'company': - employees = self.env['hr.employee'].search([('company_id', '=', holiday.mode_company_id.id)]) + leaves_second_approver = self.env['hr.leave'] + leaves_first_approver = self.env['hr.leave'] + + for leave in self: + if leave.validation_type == 'both': + leaves_second_approver += leave else: - employees = holiday.department_id.member_ids + leaves_first_approver += leave - conflicting_leaves = self.env['hr.leave'].with_context( - tracking_disable=True, - mail_activity_automation_skip=True, - leave_fast_create=True - ).search([ - ('date_from', '<=', holiday.date_to), - ('date_to', '>', holiday.date_from), - ('state', 'not in', ['cancel', 'refuse']), - ('holiday_type', '=', 'employee'), - ('employee_id', 'in', employees.ids)]) + if leave.holiday_type != 'employee' or\ + (leave.holiday_type == 'employee' and len(leave.employee_ids) > 1): + if leave.holiday_type == 'employee': + employees = leave.employee_ids + elif leave.holiday_type == 'category': + employees = leave.category_id.employee_ids + elif leave.holiday_type == 'company': + employees = self.env['hr.employee'].search([('company_id', '=', leave.mode_company_id.id)]) + else: + employees = leave.department_id.member_ids - if conflicting_leaves: - # YTI: More complex use cases could be managed in master - if holiday.leave_type_request_unit != 'day' or any(l.leave_type_request_unit == 'hour' for l in conflicting_leaves): - raise ValidationError(_('You can not have 2 time off that overlaps on the same day.')) + conflicting_leaves = self.env['hr.leave'].with_context( + tracking_disable=True, + mail_activity_automation_skip=True, + leave_fast_create=True + ).search([ + ('date_from', '<=', leave.date_to), + ('date_to', '>', leave.date_from), + ('state', 'not in', ['cancel', 'refuse']), + ('holiday_type', '=', 'employee'), + ('employee_id', 'in', employees.ids)]) - # keep track of conflicting leaves states before refusal - target_states = {l.id: l.state for l in conflicting_leaves} - conflicting_leaves.action_refuse() - split_leaves_vals = [] - for conflicting_leave in conflicting_leaves: - if conflicting_leave.leave_type_request_unit == 'half_day' and conflicting_leave.request_unit_half: - continue + if conflicting_leaves: + # YTI: More complex use cases could be managed in master + if leave.leave_type_request_unit != 'day' or any(l.leave_type_request_unit == 'hour' for l in conflicting_leaves): + raise ValidationError(_('You can not have 2 time off that overlaps on the same day.')) - # Leaves in days - if conflicting_leave.date_from < holiday.date_from: - before_leave_vals = conflicting_leave.copy_data({ - 'date_from': conflicting_leave.date_from.date(), - 'date_to': holiday.date_from.date() + timedelta(days=-1), - 'state': target_states[conflicting_leave.id], - })[0] - before_leave = self.env['hr.leave'].new(before_leave_vals) - before_leave._compute_date_from_to() + # keep track of conflicting leaves states before refusal + target_states = {l.id: l.state for l in conflicting_leaves} + conflicting_leaves.action_refuse() + split_leaves_vals = [] + for conflicting_leave in conflicting_leaves: + if conflicting_leave.leave_type_request_unit == 'half_day' and conflicting_leave.request_unit_half: + continue - # Could happen for part-time contract, that time off is not necessary - # anymore. - # Imagine you work on monday-wednesday-friday only. - # You take a time off on friday. - # We create a company time off on friday. - # By looking at the last attendance before the company time off - # start date to compute the date_to, you would have a date_from > date_to. - # Just don't create the leave at that time. That's the reason why we use - # new instead of create. As the leave is not actually created yet, the sql - # constraint didn't check date_from < date_to yet. - if before_leave.date_from < before_leave.date_to: - split_leaves_vals.append(before_leave._convert_to_write(before_leave._cache)) - if conflicting_leave.date_to > holiday.date_to: - after_leave_vals = conflicting_leave.copy_data({ - 'date_from': holiday.date_to.date() + timedelta(days=1), - 'date_to': conflicting_leave.date_to.date(), - 'state': target_states[conflicting_leave.id], - })[0] - after_leave = self.env['hr.leave'].new(after_leave_vals) - after_leave._compute_date_from_to() - # Could happen for part-time contract, that time off is not necessary - # anymore. - if after_leave.date_from < after_leave.date_to: - split_leaves_vals.append(after_leave._convert_to_write(after_leave._cache)) + # Leaves in days + if conflicting_leave.date_from < leave.date_from: + before_leave_vals = conflicting_leave.copy_data({ + 'date_from': conflicting_leave.date_from.date(), + 'date_to': leave.date_from.date() + timedelta(days=-1), + 'state': target_states[conflicting_leave.id], + })[0] + before_leave = self.env['hr.leave'].new(before_leave_vals) + before_leave._compute_date_from_to() - split_leaves = self.env['hr.leave'].with_context( + # Could happen for part-time contract, that time off is not necessary + # anymore. + # Imagine you work on monday-wednesday-friday only. + # You take a time off on friday. + # We create a company time off on friday. + # By looking at the last attendance before the company time off + # start date to compute the date_to, you would have a date_from > date_to. + # Just don't create the leave at that time. That's the reason why we use + # new instead of create. As the leave is not actually created yet, the sql + # constraint didn't check date_from < date_to yet. + if before_leave.date_from < before_leave.date_to: + split_leaves_vals.append(before_leave._convert_to_write(before_leave._cache)) + if conflicting_leave.date_to > leave.date_to: + after_leave_vals = conflicting_leave.copy_data({ + 'date_from': leave.date_to.date() + timedelta(days=1), + 'date_to': conflicting_leave.date_to.date(), + 'state': target_states[conflicting_leave.id], + })[0] + after_leave = self.env['hr.leave'].new(after_leave_vals) + after_leave._compute_date_from_to() + # Could happen for part-time contract, that time off is not necessary + # anymore. + if after_leave.date_from < after_leave.date_to: + split_leaves_vals.append(after_leave._convert_to_write(after_leave._cache)) + + split_leaves = self.env['hr.leave'].with_context( + tracking_disable=True, + mail_activity_automation_skip=True, + leave_fast_create=True, + leave_skip_state_check=True + ).create(split_leaves_vals) + + split_leaves.filtered(lambda l: l.state in 'validate')._validate_leave_request() + + values = leave._prepare_employees_holiday_values(employees) + leaves = self.env['hr.leave'].with_context( tracking_disable=True, mail_activity_automation_skip=True, leave_fast_create=True, - leave_skip_state_check=True - ).create(split_leaves_vals) + leave_skip_state_check=True, + ).create(values) - split_leaves.filtered(lambda l: l.state in 'validate')._validate_leave_request() + leaves._validate_leave_request() - values = holiday._prepare_employees_holiday_values(employees) - leaves = self.env['hr.leave'].with_context( - tracking_disable=True, - mail_activity_automation_skip=True, - leave_fast_create=True, - leave_skip_state_check=True, - ).create(values) - - leaves._validate_leave_request() + leaves_second_approver.write({'second_approver_id': current_employee.id}) + leaves_first_approver.write({'first_approver_id': current_employee.id}) employee_requests = self.filtered(lambda hol: hol.holiday_type == 'employee') employee_requests._validate_leave_request() @@ -1140,6 +1225,18 @@ class HolidaysRequest(models.Model): self.activity_update() return True + def action_documents(self): + domain = [('id', 'in', self.attachment_ids.ids)] + return { + 'name': _("Supporting Documents"), + 'type': 'ir.actions.act_window', + 'res_model': 'ir.attachment', + 'context': {'create': False}, + 'view_mode': 'list', + 'domain': domain + } + + def _check_approval_update(self, state): """ Check if target state is achievable. """ if self.env.is_superuser(): diff --git a/addons/hr_holidays/models/hr_leave_accrual_plan.py b/addons/hr_holidays/models/hr_leave_accrual_plan.py new file mode 100644 index 00000000000..cb5b30f49ed --- /dev/null +++ b/addons/hr_holidays/models/hr_leave_accrual_plan.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ + + +class AccrualPlan(models.Model): + _name = "hr.leave.accrual.plan" + _description = "Accrual Plan" + + name = fields.Char('Name', required=True) + time_off_type_id = fields.Many2one('hr.leave.type', string="Time Off Type") + employees_count = fields.Integer("Employees", compute='_compute_employee_count') + level_ids = fields.One2many('hr.leave.accrual.level', 'accrual_plan_id') + allocation_ids = fields.One2many('hr.leave.allocation', 'accrual_plan_id') + transition_mode = fields.Selection([ + ('immediately', 'Immediately'), + ('end_of_accrual', "After this accrual's period")], + string="Level Transition", default="immediately", required=True, + help="""Immediately: When the date corresponds to the new level, your accrual is automatically computed, granted and you switch to new level + After this accrual's period: When the accrual is complete (a week, a month), and granted, you switch to next level if allocation date corresponds""") + + @api.depends('allocation_ids') + def _compute_employee_count(self): + allocations_read_group = self.env['hr.leave.allocation'].read_group( + [('accrual_plan_id', 'in', self.ids)], + ['accrual_plan_id', 'employee_count:count_distinct(employee_id)'], + ['accrual_plan_id'], + ) + allocations_dict = {res['accrual_plan_id'][0]: res['employee_count'] for res in allocations_read_group} + for plan in self: + plan.employees_count = allocations_dict.get(plan.id, 0) + + def action_open_accrual_plan_employees(self): + self.ensure_one() + + return { + 'name': _("Accrual Plan's Employees"), + 'type': 'ir.actions.act_window', + 'view_mode': 'kanban,tree,form', + 'res_model': 'hr.employee', + 'domain': [('id', 'in', self.allocation_ids.employee_id.ids)], + } diff --git a/addons/hr_holidays/models/hr_leave_accrual_plan_level.py b/addons/hr_holidays/models/hr_leave_accrual_plan_level.py new file mode 100644 index 00000000000..49dac51ddbf --- /dev/null +++ b/addons/hr_holidays/models/hr_leave_accrual_plan_level.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import datetime +import calendar + +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models +from odoo.tools.date_utils import get_timedelta + + +DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] +MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] + +def _get_date_check_month(year, month, day): + """ + Returns the last day of the month for the given date if the day is outside of the month's range, i.e 30 feb + """ + month_range = calendar.monthrange(year, month) + if day > month_range[1]: + return datetime.date(year, month, min(day, month_range[1])) + relativedelta(days=(day - month_range[1])) + return datetime.date(year, month, day) + +class AccrualPlanLevel(models.Model): + _name = "hr.leave.accrual.level" + _description = "Accrual Plan Level" + _order = 'sequence asc' + + sequence = fields.Integer( + string='sequence', compute='_compute_sequence', store=True, + help='Sequence is generated automatically by start time delta.') + level = fields.Integer(compute='_compute_level', help='Level computed through the sequence.') + accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', "Accrual Plan", required=True) + start_count = fields.Integer( + "Start after", + help="The accrual starts after a defined period from the employee start date. This field define the number of days, month or years after which accrual is used.", default="1") + start_type = fields.Selection( + [('day', 'day(s)'), + ('month', 'month(s)'), + ('year', 'year(s)')], + default='day', string=" ", required=True, + help="This field define the unit of time after which the accrual starts.") + is_based_on_worked_time = fields.Boolean("Based on worked time") + + # Accrue of + added_value = fields.Float( + "Gain", required=True, + help="The number of days that will be incremented for every period") + added_value_type = fields.Selection( + [('days', 'Days'), + ('hours', 'Hours')], + default='days', required=True) + frequency = fields.Selection([ + ('daily', 'Daily'), + ('weekly', 'Weekly'), + ('bimonthly', 'Twice a month'), + ('monthly', 'Monthly'), + ('biyearly', 'Twice a year'), + ('yearly', 'Yearly'), + ], default='daily', required=True, string="Frequency") + week_day = fields.Selection([ + ('mon', 'Monday'), + ('tue', 'Tuesday'), + ('wed', 'Wednesday'), + ('thu', 'Thursday'), + ('fri', 'Friday'), + ('sat', 'Saturday'), + ('sun', 'Sunday'), + ], default='mon', required=True, string="Allocation on") + first_day = fields.Integer(default=1) + second_day = fields.Integer(default=15) + first_month_day = fields.Integer(default=1) + first_month = fields.Selection([ + ('jan', 'January'), + ('feb', 'February'), + ('mar', 'March'), + ('apr', 'April'), + ('may', 'May'), + ('jun', 'June'), + ], default="jan") + second_month_day = fields.Integer(default=1) + second_month = fields.Selection([ + ('jul', 'July'), + ('aug', 'Augustus'), + ('sep', 'September'), + ('oct', 'October'), + ('nov', 'November'), + ('dec', 'December') + ], default="jul") + yearly_month = fields.Selection([ + ('jan', 'January'), + ('feb', 'February'), + ('mar', 'March'), + ('apr', 'April'), + ('may', 'May'), + ('jun', 'June'), + ('jul', 'July'), + ('aug', 'Augustus'), + ('sep', 'September'), + ('oct', 'October'), + ('nov', 'November'), + ('dec', 'December') + ], default="jan") + yearly_day = fields.Integer(default=1) + maximum_leave = fields.Float( + 'Limit to', required=False, default=100, + help="Choose a maximum limit of days for this accrual. 0 means no limit.") + parent_id = fields.Many2one( + 'hr.leave.accrual.level', string="Previous Level", + help="If this field is empty, this level is the first one.") + action_with_unused_accruals = fields.Selection( + [('postponed', 'Postponed to next year'), + ('lost', 'Lost')], + string="At the end of the year, unused accruals will be", + default='postponed', required='True') + + _sql_constraints = [ + ('check_dates', + "CHECK( (frequency = 'daily') or" + "(week_day IS NOT NULL AND frequency = 'weekly') or " + "(first_day > 0 AND second_day > first_day AND first_day <= 31 AND second_day <= 31 AND frequency = 'bimonthly') or " + "(first_day > 0 AND first_day <= 31 AND frequency = 'monthly')or " + "(first_month_day > 0 AND first_month_day <= 31 AND second_month_day > 0 AND second_month_day <= 31 AND frequency = 'biyearly') or " + "(yearly_day > 0 AND yearly_day <= 31 AND frequency = 'yearly'))", + "The dates you've set up aren't correct. Please check them."), + ('start_count_check', "CHECK( start_count >= 1 )", "You must start after more than 0 days."), + ('added_value_greater_than_zero', 'CHECK(added_value > 0)', 'You must give the gain greater than 0 in accrual plan levels.') + ] + + @api.depends('start_count', 'start_type') + def _compute_sequence(self): + # Not 100% accurate because of odd months/years, but good enough + start_type_multipliers = { + 'day': 1, + 'month': 30, + 'year': 365, + } + for level in self: + level.sequence = level.start_count * start_type_multipliers[level.start_type] + + @api.depends('sequence', 'accrual_plan_id') + def _compute_level(self): + #Mapped level_ids.ids ordered by sequence per plan + mapped_level_ids = {} + for plan in self.accrual_plan_id: + # We can not use .ids here because we also deal with NewIds + mapped_level_ids[plan] = [level.id for level in plan.level_ids.sorted('sequence')] + for level in self: + if level.accrual_plan_id: + level.level = mapped_level_ids[level.accrual_plan_id].index(level.id) + 1 + else: + level.level = 1 + + def _get_accrual_values(self, allocation_create_date): + """ + This method returns all the accrual linked to their accrual_plan with the updated dynamic parameters depending + on the date. + :return: dict: {accrual_id, accrual_start, accrual_stop, nextcall, sufficient_seniority} + where accrual_start and accrual_stop are start and stop of the current period + """ + today = fields.Date.context_today(self, ) + results = [] + for accrual in self: + seniority = allocation_create_date + get_timedelta(accrual.start_count, accrual.start_type) + frequency = accrual.frequency + if frequency == 'daily': + accrual_start = max(today, seniority.date()) + accrual_stop = accrual_start + relativedelta(days=1) + nextcall = accrual_stop + elif frequency == 'weekly': + min_accrual_date = max(today, seniority.date()) + if min_accrual_date.isoweekday() == DAYS.index(accrual.week_day): + accrual_stop = min_accrual_date + else: + accrual_stop = accrual._get_next_weekday(min_accrual_date, accrual.week_day) + accrual_start = accrual_stop - relativedelta(days=7) + nextcall = accrual._get_next_weekday(min_accrual_date, accrual.week_day) + elif frequency == 'bimonthly': + if today.day <= accrual.first_day: + accrual_start = datetime.date(today.year, today.month, accrual.second_day) - relativedelta(months=1) + accrual_stop = datetime.date(today.year, today.month, accrual.first_day) + nextcall = datetime.date(today.year, today.month, accrual.second_day) + else: + if today.day <= accrual.second_day: + accrual_start = datetime.date(today.year, today.month, accrual.first_day) + accrual_stop = datetime.date(today.year, today.month, accrual.second_day) + nextcall = datetime.date(today.year, today.month, accrual.first_day) + relativedelta(months=1) + else: + accrual_start = datetime.date(today.year, today.month, accrual.second_day) + accrual_stop = datetime.date(today.year, today.month, accrual.first_day) + relativedelta(months=1) + nextcall = datetime.date(today.year, today.month, accrual.second_day) + relativedelta(months=1) + elif frequency == 'monthly': + if today.day <= accrual.first_day: + accrual_start = datetime.date(today.year, today.month, accrual.first_day) - relativedelta(months=1) + accrual_stop = datetime.date(today.year, today.month, accrual.first_day) + nextcall = datetime.date(today.year, today.month, accrual.first_day) + relativedelta(months=1) + else: + accrual_start = datetime.date(today.year, today.month, accrual.first_day) + accrual_stop = datetime.date(today.year, today.month, accrual.first_day) + relativedelta(months=1) + nextcall = datetime.date(today.year, today.month, accrual.first_day) + relativedelta(months=2) + elif frequency == 'biyearly': + first_month = MONTHS.index(accrual.first_month) + 1 + second_month = MONTHS.index(accrual.second_month) + 1 + potential_first_accrual_date = datetime.date(today.year, first_month, accrual.first_month_day) + potential_second_accrual_date = datetime.date(today.year, second_month, accrual.second_month_day) + if today <= potential_first_accrual_date: + accrual_start = potential_second_accrual_date - relativedelta(years=1) + accrual_stop = potential_first_accrual_date + nextcall = potential_second_accrual_date + else: + if today <= potential_second_accrual_date: + accrual_start = potential_first_accrual_date + accrual_stop = potential_second_accrual_date + nextcall = potential_first_accrual_date + relativedelta(years=1) + else: + accrual_start = potential_second_accrual_date + accrual_stop = potential_first_accrual_date + relativedelta(years=1) + nextcall = potential_first_accrual_date + relativedelta(years=1) + elif frequency == 'yearly': + month = MONTHS.index(accrual.yearly_month) + 1 + potential_accrual_date = datetime.date(today.year, month, accrual.yearly_day) + if today <= potential_accrual_date: + accrual_start = potential_accrual_date - relativedelta(years=1) + accrual_stop = potential_accrual_date + nextcall = potential_accrual_date + relativedelta(years=1) + else: + accrual_start = potential_accrual_date + accrual_stop = potential_accrual_date + relativedelta(years=1) + nextcall = accrual_stop + + results.append({'accrual_level_id': accrual.id, + 'start_after': accrual.start_count, + 'accrual_start': datetime.datetime.combine(accrual_start, datetime.datetime.min.time()), + 'accrual_stop': datetime.datetime.combine(accrual_stop, datetime.datetime.min.time()), + 'nextcall': nextcall, + 'sufficient_seniority': seniority.date() <= today}) + return results + + def _get_next_date(self, last_call): + """ + Returns the next date with the given last call + """ + self.ensure_one() + if self.frequency == 'daily': + return last_call + relativedelta(days=1) + elif self.frequency == 'weekly': + return self._get_next_weekday(last_call, self.week_day) + elif self.frequency == 'bimonthly': + if last_call.day < self.first_day: + return _get_date_check_month(last_call.year, last_call.month, self.first_day) + elif last_call.day < self.second_day: + return _get_date_check_month(last_call.year, last_call.month, self.second_day) + else: + return _get_date_check_month(last_call.year, last_call.month, self.first_day) + relativedelta(months=1) + elif self.frequency == 'monthly': + if last_call.day < self.first_day: + return _get_date_check_month(last_call.year, last_call.month, self.first_day) + else: + return _get_date_check_month(last_call.year, last_call.month, self.first_day) + relativedelta(months=1) + elif self.frequency == 'biyearly': + first_month = MONTHS.index(self.first_month) + 1 + second_month = MONTHS.index(self.second_month) + 1 + if last_call < _get_date_check_month(last_call.year, first_month, self.first_month_day): + return _get_date_check_month(last_call.year, first_month, self.first_month_day) + elif last_call < _get_date_check_month(last_call.year, second_month, self.second_month_day): + return _get_date_check_month(last_call.year, second_month, self.second_month_day) + else: + return _get_date_check_month(last_call.year, first_month, self.first_month_day) + relativedelta(years=1) + elif self.frequency == 'yearly': + month = MONTHS.index(self.yearly_month) + 1 + if last_call < _get_date_check_month(last_call.year, month, self.yearly_day): + return _get_date_check_month(last_call.year, month, self.yearly_day) + else: + return _get_date_check_month(last_call.year, month, self.yearly_day) + relativedelta(years=1) + else: + return False + + @api.model + def _get_next_weekday(self, day, weekday): + """ + :param day: a datetime object + :param weekday: Weekday as a decimal number, where 0 is Sunday and 6 is Saturday. + :return: datetime of the next weekday + """ + daynames = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] + weekday = daynames.index(weekday) + days_ahead = weekday - day.isoweekday() + if days_ahead <= 0: + days_ahead += 7 + return day + relativedelta(days=days_ahead) diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index eda52005cbf..4a737ceca04 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -3,6 +3,7 @@ # Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com) +from collections import defaultdict import logging from datetime import datetime, time @@ -13,6 +14,7 @@ from odoo.addons.resource.models.resource import HOURS_PER_DAY from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools.translate import _ from odoo.tools.float_utils import float_round +from odoo.tools.date_utils import get_timedelta from odoo.osv import expression _logger = logging.getLogger(__name__) @@ -28,15 +30,15 @@ class HolidaysAllocation(models.Model): def _default_holiday_status_id(self): if self.user_has_groups('hr_holidays.group_hr_holidays_user'): - domain = [('valid', '=', True), ('allocation_type', '!=', 'no')] + domain = [('has_valid_allocation', '=', True), ('requires_allocation', '=', 'yes')] else: - domain = [('valid', '=', True), ('allocation_type', '=', 'fixed_allocation')] + domain = [('has_valid_allocation', '=', True), ('requires_allocation', '=', 'yes'), ('employee_requests', '=', 'yes')] return self.env['hr.leave.type'].search(domain, limit=1) - def _holiday_status_id_domain(self): - if self.user_has_groups('hr_holidays.group_hr_holidays_manager'): - return [('valid', '=', True), ('allocation_type', '!=', 'no')] - return [('valid', '=', True), ('allocation_type', '=', 'fixed_allocation')] + def _domain_holiday_status_id(self): + if self.user_has_groups('hr_holidays.group_hr_holidays_user'): + return [] + return [('employee_requests', '=', 'yes')] name = fields.Char('Description', compute='_compute_description', inverse='_inverse_description', search='_search_description', compute_sudo=False) active = fields.Boolean(default=True) @@ -46,30 +48,28 @@ class HolidaysAllocation(models.Model): ('cancel', 'Cancelled'), ('confirm', 'To Approve'), ('refuse', 'Refused'), - ('validate1', 'Second Approval'), ('validate', 'Approved') - ], string='Status', readonly=True, tracking=True, copy=False, default='confirm', + ], string='Status', readonly=True, tracking=True, copy=False, default='draft', help="The status is set to 'To Submit', when an allocation request is created." + "\nThe status is 'To Approve', when an allocation request is confirmed by user." + "\nThe status is 'Refused', when an allocation request is refused by manager." + "\nThe status is 'Approved', when an allocation request is approved by manager.") - date_from = fields.Datetime( - 'Start Date', readonly=True, index=True, copy=False, default=fields.Date.context_today, - states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, tracking=True) - date_to = fields.Datetime( - 'End Date', compute='_compute_from_holiday_status_id', store=True, readonly=False, copy=False, tracking=True, + date_from = fields.Date('Start Date', index=True, copy=False, default=fields.Date.context_today, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, tracking=True, required=True) + date_to = fields.Date('End Date', copy=False, tracking=True, states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]}) holiday_status_id = fields.Many2one( - "hr.leave.type", compute='_compute_from_employee_id', store=True, string="Time Off Type", required=True, readonly=False, + "hr.leave.type", compute='_compute_holiday_status_id', store=True, string="Time Off Type", required=True, readonly=False, states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]}, - domain=_holiday_status_id_domain, default=_default_holiday_status_id) + domain=_domain_holiday_status_id, + default=_default_holiday_status_id) employee_id = fields.Many2one( - 'hr.employee', compute='_compute_from_holiday_type', store=True, string='Employee', index=True, readonly=False, ondelete="restrict", tracking=True, - states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]}) - employee_company_id = fields.Many2one(related='employee_id.company_id', readonly=True) + 'hr.employee', compute='_compute_from_employee_ids', store=True, string='Employee', index=True, readonly=False, ondelete="restrict", tracking=True, + states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate': [('readonly', True)]}) + employee_company_id = fields.Many2one(related='employee_id.company_id', readonly=True, store=True) active_employee = fields.Boolean('Active Employee', related='employee_id.active', readonly=True) - manager_id = fields.Many2one('hr.employee', compute='_compute_from_employee_id', store=True, string='Manager') - notes = fields.Html('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) + manager_id = fields.Many2one('hr.employee', compute='_compute_manager_id', store=True, string='Manager') + notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) # duration number_of_days = fields.Float( 'Number of Days', compute='_compute_from_holiday_status_id', store=True, readonly=False, tracking=True, default=1, @@ -86,12 +86,9 @@ class HolidaysAllocation(models.Model): # details parent_id = fields.Many2one('hr.leave.allocation', string='Parent') linked_request_ids = fields.One2many('hr.leave.allocation', 'parent_id', string='Linked Requests') - first_approver_id = fields.Many2one( + approver_id = fields.Many2one( 'hr.employee', string='First Approval', readonly=True, copy=False, help='This area is automatically filled by the user who validates the allocation') - second_approver_id = fields.Many2one( - 'hr.employee', string='Second Approval', readonly=True, copy=False, - help='This area is automatically filled by the user who validates the allocation with second level (If allocation type need second validation)') validation_type = fields.Selection(string='Validation Type', related='holiday_status_id.allocation_validation_type', readonly=True) can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve') @@ -108,126 +105,49 @@ class HolidaysAllocation(models.Model): "\n- By Company: all employees of the specified company" "\n- By Department: all employees of the specified department" "\n- By Employee Tag: all employees of the specific employee group category") + employee_ids = fields.Many2many( + 'hr.employee', compute='_compute_from_holiday_type', store=True, string='Employees', readonly=False, + states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate': [('readonly', True)]}) + multi_employee = fields.Boolean( + compute='_compute_from_employee_ids', store=True, + help='Holds whether this allocation concerns more than 1 employee') mode_company_id = fields.Many2one( 'res.company', compute='_compute_from_holiday_type', store=True, string='Company Mode', readonly=False, - states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]}) + states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate': [('readonly', True)]}) department_id = fields.Many2one( 'hr.department', compute='_compute_department_id', store=True, string='Department', states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) category_id = fields.Many2one( 'hr.employee.category', compute='_compute_from_holiday_type', store=True, string='Employee Tag', readonly=False, - states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]}) + states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate': [('readonly', True)]}) # accrual configuration + lastcall = fields.Date("Date of the last accrual allocation", readonly=True, default=fields.Date.context_today) + nextcall = fields.Date("Date of the next accrual allocation", default=False, readonly=True) allocation_type = fields.Selection( [ ('regular', 'Regular Allocation'), ('accrual', 'Accrual Allocation') ], string="Allocation Type", default="regular", required=True, readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) - accrual_limit = fields.Integer('Balance limit', default=0, help="Maximum of allocation for accrual; 0 means no maximum.") - number_per_interval = fields.Float("Number of unit per interval", compute='_compute_from_holiday_status_id', store=True, readonly=False, - states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]}) - interval_number = fields.Integer("Number of unit between two intervals", compute='_compute_from_holiday_status_id', store=True, readonly=False, - states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]}) - unit_per_interval = fields.Selection([ - ('hours', 'Hours'), - ('days', 'Days') - ], compute='_compute_from_holiday_status_id', store=True, string="Unit of time added at each interval", readonly=False, - states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]}) - interval_unit = fields.Selection([ - ('days', 'Days'), - ('weeks', 'Weeks'), - ('months', 'Months'), - ('years', 'Years') - ], compute='_compute_from_holiday_status_id', store=True, string="Unit of time between two intervals", readonly=False, - states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]}) - nextcall = fields.Date("Date of the next accrual allocation", default=False, readonly=True) + is_officer = fields.Boolean(compute='_compute_is_officer') + accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', compute="_compute_from_holiday_status_id", store=True, readonly=False, domain="[('time_off_type_id', '=?', holiday_status_id)]", tracking=True) max_leaves = fields.Float(compute='_compute_leaves') leaves_taken = fields.Float(compute='_compute_leaves') + taken_leave_ids = fields.One2many('hr.leave', 'holiday_allocation_id', domain="[('state', 'in', ['confirm', 'validate1', 'validate'])]") _sql_constraints = [ ('type_value', - "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) or " + "CHECK( (holiday_type='employee' AND (employee_id IS NOT NULL OR multi_employee IS TRUE)) or " "(holiday_type='category' AND category_id IS NOT NULL) or " "(holiday_type='department' AND department_id IS NOT NULL) or " "(holiday_type='company' AND mode_company_id IS NOT NULL))", "The employee, department, company or employee category of this request is missing. Please make sure that your user login is linked to an employee."), - ('duration_check', "CHECK ( number_of_days >= 0 )", "The number of days must be greater than 0."), - ('number_per_interval_check', "CHECK(number_per_interval > 0)", "The number per interval should be greater than 0"), - ('interval_number_check', "CHECK(interval_number > 0)", "The interval number should be greater than 0"), + ('duration_check', "CHECK( ( number_of_days > 0 AND allocation_type='regular') or (allocation_type != 'regular'))", "The duration must be greater than 0."), ] - @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([('allocation_type', '=', 'accrual'), ('employee_id.active', '=', 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 == 'days': - delta = relativedelta(days=holiday.interval_number) - 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) - - if holiday.nextcall: - values['nextcall'] = holiday.nextcall + delta - else: - values['nextcall'] = holiday.date_from - while values['nextcall'] <= datetime.combine(today, time(0, 0, 0)): - values['nextcall'] += 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 or period_end < holiday.date_from: - 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 - - employee = holiday.employee_id - worked = employee._get_work_days_data_batch( - period_start, period_end, - domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')] - )[employee.id]['days'] - left = employee._get_leave_days_data_batch( - period_start, period_end, - domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')] - )[employee.id]['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 / (employee.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) + @api.depends_context('uid') + def _compute_is_officer(self): + self.is_officer = self.user_has_groups("hr_holidays.group_hr_holidays_user") @api.depends_context('uid') def _compute_description(self): @@ -261,9 +181,8 @@ class HolidaysAllocation(models.Model): @api.depends('employee_id', 'holiday_status_id') def _compute_leaves(self): for allocation in self: - leave_type = allocation.holiday_status_id.with_context(employee_id=allocation.employee_id.id) - allocation.max_leaves = leave_type.max_leaves - allocation.leaves_taken = leave_type.leaves_taken + allocation.max_leaves = allocation.number_of_hours_display if allocation.type_request_unit == 'hour' else allocation.number_of_days + allocation.leaves_taken = sum(taken_leave.number_of_hours_display if taken_leave.leave_type_request_unit == 'hour' else taken_leave.number_of_days for taken_leave in allocation.taken_leave_ids) @api.depends('number_of_days') def _compute_number_of_days_display(self): @@ -303,37 +222,44 @@ class HolidaysAllocation(models.Model): def _compute_can_approve(self): for allocation in self: try: - if allocation.state == 'confirm' and allocation.validation_type == 'both': - allocation._check_approval_update('validate1') - else: + if allocation.state == 'confirm' and allocation.validation_type != 'no': allocation._check_approval_update('validate') except (AccessError, UserError): allocation.can_approve = False else: allocation.can_approve = True + @api.depends('employee_ids') + def _compute_from_employee_ids(self): + for allocation in self: + if len(allocation.employee_ids) == 1: + allocation.employee_id = allocation.employee_ids[0]._origin + else: + allocation.employee_id = False + allocation.multi_employee = (len(allocation.employee_ids) > 1) + @api.depends('holiday_type') def _compute_from_holiday_type(self): for allocation in self: if allocation.holiday_type == 'employee': - if not allocation.employee_id: - allocation.employee_id = self.env.user.employee_id + if not allocation.employee_ids: + allocation.employee_ids = self.env.user.employee_id allocation.mode_company_id = False allocation.category_id = False if allocation.holiday_type == 'company': - allocation.employee_id = False + allocation.employee_ids = False if not allocation.mode_company_id: allocation.mode_company_id = self.env.company allocation.category_id = False elif allocation.holiday_type == 'department': - allocation.employee_id = False + allocation.employee_ids = False allocation.mode_company_id = False allocation.category_id = False elif allocation.holiday_type == 'category': - allocation.employee_id = False + allocation.employee_ids = False allocation.mode_company_id = False - elif not allocation.employee_id and not allocation._origin.employee_id: - allocation.employee_id = self.env.context.get('default_employee_id') or self.env.user.employee_id + else: + allocation.employee_ids = self.env.context.get('default_employee_id') or self.env.user.employee_id @api.depends('holiday_type', 'employee_id') def _compute_department_id(self): @@ -347,47 +273,207 @@ class HolidaysAllocation(models.Model): allocation.department_id = False @api.depends('employee_id') - def _compute_from_employee_id(self): - default_holiday_status_id = self._default_holiday_status_id() + def _compute_manager_id(self): + for allocation in self: + allocation.manager_id = allocation.employee_id and allocation.employee_id.parent_id + + @api.depends('employee_id', 'accrual_plan_id') + def _compute_holiday_status_id(self): + default_holiday_status_id = None + holiday_status_by_accrual = {accrual.id: accrual.time_off_type_id for accrual in self.accrual_plan_id} for holiday in self: - holiday.manager_id = holiday.employee_id and holiday.employee_id.parent_id if holiday.employee_id.user_id != self.env.user and holiday._origin.employee_id != holiday.employee_id: holiday.holiday_status_id = False - elif not holiday.holiday_status_id and not holiday._origin.holiday_status_id: - holiday.holiday_status_id = default_holiday_status_id + elif not holiday.holiday_status_id: + holiday_status = None + if holiday.accrual_plan_id: + holiday_status = holiday_status_by_accrual.get(holiday.accrual_plan_id.id) + if holiday_status: + holiday.holiday_status_id = holiday_status + elif not holiday._origin.holiday_status_id: + if not default_holiday_status_id: # fetch when we need it + default_holiday_status_id = self._default_holiday_status_id() + holiday.holiday_status_id = default_holiday_status_id - @api.depends('holiday_status_id', 'allocation_type', 'number_of_hours_display', 'number_of_days_display') + @api.depends('holiday_status_id', 'allocation_type', 'number_of_hours_display', 'number_of_days_display', 'date_to') def _compute_from_holiday_status_id(self): + accrual_allocations = self.filtered(lambda alloc: alloc.allocation_type == 'accrual' and not alloc.accrual_plan_id and alloc.holiday_status_id) + accruals_dict = {} + default_accrual_plan = None + if accrual_allocations: + accruals_read_group = self.env['hr.leave.accrual.plan'].read_group( + [('time_off_type_id', 'in', accrual_allocations.holiday_status_id.ids)], + ['time_off_type_id', 'ids:array_agg(id)'], + ['time_off_type_id'], + ) + accruals_dict = {res['time_off_type_id'][0]: res['ids'] for res in accruals_read_group} for allocation in self: allocation.number_of_days = allocation.number_of_days_display if allocation.type_request_unit == 'hour': allocation.number_of_days = allocation.number_of_hours_display / (allocation.employee_id.sudo().resource_calendar_id.hours_per_day or HOURS_PER_DAY) - - # set default values - if not allocation.interval_number and not allocation._origin.interval_number: - allocation.interval_number = 1 - if not allocation.number_per_interval and not allocation._origin.number_per_interval: - allocation.number_per_interval = 1 - if not allocation.unit_per_interval and not allocation._origin.unit_per_interval: - allocation.unit_per_interval = 'hours' - if not allocation.interval_unit and not allocation._origin.interval_unit: - allocation.interval_unit = 'weeks' - - if allocation.holiday_status_id.validity_stop and allocation.date_to: - new_date_to = datetime.combine(allocation.holiday_status_id.validity_stop, time.max) - if new_date_to < allocation.date_to: - allocation.date_to = new_date_to - - if allocation.allocation_type == 'accrual': - if allocation.holiday_status_id.request_unit == 'hour': - allocation.unit_per_interval = 'hours' + if allocation.accrual_plan_id.time_off_type_id != allocation.holiday_status_id: + allocation.accrual_plan_id = False + if allocation.allocation_type == 'accrual' and not allocation.accrual_plan_id: + accrual_plan = None + if allocation.holiday_status_id: + accrual_plan = accruals_dict.get(allocation.holiday_status_id.id, [False])[0] else: - allocation.unit_per_interval = 'days' + if not default_accrual_plan: # fetch the first accrual plan only when we need + default_accrual_plan = self.env['hr.leave.accrual.plan'].search([], limit=1) + accrual_plan = default_accrual_plan + allocation.accrual_plan_id = accrual_plan + + #TODO: remove after impl prorata in _process_accrual_plan_level + def _compute_accrual_days(self, first_day, start_date, end_date): + """ + Increase the number of days depending on the period, the start date of the employee and how many days he worked according to his + work entry. + This method takes also in account if the employee started during the period to apply a prorata + """ + for allocation in self.filtered(lambda i: i.employee_id and i.accrual_plan_id): + start_date_prorata = 1.0 + if first_day > start_date: + # The employee was created during the period after the start_date + # This prorata depends on the number of days worked during this period + worked_days = end_date - first_day + period_days = end_date - start_date + start_date_prorata = worked_days.days / period_days.days + # We calculate from the first_day to end_date + start_date = first_day + if end_date <= first_day or end_date < datetime.combine(allocation.date_from, time(0, 0, 0)): + # Stop the calculation if + # * the user was created after the period + # * the period ends before the allocation is valid + return + # The prorata depends if the employee took holidays during the period. + # Unpaid holidays and work_entry_type.leave_right = False decrease the amount of holidays + if allocation.accrual_level_id.is_based_on_worked_time: + worked = allocation.employee_id._get_work_days_data_batch(start_date, end_date, calendar=allocation.employee_id.resource_calendar_id)\ + [allocation.employee_id.id]['days'] + left = allocation.employee_id.sudo()._get_leave_days_data_batch(start_date, end_date, + domain=[('time_type', '=', 'leave')])[allocation.employee_id.id]['days'] + work_entry_prorata = worked / (left + worked) if worked else 0 + # Calculate the number of days that could be added + added_value = work_entry_prorata * start_date_prorata * allocation.accrual_level_id.added_value else: - allocation.interval_number = 1 - allocation.interval_unit = 'weeks' - allocation.number_per_interval = 1 - allocation.unit_per_interval = 'hours' + added_value = allocation.accrual_level_id.added_value + number_of_days = min(allocation.number_of_days + added_value, allocation.accrual_level_id.maximum_leave) + allocation.write({'number_of_days': number_of_days}) + # Do not send a message if the maximum number of days is already reached + + def _end_of_year_accrual(self): + # to override in payroll + today = fields.Date.today() + for allocation in self: + current_level = allocation._get_current_accrual_plan_level_id(today) + if current_level and current_level.action_with_unused_accruals == 'lost': + # Allocations are lost but number_of_days should not be lower than leaves_taken + allocation.write({'number_of_days': allocation.leaves_taken, 'lastcall': today, 'nextcall': False}) + + def _get_current_accrual_plan_level_id(self, date): + """ + Returns the accrual_plan_level for the given date of the record's accrual plan + """ + self.ensure_one() + if not self.accrual_plan_id.level_ids: + return False + # Sort by sequence which should be equivalent to the level + level_ids = self.accrual_plan_id.level_ids.sorted('sequence') + current_level = False + current_level_idx = -1 + for idx, level in enumerate(level_ids): + if date >= self.date_from + get_timedelta(level.start_count, level.start_type): + current_level = level + current_level_idx = idx + # If transition_mode is set to `immediately` or we are currently on the first level + # the current_level is simply the first level in the list. + if current_level_idx <= 0 or self.accrual_plan_id.transition_mode == "immediately": + return current_level + # In this case we have to verify that the 'previous level' is not the current one due to `end_of_accrual` + level_start_date = self.date_from + get_timedelta(current_level.start_count, current_level.start_type) + previous_level = level_ids[current_level_idx - 1] + # If the next date from the current level's start date is before the last call of the previous level + # return the previous level + if current_level._get_next_date(level_start_date) < previous_level._get_next_date(level_start_date): + return previous_level + return current_level + + def _get_first_accrual_plan_level_start_date(self): + self.ensure_one() + level = self.accrual_plan_id.level_ids.sorted('sequence')[0] + if not level: + return False + return self.date_from + get_timedelta(level.start_count, level.start_type) + + def _process_accrual_plan_level(self, level, start_date, end_date): + """ + Returns the added days for that level + """ + self.ensure_one() + #TODO: ask gmf about prorata stuff see `_compute_accrual_days` + if level.is_based_on_worked_time: + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\ + [self.employee_id.id]['days'] + left = self.employee_id.sudo()._get_leave_days_data_batch(start_dt, end_dt, + domain=[('time_type', '=', 'leave')])[self.employee_id.id]['days'] + work_entry_prorata = worked / (left + worked) if worked else 0 + added_value = work_entry_prorata * level.added_value + else: + added_value = level.added_value + # Convert time in hours to time in days in case the level is encoded in hours + if level.added_value_type == 'hours': + added_value = added_value / (self.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY) + return added_value + + def _process_accrual_plans(self): + """ + This method is part of the cron's process. + The goal of this method is to retroactively apply accrual plan levels and progress from nextcall to today + """ + today = fields.Date.today() + for allocation in self: + if not allocation.nextcall: + current_level = allocation._get_current_accrual_plan_level_id(today) + if not current_level: + # Accrual plan is not configured properly or has not started + continue + allocation.lastcall = max(allocation.lastcall, allocation.date_from + get_timedelta(current_level.start_count, current_level.start_type)) + allocation.nextcall = current_level._get_next_date(allocation.lastcall) + days_added_per_level = defaultdict(lambda: 0) + while allocation.nextcall <= today: + current_level = allocation._get_current_accrual_plan_level_id(allocation.nextcall) + nextcall = current_level._get_next_date(allocation.nextcall) + days_added_per_level[current_level] += allocation._process_accrual_plan_level(current_level, allocation.lastcall, allocation.nextcall) + allocation.lastcall = allocation.nextcall + allocation.nextcall = nextcall + if days_added_per_level: + number_of_days_to_add = 0 + for value in days_added_per_level.values(): + number_of_days_to_add += value + # Let's assume the limit of the last level is the correct one + allocation.write({'number_of_days': min(allocation.number_of_days + number_of_days_to_add, current_level.maximum_leave)}) + + @api.model + def _update_accrual(self): + """ + Method called by the cron task in order to increment the number_of_days when + necessary. + """ + # Get the current date to determine the start and end of the accrual period + today = datetime.combine(fields.Date.today(), time(0, 0, 0)) + if today.day == 1 and today.month == 1: + end_of_year_allocations = self.search( + [('allocation_type', '=', 'accrual'), ('state', '=', 'validate'), ('accrual_plan_id', '!=', False), ('employee_id', '!=', False), + '|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now())]) + end_of_year_allocations._end_of_year_accrual() + end_of_year_allocations.flush() + allocations = self.search( + [('allocation_type', '=', 'accrual'), ('state', '=', 'validate'), ('accrual_plan_id', '!=', False), ('employee_id', '!=', False), + '|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()), + '|', ('nextcall', '=', False), ('nextcall', '<=', today)]) + allocations._process_accrual_plans() #################################################### # ORM Overrides methods @@ -402,8 +488,10 @@ class HolidaysAllocation(models.Model): target = allocation.department_id.name elif allocation.holiday_type == 'category': target = allocation.category_id.name + elif allocation.employee_id: + target = allocation.employee_id.name else: - target = allocation.employee_id.sudo().name + target = ', '.join(allocation.employee_ids.sudo().mapped('name')) res.append( (allocation.id, @@ -421,20 +509,6 @@ class HolidaysAllocation(models.Model): if employee.user_id: self.message_subscribe(partner_ids=employee.user_id.partner_id.ids) - @api.constrains('holiday_status_id') - def _check_leave_type_validity(self): - for allocation in self: - if allocation.holiday_status_id.validity_stop: - vstop = allocation.holiday_status_id.validity_stop - today = fields.Date.today() - - if vstop < today: - raise ValidationError(_( - 'You can allocate %(allocation_type)s only before %(date)s.', - allocation_type=allocation.holiday_status_id.display_name, - date=allocation.holiday_status_id.validity_stop - )) - @api.model_create_multi def create(self, vals_list): """ Override to avoid automatic logging of creation """ @@ -442,6 +516,9 @@ class HolidaysAllocation(models.Model): employee_id = values.get('employee_id', False) if not values.get('department_id'): values.update({'department_id': self.env['hr.employee'].browse(employee_id).department_id.id}) + # default `lastcall` to `nextcall` + if 'date_from' in values and 'lastcall' not in values: + values['lastcall'] = values['date_from'] holidays = super(HolidaysAllocation, self.with_context(mail_create_nosubscribe=True)).create(vals_list) for holiday in holidays: partners_to_subscribe = set() @@ -453,6 +530,8 @@ class HolidaysAllocation(models.Model): holiday.message_subscribe(partner_ids=tuple(partners_to_subscribe)) if not self._context.get('import_file'): holiday.activity_update() + if holiday.validation_type == 'no': + holiday.state = 'validate' return holidays def write(self, values): @@ -476,9 +555,9 @@ class HolidaysAllocation(models.Model): # Business methods #################################################### - def _prepare_holiday_values(self, employee): + def _prepare_holiday_values(self, employees): self.ensure_one() - values = { + return [{ 'name': self.name, 'holiday_type': 'employee', 'holiday_status_id': self.holiday_status_id.id, @@ -486,23 +565,19 @@ class HolidaysAllocation(models.Model): 'number_of_days': self.number_of_days, 'parent_id': self.id, 'employee_id': employee.id, + 'state': 'confirm', 'allocation_type': self.allocation_type, 'date_from': self.date_from, 'date_to': self.date_to, - 'interval_unit': self.interval_unit, - 'interval_number': self.interval_number, - 'number_per_interval': self.number_per_interval, - 'unit_per_interval': self.unit_per_interval, - } - return values + 'accrual_plan_id': self.accrual_plan_id.id, + } for employee in employees] def action_draft(self): if any(holiday.state not in ['confirm', 'refuse'] for holiday in self): raise UserError(_('Allocation request state must be "Refused" or "To Approve" in order to be reset to Draft.')) self.write({ 'state': 'draft', - 'first_approver_id': False, - 'second_approver_id': False, + 'approver_id': False, }) linked_requests = self.mapped('linked_request_ids') if linked_requests: @@ -518,29 +593,14 @@ class HolidaysAllocation(models.Model): self.activity_update() return res - def action_approve(self): - # if validation_type == 'both': this method is the first approval approval - # if validation_type != 'both': this method calls action_validate() below - if any(holiday.state != 'confirm' for holiday in self): - raise UserError(_('Allocation request must be confirmed ("To Approve") in order to approve it.')) - - current_employee = self.env.user.employee_id - - self.filtered(lambda hol: hol.validation_type == 'both').write({'state': 'validate1', 'first_approver_id': current_employee.id}) - self.filtered(lambda hol: not hol.validation_type == 'both').action_validate() - self.activity_update() - def action_validate(self): current_employee = self.env.user.employee_id for holiday in self: - if holiday.state not in ['confirm', 'validate1']: + if holiday.state != 'confirm': raise UserError(_('Allocation request must be confirmed in order to approve it.')) holiday.write({'state': 'validate'}) - if holiday.validation_type == 'both': - holiday.write({'second_approver_id': current_employee.id}) - else: - holiday.write({'first_approver_id': current_employee.id}) + holiday.write({'approver_id': current_employee.id}) holiday._action_validate_create_childs() self.activity_update() @@ -548,22 +608,26 @@ class HolidaysAllocation(models.Model): def _action_validate_create_childs(self): childs = self.env['hr.leave.allocation'] - if self.state == 'validate' and self.holiday_type in ['category', 'department', 'company']: - if self.holiday_type == 'category': + # In the case we are in holiday_type `employee` and there is only one employee we can keep the same allocation + # Otherwise we do need to create an allocation for all employees to have a behaviour that is in line + # with the other holiday_type + if self.state == 'validate' and (self.holiday_type in ['category', 'department', 'company'] or + (self.holiday_type == 'employee' and len(self.employee_ids) > 1)): + if self.holiday_type == 'employee': + employees = self.employee_ids + elif self.holiday_type == 'category': employees = self.category_id.employee_ids elif self.holiday_type == 'department': employees = self.department_id.member_ids else: employees = self.env['hr.employee'].search([('company_id', '=', self.mode_company_id.id)]) - for employee in employees: - childs += self.with_context( - mail_notify_force_send=False, - mail_activity_automation_skip=True - ).create(self._prepare_holiday_values(employee)) - # TODO is it necessary to interleave the calls? - childs.action_approve() - if childs and self.validation_type == 'both': + allocation_create_vals = self._prepare_holiday_values(employees) + childs += self.with_context( + mail_notify_force_send=False, + mail_activity_automation_skip=True + ).create(allocation_create_vals) + if childs and self.validation_type != 'no': childs.action_validate() return childs @@ -572,9 +636,7 @@ class HolidaysAllocation(models.Model): if any(holiday.state not in ['confirm', 'validate', 'validate1'] for holiday in self): raise UserError(_('Allocation request must be confirmed or validated in order to refuse it.')) - validated_holidays = self.filtered(lambda hol: hol.state == 'validate1') - validated_holidays.write({'state': 'refuse', 'first_approver_id': current_employee.id}) - (self - validated_holidays).write({'state': 'refuse', 'second_approver_id': current_employee.id}) + self.write({'state': 'refuse', 'approver_id': current_employee.id}) # If a category that created several holidays, cancel all related linked_requests = self.mapped('linked_request_ids') if linked_requests: @@ -622,6 +684,13 @@ class HolidaysAllocation(models.Model): if not is_officer: raise UserError(_('Only a Time off Approver can apply the second approval on allocation requests.')) + @api.onchange('allocation_type') + def _onchange_allocation_type(self): + if self.allocation_type == 'accrual': + self.number_of_days = 0.0 + elif not self.number_of_days_display: + self.number_of_days = 1.0 + # ------------------------------------------------------------ # Activity methods # ------------------------------------------------------------ diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index e6f6ab72ee5..5063168e76b 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -9,7 +9,6 @@ import logging from collections import defaultdict from odoo import api, fields, models -from odoo.exceptions import ValidationError from odoo.osv import expression from odoo.tools.translate import _ from odoo.tools.float_utils import float_round @@ -26,10 +25,9 @@ class HolidaysType(models.Model): def _model_sorting_key(self, leave_type): remaining = leave_type.virtual_remaining_leaves > 0 taken = leave_type.leaves_taken > 0 - return -1*leave_type.sequence, leave_type.allocation_type == 'fixed' and remaining, leave_type.allocation_type == 'fixed_allocation' and remaining, taken + return -1*leave_type.sequence, leave_type.employee_requests == 'no' and remaining, leave_type.employee_requests == 'yes' and remaining, taken name = fields.Char('Time Off Type', required=True, translate=True) - code = fields.Char('Code') sequence = fields.Integer(default=100, help='The type with the smallest sequence is the default value in time off request') create_calendar_meeting = fields.Boolean(string="Display Time Off in Calendar", default=True) @@ -50,7 +48,9 @@ class HolidaysType(models.Model): ('lavender', 'Lavender'), ('wheat', 'Wheat'), ('ivory', 'Ivory')], string='Color in Report', required=True, default='red', - help='This color will be used in the time off summary located in Reporting > Time off by Department.') + help='This color will be used in the time off summary located in Reporting > Time off by Department.') + color = fields.Integer(string='Color', help="The color selected here will be used in every screen with the time off type.") + icon_id = fields.Many2one('ir.attachment', string='Cover Image', domain="[('res_model', '=', 'hr.leave.type'), ('res_field', '=', 'icon_id')]") active = fields.Boolean('Active', default=True, help="If the active field is set to false, it will allow you to hide the time off type without removing it.") max_leaves = fields.Float(compute='_compute_leaves', string='Maximum Allowed', search='_search_max_leaves', @@ -72,70 +72,86 @@ class HolidaysType(models.Model): group_days_leave = fields.Float( compute='_compute_group_days_leave', string='Group Time Off') company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) - responsible_id = fields.Many2one('res.users', 'Responsible', + responsible_id = fields.Many2one( + 'res.users', 'Responsible Time Off Officer', domain=lambda self: [('groups_id', 'in', self.env.ref('hr_holidays.group_hr_holidays_user').id)], - help="This user will be responsible for approving this type of time off. " - "This is only used when validation is 'By Time Off Officer' or 'By Employee's Manager and Time Off Officer'",) + help="Choose the Time Off Officer who will be notified to approve allocation or Time Off request") leave_validation_type = fields.Selection([ ('no_validation', 'No Validation'), ('hr', 'By Time Off Officer'), ('manager', "By Employee's Approver"), ('both', "By Employee's Approver and Time Off Officer")], default='hr', string='Leave Validation') + requires_allocation = fields.Selection([ + ('yes', 'Yes'), + ('no', 'No Limit')], default="yes", required=True, string='Requires allocation') + employee_requests = fields.Selection([ + ('yes', 'Extra Days Requests Allowed'), + ('no', 'Not Allowed')], default="no", required=True, string="Employee Requests") allocation_validation_type = fields.Selection([ - ('hr', 'By Time Off Officer'), - ('manager', "By Employee's Approver"), - ('both', "By Employee's Approver and Time Off Officer")], default='manager', string='Allocation Validation') - allocation_type = fields.Selection([ - ('no', 'No Limit'), - ('fixed_allocation', 'Allow Employees Requests'), - ('fixed', 'Set by Time Off Officer')], - default='no', string='Mode', - help='\tNo Limit: no allocation by default, users can freely request time off; ' - '\tAllow Employees Requests: allocated by HR and users can request time off and allocations; ' - '\tSet by Time Off Officer: allocated by HR and cannot be bypassed; users can request time off;') - validity_start = fields.Date("From", - help='Adding validity to types of time off so that it cannot be selected outside this time period') - validity_stop = fields.Date("To") - valid = fields.Boolean(compute='_compute_valid', search='_search_valid', help='This indicates if it is still possible to use this type of leave') + ('no', 'No validation needed'), + ('officer', 'Approved by Time Off Officer'), + ('set', "Set by Time Off Officer")], default='officer', string='Approval') + has_valid_allocation = fields.Boolean(compute='_compute_valid', search='_search_valid', help='This indicates if it is still possible to use this type of leave') time_type = fields.Selection([('leave', 'Time Off'), ('other', 'Other')], default='leave', string="Kind of Leave", help="Whether this should be computed as a holiday or as work time (eg: formation)") request_unit = fields.Selection([ - ('day', 'Day'), ('half_day', 'Half Day'), ('hour', 'Hours')], - default='day', string='Take Time Off in', required=True) + ('day', 'Day'), + ('half_day', 'Half Day'), + ('hour', 'Hours')], default='day', string='Take Time Off in', required=True) unpaid = fields.Boolean('Is Unpaid', default=False) leave_notif_subtype_id = fields.Many2one('mail.message.subtype', string='Time Off Notification Subtype', default=lambda self: self.env.ref('hr_holidays.mt_leave', raise_if_not_found=False)) allocation_notif_subtype_id = fields.Many2one('mail.message.subtype', string='Allocation Notification Subtype', default=lambda self: self.env.ref('hr_holidays.mt_leave_allocation', raise_if_not_found=False)) support_document = fields.Boolean(string='Supporting Document') + accruals_ids = fields.One2many('hr.leave.accrual.plan', 'time_off_type_id') + accrual_count = fields.Float(compute="_compute_accrual_count", string="Accruals count") - @api.constrains('validity_start', 'validity_stop') - def _check_validity_dates(self): - for leave_type in self: - if leave_type.validity_start and leave_type.validity_stop and \ - leave_type.validity_start > leave_type.validity_stop: - raise ValidationError(_("End of validity period should be greater than start of validity period")) - - @api.depends('validity_start', 'validity_stop') - def _compute_valid(self): - dt = self._context.get('default_date_from') or fields.Date.context_today(self) - - for holiday_type in self: - if holiday_type.validity_start and holiday_type.validity_stop: - holiday_type.valid = ((dt < holiday_type.validity_stop) and (dt > holiday_type.validity_start)) - elif holiday_type.validity_start and (dt > holiday_type.validity_start): - holiday_type.valid = False - else: - holiday_type.valid = True + @api.model def _search_valid(self, operator, value): - dt = self._context.get('default_date_from', False) + """ Returns leave_type ids for which a valid allocation exists + or that don't need an allocation + return [('id', domain_operator, [x['id'] for x in res])] + """ + date_to = self._context.get('default_date_from') or fields.Date.today().strftime('%Y-1-1') + date_from = self._context.get('default_date_to') or fields.Date.today().strftime('%Y-12-31') - if not dt: - return [] - signs = ['>=', '<='] if operator == '=' else ['<=', '>='] + if not isinstance(value, bool): + raise ValueError('Invalid value: %s' % (value)) + if operator not in ['=', '!=']: + raise ValueError('Invalid operator: %s' % (operator)) + new_operator = 'in' if operator == '=' else 'not in' - return ['|', ('validity_stop', operator, False), '&', - ('validity_stop', signs[0] if value else signs[1], dt), - ('validity_start', signs[1] if value else signs[0], dt)] + query = ''' + SELECT + holiday_status_id + FROM + hr_leave_allocation alloc + WHERE + alloc.date_to >= %s OR alloc.date_to IS NULL AND + alloc.date_from <= %s + ''' + + self._cr.execute(query, (date_to, date_from)) + + return [('id', new_operator, [x['holiday_status_id'] for x in self._cr.dictfetchall()])] + + + @api.depends('requires_allocation') + def _compute_valid(self): + date_to = self._context.get('default_date_to', fields.Datetime.today()) + date_from = self._context.get('default_date_from', fields.Datetime.today()) + for holiday_type in self: + if holiday_type.requires_allocation: + allocation = self.env['hr.leave.allocation'].search([ + ('holiday_status_id', '=', holiday_type.id), + '|', + ('date_to', '>=', date_to), + '&', + ('date_to', '=', False), + ('date_from', '<=', date_from)]) + holiday_type.has_valid_allocation = bool(allocation) + else: + holiday_type.has_valid_allocation = True def _search_max_leaves(self, operator, value): value = float(value) @@ -172,7 +188,7 @@ class HolidaysType(models.Model): valid_leave_types = self.env['hr.leave.type'] for leave_type in leave_types: - if leave_type.allocation_type != 'no': + if leave_type.requires_allocation == "yes": if operator == '>' and leave_type.virtual_remaining_leaves > value: valid_leave_types |= leave_type elif operator == '<' and leave_type.virtual_remaining_leaves < value: @@ -250,8 +266,7 @@ class HolidaysType(models.Model): @api.model def get_days_all_request(self): - leave_types = sorted(self.search([]).filtered(lambda x: ((not x.validity_stop or x.validity_stop >= fields.Date.today()) - and (x.virtual_remaining_leaves or x.max_leaves))), key=self._model_sorting_key, reverse=True) + leave_types = sorted(self.search([]).filtered(lambda x: ((x.virtual_remaining_leaves or x.max_leaves))), key=self._model_sorting_key, reverse=True) return [lt._get_days_request() for lt in leave_types] def _get_days_request(self): @@ -263,7 +278,8 @@ class HolidaysType(models.Model): 'leaves_taken': ('%.2f' % self.leaves_taken).rstrip('0').rstrip('.'), 'virtual_leaves_taken': ('%.2f' % self.virtual_leaves_taken).rstrip('0').rstrip('.'), 'request_unit': self.request_unit, - }, self.allocation_type, self.validity_stop, self.id) + 'icon': self.sudo().icon_id.url, + }, self.requires_allocation, self.id) def _get_contextual_employee_id(self): if 'employee_id' in self._context: @@ -290,18 +306,16 @@ class HolidaysType(models.Model): holiday_status.virtual_leaves_taken = result.get('virtual_leaves_taken', 0) def _compute_group_days_allocation(self): + date_from = fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)) domain = [ ('holiday_status_id', 'in', self.ids), - ('holiday_type', '!=', 'employee'), ('state', '=', 'validate'), - ] - domain2 = [ '|', - ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))), + ('date_from', '>=', date_from), ('date_from', '=', False), ] grouped_res = self.env['hr.leave.allocation'].read_group( - expression.AND([domain, domain2]), + domain, ['holiday_status_id', 'number_of_days'], ['holiday_status_id'], ) @@ -311,7 +325,7 @@ class HolidaysType(models.Model): def _compute_group_days_leave(self): grouped_res = self.env['hr.leave'].read_group( - [('holiday_status_id', 'in', self.ids), ('holiday_type', '=', 'employee'), ('state', '=', 'validate'), + [('holiday_status_id', 'in', self.ids), ('state', '=', 'validate'), ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)))], ['holiday_status_id'], ['holiday_status_id'], @@ -320,6 +334,12 @@ class HolidaysType(models.Model): for allocation in self: allocation.group_days_leave = grouped_dict.get(allocation.id, 0) + def _compute_accrual_count(self): + accrual_allocations = self.env['hr.leave.allocation'].read_group([('employee_id', '!=', False), ('accrual_plan_id', '!=', False), ('state', '=', 'validate')], ['id'], ['holiday_status_id']) + mapped_data = dict((data['holiday_status_id'][0], data['holiday_status_id_count']) for data in accrual_allocations) + for leave_type in self: + leave_type.accrual_count = mapped_data.get(leave_type.id, 0) + def name_get(self): if not self._context.get('employee_id'): # leave counts is based on employee_id, would be inaccurate if not based on correct employee @@ -327,7 +347,7 @@ class HolidaysType(models.Model): res = [] for record in self: name = record.name - if record.allocation_type != 'no': + if record.requires_allocation == "yes": name = "%(name)s (%(count)s)" % { 'name': name, 'count': _('%g remaining out of %g') % ( @@ -362,16 +382,14 @@ class HolidaysType(models.Model): def action_see_days_allocated(self): self.ensure_one() action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.hr_leave_allocation_action_all") - domain = [ + date_from = fields.Datetime.to_string( + datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)) + action['domain'] = [ ('holiday_status_id', 'in', self.ids), - ('holiday_type', '!=', 'employee'), - ] - domain2 = [ '|', - ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))), + ('date_from', '>=', date_from), ('date_from', '=', False), ] - action['domain'] = expression.AND([domain, domain2]) action['context'] = { 'default_holiday_type': 'department', 'default_holiday_status_id': self.ids[0], @@ -389,3 +407,10 @@ class HolidaysType(models.Model): 'default_holiday_status_id': self.ids[0], } return action + + def action_see_accrual_plans(self): + self.ensure_one() + action = self.env["ir.actions.actions"]._for_xml_id("hr.holidays.open_view_accrual_plans") + action['context'] = { + 'default_time_off_type_id': self.id, + } diff --git a/addons/hr_holidays/report/__init__.py b/addons/hr_holidays/report/__init__.py index c59b1e68b62..aafcb0afab5 100644 --- a/addons/hr_holidays/report/__init__.py +++ b/addons/hr_holidays/report/__init__.py @@ -4,3 +4,4 @@ from . import holidays_summary_report from . import hr_leave_report from . import hr_leave_report_calendar +from . import hr_leave_employee_type_report diff --git a/addons/hr_holidays/report/hr_leave_employee_type_report.py b/addons/hr_holidays/report/hr_leave_employee_type_report.py new file mode 100644 index 00000000000..549f5ef5389 --- /dev/null +++ b/addons/hr_holidays/report/hr_leave_employee_type_report.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, tools, _ + + +class LeaveReport(models.Model): + _name = "hr.leave.employee.type.report" + _description = 'Time Off Summary / Report' + _auto = False + _order = "date_from DESC, employee_id" + + employee_id = fields.Many2one('hr.employee', string="Employee", readonly=True) + active_employee = fields.Boolean(related='employee_id.active', readonly=True) + number_of_days = fields.Float('Number of Days', readonly=True, group_operator="sum") + department_id = fields.Many2one('hr.department', string='Department', readonly=True) + leave_type = fields.Many2one("hr.leave.type", string="Leave Type", readonly=True) + holiday_status = fields.Selection([ + ('taken', 'Taken'), #taken = validated + ('left', 'Left'), + ('planned', 'Planned') + ]) + state = fields.Selection([ + ('draft', 'To Submit'), + ('cancel', 'Cancelled'), + ('confirm', 'To Approve'), + ('refuse', 'Refused'), + ('validate1', 'Second Approval'), + ('validate', 'Approved') + ], string='Status', readonly=True) + date_from = fields.Datetime('Start Date', readonly=True) + date_to = fields.Datetime('End Date', readonly=True) + company_id = fields.Many2one('res.company', string="Company", readonly=True) + + def init(self): + tools.drop_view_if_exists(self._cr, 'hr_leave_employee_type_report') + + self._cr.execute(""" + CREATE or REPLACE view hr_leave_employee_type_report as ( + SELECT row_number() over(ORDER BY leaves.employee_id) as id, + leaves.employee_id as employee_id, + leaves.number_of_days as number_of_days, + leaves.department_id as department_id, + leaves.leave_type as leave_type, + leaves.holiday_status as holiday_status, + leaves.state as state, + leaves.date_from as date_from, + leaves.date_to as date_to, + leaves.company_id as company_id + FROM (SELECT + allocation.employee_id as employee_id, + CASE + WHEN request.number_of_days > 0 THEN allocation.number_of_days - request.number_of_days + ELSE allocation.number_of_days + END as number_of_days, + allocation.department_id as department_id, + allocation.holiday_status_id as leave_type, + allocation.state as state, + allocation.date_from as date_from, + allocation.date_to as date_to, + 'left' as holiday_status, + allocation.employee_company_id as company_id + FROM hr_leave_allocation as allocation + LEFT JOIN + (SELECT holiday_status_id, employee_id, sum(number_of_days) as number_of_days + FROM hr_leave GROUP BY holiday_status_id, employee_id) request + on (allocation.employee_id=request.employee_id and allocation.holiday_status_id = request.holiday_status_id) + UNION ALL SELECT + request.employee_id as employee_id, + request.number_of_days as number_of_days, + request.department_id as department_id, + request.holiday_status_id as leave_type, + request.state as state, + request.date_from as date_from, + request.date_to as date_to, + CASE + WHEN request.state IN ('validate1', 'validate') THEN 'taken' + WHEN request.state = 'confirm' THEN 'planned' + END as holiday_status, + request.employee_company_id as company_id + FROM hr_leave as request) leaves + ); + """) + + @api.model + def action_time_off_analysis(self): + domain = [] + if self.env.context.get('active_ids'): + domain = [('employee_id', 'in', self.env.context.get('active_ids', []))] + + return { + 'name': _('Time Off Analysis'), + 'type': 'ir.actions.act_window', + 'res_model': 'hr.leave.employee.type.report', + 'view_mode': 'pivot', + 'search_view_id': [self.env.ref('hr_holidays.view_search_hr_holidays_employee_type_report').id], + 'domain': domain, + 'context': { + 'search_default_year': True, + 'search_default_company': True, + 'search_default_employee': True, + 'group_expand': True, + } + } diff --git a/addons/hr_holidays/report/hr_leave_employee_type_report.xml b/addons/hr_holidays/report/hr_leave_employee_type_report.xml new file mode 100644 index 00000000000..15ddf1b329e --- /dev/null +++ b/addons/hr_holidays/report/hr_leave_employee_type_report.xml @@ -0,0 +1,42 @@ + + + + hr.holidays.filter + hr.leave.employee.type.report + + + + + + + + + + + + + + hr.leave.employee.type.report.view.pivot + hr.leave.employee.type.report + + + + + + + + + + + + Time off Analysis by Employee and Time Off Type + + + code + + + action = model.action_time_off_analysis() + + + + diff --git a/addons/hr_holidays/report/hr_leave_report.py b/addons/hr_holidays/report/hr_leave_report.py index 7608068d846..28b6c43356f 100644 --- a/addons/hr_holidays/report/hr_leave_report.py +++ b/addons/hr_holidays/report/hr_leave_report.py @@ -36,7 +36,7 @@ class LeaveReport(models.Model): ], string='Allocation Mode', readonly=True) date_from = fields.Datetime('Start Date', readonly=True) date_to = fields.Datetime('End Date', readonly=True) - payslip_status = fields.Boolean('Reported in last payslips', readonly=True) + company_id = fields.Many2one('res.company', string="Company", readonly=True) def init(self): tools.drop_view_if_exists(self._cr, 'hr_leave_report') @@ -49,7 +49,7 @@ class LeaveReport(models.Model): leaves.category_id as category_id, leaves.department_id as department_id, leaves.holiday_status_id as holiday_status_id, leaves.state as state, leaves.holiday_type as holiday_type, leaves.date_from as date_from, - leaves.date_to as date_to, leaves.payslip_status as payslip_status + leaves.date_to as date_to, leaves.company_id from (select allocation.employee_id as employee_id, allocation.private_name as name, @@ -61,8 +61,8 @@ class LeaveReport(models.Model): allocation.holiday_type, null as date_from, null as date_to, - FALSE as payslip_status, - 'allocation' as leave_type + 'allocation' as leave_type, + allocation.employee_company_id as company_id from hr_leave_allocation as allocation union all select request.employee_id as employee_id, @@ -75,8 +75,8 @@ class LeaveReport(models.Model): request.holiday_type, request.date_from as date_from, request.date_to as date_to, - request.payslip_status as payslip_status, - 'request' as leave_type + 'request' as leave_type, + request.employee_company_id as company_id from hr_leave as request) leaves ); """) diff --git a/addons/hr_holidays/report/hr_leave_report_calendar.py b/addons/hr_holidays/report/hr_leave_report_calendar.py index fdd2839508e..79ae1dae7ec 100644 --- a/addons/hr_holidays/report/hr_leave_report_calendar.py +++ b/addons/hr_holidays/report/hr_leave_report_calendar.py @@ -19,6 +19,7 @@ class LeaveReportCalendar(models.Model): duration = fields.Float(string='Duration', readonly=True) employee_id = fields.Many2one('hr.employee', readonly=True) department_id = fields.Many2one('hr.department', readonly=True) + job_id = fields.Many2one('hr.job', readonly=True) company_id = fields.Many2one('res.company', readonly=True) state = fields.Selection([ ('draft', 'To Submit'), @@ -30,6 +31,7 @@ class LeaveReportCalendar(models.Model): ], readonly=True) is_hatched = fields.Boolean('Hatched', readonly=True) + is_striked = fields.Boolean('Striked', readonly=True) def init(self): tools.drop_view_if_exists(self._cr, 'hr_leave_report_calendar') @@ -43,11 +45,13 @@ class LeaveReportCalendar(models.Model): hl.state AS state, hl.department_id AS department_id, em.company_id AS company_id, + em.job_id AS job_id, CASE WHEN hl.holiday_type = 'employee' THEN rr.tz ELSE %s END AS tz, - state != 'validate' as is_hatched + state = 'refuse' as is_striked, + state not in ('validate', 'refuse') as is_hatched FROM hr_leave hl LEFT JOIN hr_employee em ON em.id = hl.employee_id diff --git a/addons/hr_holidays/report/hr_leave_report_calendar.xml b/addons/hr_holidays/report/hr_leave_report_calendar.xml index 54d58cd6480..341761d99ce 100644 --- a/addons/hr_holidays/report/hr_leave_report_calendar.xml +++ b/addons/hr_holidays/report/hr_leave_report_calendar.xml @@ -1,17 +1,5 @@ - - All Time Off - hr.leave.report.calendar - calendar - - - - [('employee_id.active','=',True)] - {'hide_employee_name': 1} - - hr.leave.report.calendar.view hr.leave.report.calendar @@ -47,17 +35,28 @@ + + + + + + + - + + All Time Off + hr.leave.report.calendar calendar + [('employee_id.active','=',True)] + {'hide_employee_name': 1} diff --git a/addons/hr_holidays/report/hr_leave_reports.xml b/addons/hr_holidays/report/hr_leave_reports.xml index bf2120b983c..432b2edec41 100644 --- a/addons/hr_holidays/report/hr_leave_reports.xml +++ b/addons/hr_holidays/report/hr_leave_reports.xml @@ -28,6 +28,7 @@ + @@ -52,58 +53,20 @@ - - - report.hr.holidays.report.leave_all.kanban + + report.hr.holidays.report.leave_all.pivot hr.leave.report - - + + + + + - - - -
-
- - - - -
- - - - -
-
- -
-
-
-
-
-
- - - - - - - - - -
from
to
-
-
- days -
-
-
-
-
-
+ +
diff --git a/addons/hr_holidays/security/hr_holidays_security.xml b/addons/hr_holidays/security/hr_holidays_security.xml index 30a8adb8e45..e065effb03e 100644 --- a/addons/hr_holidays/security/hr_holidays_security.xml +++ b/addons/hr_holidays/security/hr_holidays_security.xml @@ -12,7 +12,7 @@
- All Approver + Time Off Officer @@ -161,14 +161,15 @@ Allocations: base.group_user create/write [ - ('holiday_status_id.allocation_type', '=', 'fixed_allocation'), + ('holiday_status_id.requires_allocation', '=', 'yes'), + ('holiday_status_id.employee_requests', '=', 'yes'), ('holiday_type', '=', 'employee'), '|', '&', ('employee_id.user_id', '=', user.id), - ('state', 'not in', ['validate', 'validate1']), + ('state', '!=', 'validate'), '&', - ('validation_type', 'in', ['manager', 'both']), + ('validation_type', 'in', ['officer', 'set']), ('employee_id.leave_manager_id', '=', user.id), ] diff --git a/addons/hr_holidays/security/ir.model.access.csv b/addons/hr_holidays/security/ir.model.access.csv index bfc2e5fa19d..95236f4c0b4 100644 --- a/addons/hr_holidays/security/ir.model.access.csv +++ b/addons/hr_holidays/security/ir.model.access.csv @@ -16,3 +16,8 @@ access_calendar_attendee_hr_user,calendar.attendee.hr.user,calendar.model_calend access_mail_activity_type_holidays_manager,mail.activity.type.holidays.manager,mail.model_mail_activity_type,hr_holidays.group_hr_holidays_manager,1,1,1,1 access_hr_holidays_summary_employee,access.hr.holidays.summary.employee,model_hr_holidays_summary_employee,hr_holidays.group_hr_holidays_user,1,1,1,0 access_hr_leave_report_calendar,access_hr_leave_report_calendar,model_hr_leave_report_calendar,base.group_user,1,0,0,0 +access_hr_leave_employee_type_report,access_hr_leave_employee_type_report,model_hr_leave_employee_type_report,base.group_user,1,0,0,0 +access_hr_leave_accrual_plan_user,hr.leave.accrual.plan.user,model_hr_leave_accrual_plan,hr_holidays.group_hr_holidays_user,1,0,0,0 +access_hr_leave_accrual_level_user,hr.leave.accrual.level.user,model_hr_leave_accrual_level,hr_holidays.group_hr_holidays_user,1,0,0,0 +access_hr_leave_accrual_plan_manager,hr.leave.accrual.plan.manager,model_hr_leave_accrual_plan,hr_holidays.group_hr_holidays_manager,1,1,1,1 +access_hr_leave_accrual_level_manager,hr.leave.accrual.level.manager,model_hr_leave_accrual_level,hr_holidays.group_hr_holidays_manager,1,1,1,1 diff --git a/addons/hr_holidays/static/src/img/icons/Annual_Time_Off.svg b/addons/hr_holidays/static/src/img/icons/Annual_Time_Off.svg new file mode 100644 index 00000000000..635e671f2f2 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Annual_Time_Off.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Annual_Time_Off_2.svg b/addons/hr_holidays/static/src/img/icons/Annual_Time_Off_2.svg new file mode 100644 index 00000000000..950bec8ddfa --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Annual_Time_Off_2.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Annual_Time_Off_3.svg b/addons/hr_holidays/static/src/img/icons/Annual_Time_Off_3.svg new file mode 100644 index 00000000000..322adf39f98 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Annual_Time_Off_3.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Compensatory_Time_Off.svg b/addons/hr_holidays/static/src/img/icons/Compensatory_Time_Off.svg new file mode 100644 index 00000000000..89da61ee05c --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Compensatory_Time_Off.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Compensatory_Time_Off_2.svg b/addons/hr_holidays/static/src/img/icons/Compensatory_Time_Off_2.svg new file mode 100644 index 00000000000..315e777bf4c --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Compensatory_Time_Off_2.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Compensatory_Time_Off_3.svg b/addons/hr_holidays/static/src/img/icons/Compensatory_Time_Off_3.svg new file mode 100644 index 00000000000..b7d30cf84b5 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Compensatory_Time_Off_3.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Credit_Time.svg b/addons/hr_holidays/static/src/img/icons/Credit_Time.svg new file mode 100644 index 00000000000..5ea32df72f0 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Credit_Time.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Credit_Time_2.svg b/addons/hr_holidays/static/src/img/icons/Credit_Time_2.svg new file mode 100644 index 00000000000..454ba0ea918 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Credit_Time_2.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Extra_Time_Off.svg b/addons/hr_holidays/static/src/img/icons/Extra_Time_Off.svg new file mode 100644 index 00000000000..9c6004932f2 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Extra_Time_Off.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Extra_Time_Off_2.svg b/addons/hr_holidays/static/src/img/icons/Extra_Time_Off_2.svg new file mode 100644 index 00000000000..d2874f2988b --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Extra_Time_Off_2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Maternity_Time_Off.svg b/addons/hr_holidays/static/src/img/icons/Maternity_Time_Off.svg new file mode 100644 index 00000000000..640ea4234c1 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Maternity_Time_Off.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Maternity_Time_Off_2.svg b/addons/hr_holidays/static/src/img/icons/Maternity_Time_Off_2.svg new file mode 100644 index 00000000000..88a85f39130 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Maternity_Time_Off_2.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Maternity_Time_Off_3.svg b/addons/hr_holidays/static/src/img/icons/Maternity_Time_Off_3.svg new file mode 100644 index 00000000000..3d9488db857 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Maternity_Time_Off_3.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Paid_Time_Off.svg b/addons/hr_holidays/static/src/img/icons/Paid_Time_Off.svg new file mode 100644 index 00000000000..ecc9c806e86 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Paid_Time_Off.svg @@ -0,0 +1,15 @@ + + + diff --git a/addons/hr_holidays/static/src/img/icons/Paid_Time_Off_2.svg b/addons/hr_holidays/static/src/img/icons/Paid_Time_Off_2.svg new file mode 100644 index 00000000000..82e382aceac --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Paid_Time_Off_2.svg @@ -0,0 +1,15 @@ + + + diff --git a/addons/hr_holidays/static/src/img/icons/Paid_Time_Off_3.svg b/addons/hr_holidays/static/src/img/icons/Paid_Time_Off_3.svg new file mode 100644 index 00000000000..0b104f47e4b --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Paid_Time_Off_3.svg @@ -0,0 +1,12 @@ + + + diff --git a/addons/hr_holidays/static/src/img/icons/Parental_Time_Off.svg b/addons/hr_holidays/static/src/img/icons/Parental_Time_Off.svg new file mode 100644 index 00000000000..94738e4a172 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Parental_Time_Off.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Parental_Time_Off_2.svg b/addons/hr_holidays/static/src/img/icons/Parental_Time_Off_2.svg new file mode 100644 index 00000000000..53219853355 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Parental_Time_Off_2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Recovery_Bank_Holiday.svg b/addons/hr_holidays/static/src/img/icons/Recovery_Bank_Holiday.svg new file mode 100644 index 00000000000..627ff661771 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Recovery_Bank_Holiday.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Recovery_Bank_Holiday_2.svg b/addons/hr_holidays/static/src/img/icons/Recovery_Bank_Holiday_2.svg new file mode 100644 index 00000000000..24f285ec03d --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Recovery_Bank_Holiday_2.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Refused.svg b/addons/hr_holidays/static/src/img/icons/Refused.svg new file mode 100644 index 00000000000..5425b105e1c --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Refused.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/hr_holidays/static/src/img/icons/Sick_Time_Off.svg b/addons/hr_holidays/static/src/img/icons/Sick_Time_Off.svg new file mode 100644 index 00000000000..d848f464d0a --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Sick_Time_Off.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Sick_Time_Off_2.svg b/addons/hr_holidays/static/src/img/icons/Sick_Time_Off_2.svg new file mode 100644 index 00000000000..cf4cdc4b49e --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Sick_Time_Off_2.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Small_Unemployement.svg b/addons/hr_holidays/static/src/img/icons/Small_Unemployement.svg new file mode 100644 index 00000000000..9f3b1328725 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Small_Unemployement.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Small_Unemployement_2.svg b/addons/hr_holidays/static/src/img/icons/Small_Unemployement_2.svg new file mode 100644 index 00000000000..81896d90a21 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Small_Unemployement_2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Small_Unemployement_3.svg b/addons/hr_holidays/static/src/img/icons/Small_Unemployement_3.svg new file mode 100644 index 00000000000..db2578e9c43 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Small_Unemployement_3.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/To_Approve.svg b/addons/hr_holidays/static/src/img/icons/To_Approve.svg new file mode 100644 index 00000000000..69e5c7dd634 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/To_Approve.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/hr_holidays/static/src/img/icons/Training_Time_Off.svg b/addons/hr_holidays/static/src/img/icons/Training_Time_Off.svg new file mode 100644 index 00000000000..1bd009cfaac --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Training_Time_Off.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Training_Time_Off_2.svg b/addons/hr_holidays/static/src/img/icons/Training_Time_Off_2.svg new file mode 100644 index 00000000000..514bdb09193 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Training_Time_Off_2.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Unpaid_Time_Off.svg b/addons/hr_holidays/static/src/img/icons/Unpaid_Time_Off.svg new file mode 100644 index 00000000000..3da22e2f013 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Unpaid_Time_Off.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Unpaid_Time_Off_2.svg b/addons/hr_holidays/static/src/img/icons/Unpaid_Time_Off_2.svg new file mode 100644 index 00000000000..3523ee4c665 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Unpaid_Time_Off_2.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Unpredictable_Reasons.svg b/addons/hr_holidays/static/src/img/icons/Unpredictable_Reasons.svg new file mode 100644 index 00000000000..65395f6e198 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Unpredictable_Reasons.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Unpredictable_Reasons_2.svg b/addons/hr_holidays/static/src/img/icons/Unpredictable_Reasons_2.svg new file mode 100644 index 00000000000..b1990a03882 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Unpredictable_Reasons_2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Validated.svg b/addons/hr_holidays/static/src/img/icons/Validated.svg new file mode 100644 index 00000000000..445178b7d95 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Validated.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/hr_holidays/static/src/img/icons/Work_Accident_Time_Off.svg b/addons/hr_holidays/static/src/img/icons/Work_Accident_Time_Off.svg new file mode 100644 index 00000000000..0e6789f0e38 --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Work_Accident_Time_Off.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/img/icons/Work_Accident_Time_Off_2.svg b/addons/hr_holidays/static/src/img/icons/Work_Accident_Time_Off_2.svg new file mode 100644 index 00000000000..88fe00d9d2b --- /dev/null +++ b/addons/hr_holidays/static/src/img/icons/Work_Accident_Time_Off_2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/js/radio_image.js b/addons/hr_holidays/static/src/js/radio_image.js new file mode 100644 index 00000000000..5bf571614bf --- /dev/null +++ b/addons/hr_holidays/static/src/js/radio_image.js @@ -0,0 +1,37 @@ +/** @odoo-module **/ + +import { FieldRadio } from 'web.relational_fields'; +import FieldRegistry from 'web.field_registry'; + +import Core from 'web.core'; +const QWeb = Core.qweb; + +const FieldRadioImage = FieldRadio.extend({ + _render() { + const self = this; + let currentValue; + if (this.field.type === 'many2one') { + currentValue = this.value && this.value.data.id; + } else { + currentValue = this.value; + } + this.$el.empty(); + this.$el.attr('role', 'radiogroup') + .attr('aria-label', this.string); + _.each(this.values, function (value, index) { + if (self.mode === 'edit' || value[0] === currentValue) { + self.$el.append(QWeb.render('FieldRadioIcon.button', { + checked: value[0] === currentValue, + id: self.unique_id + '_' + value[0], + index: index, + name: self.unique_id, + value: value, + disabled: self.hasReadonlyModifier, + mode: self.mode, + })); + } + }); + } +}); + +FieldRegistry.add('radio_image', FieldRadioImage); diff --git a/addons/hr_holidays/static/src/js/time_off_calendar.js b/addons/hr_holidays/static/src/js/time_off_calendar.js index 9675e23c45e..92a88f8d91c 100644 --- a/addons/hr_holidays/static/src/js/time_off_calendar.js +++ b/addons/hr_holidays/static/src/js/time_off_calendar.js @@ -32,13 +32,6 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) { }, }); - var TimeoffCalendarModel = CalendarModel.extend({ - - _getFilterDomain: function() { - return this._super.apply(this, arguments).concat([["state", "!=", "refuse"]]); - }, - }); - var TimeOffCalendarController = CalendarController.extend({ events: _.extend({}, CalendarController.prototype.events, { 'click .btn-time-off': '_onNewTimeOff', @@ -69,7 +62,7 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) { $(QWeb.render('hr_holidays.dashboard.calendar.button', { time_off: _t('New Time Off'), - request: _t('New Allocation'), + request: _t('Allocation Request'), })).appendTo(this.$buttons); if ($node) { @@ -90,7 +83,7 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) { */ _onNewTimeOff: function () { var self = this; - + //use formViewId for this view? this.do_action('hr_holidays.hr_leave_action_my_request', { on_close: function () { self.reload(); @@ -118,9 +111,31 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) { } }); }, + + /** + * @override + */ + _setEventTitle: function () { + return _t('Time Off Request'); + }, }); var TimeOffPopoverRenderer = CalendarRenderer.extend({ + template: "TimeOff.CalendarView.extend", + /** + * We're overriding this to display the weeknumbers on the year view + * + * @override + * @private + */ + _getFullCalendarOptions: function () { + const oldOptions = this._super(...arguments); + // Parameters + oldOptions.views.dayGridYear.weekNumbers = true; + oldOptions.views.dayGridYear.weekNumbersWithinDays = false; + return oldOptions; + }, + config: _.extend({}, CalendarRenderer.prototype.config, { CalendarPopover: TimeOffCalendarPopover, }), @@ -147,20 +162,6 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) { var self = this; return this._super.apply(this, arguments).then(function () { self.$el.parent().find('.o_calendar_mini').hide(); - - // Check if there is a filter to display on the sidebar - // If there is no filter, hide the sidebar - const noFilters = !Object.values(self.state.filters).some(f => f.filters.length); - - // Remove the no data sidebar - self.$sidebar.find('#o_calendar_filter_no_data').remove(); - if (noFilters) { - // Show a special sidebar - self.$sidebar.html(QWeb.render('hr_holidays.calendar.sidebar.nofilter', { - title: 'Time Off Type', - description: '(no data)' - })); - } }); }, }); @@ -201,7 +202,7 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) { config: _.extend({}, CalendarView.prototype.config, { Controller: TimeOffCalendarController, Renderer: TimeOffCalendarRenderer, - Model: TimeoffCalendarModel, + Model: CalendarModel, }), }); @@ -210,6 +211,7 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) { */ var TimeOffCalendarAllView = CalendarView.extend({ config: _.extend({}, CalendarView.prototype.config, { + Controller: TimeOffCalendarController, Renderer: TimeOffPopoverRenderer, }), }); diff --git a/addons/hr_holidays/static/src/scss/accrual_plan_level.scss b/addons/hr_holidays/static/src/scss/accrual_plan_level.scss new file mode 100644 index 00000000000..ca723511e61 --- /dev/null +++ b/addons/hr_holidays/static/src/scss/accrual_plan_level.scss @@ -0,0 +1,107 @@ +$o-hr-holidays-border-color: gray('300'); + +.o_hr_holidays_hierarchy { + margin-left: -$o-horizontal-padding; + margin-right: -$o-horizontal-padding; + @include media-breakpoint-between(lg, xl, $o-extra-grid-breakpoints) { + margin-left: -$o-horizontal-padding*2; + margin-right: -$o-horizontal-padding*2; + } + margin-bottom:-24px; + padding: 10px 0px 24px 16px; + background-color: #F0F0F0; + box-shadow: 0px 1px 1px rgba(17, 17, 17, 0.23); + overflow-x: auto; + .o_hr_holidays_title { + padding: 0px 0px 0px 86px; + font-size: 18px; + } + + .o_hr_holidays_hierarchy_readonly { + padding: 40px 0px 0px 40px; + } + + .o_hr_holidays_plan_level_container { + .o-kanban-button-new { + padding: 2px 12px; + margin: 0px 0px 0px 44px; + border-radius: 25px; + } + + .o_kanban_view.o_kanban_ungrouped .o_kanban_record { + display: flex; + flex: 0 0 100%; + border: 0px; + padding: 0px 0px 0px 100px; + margin: 0px 0px 0px 0px; + background-color: transparent; + + // Timeline Border + &:before { + content: ''; + display: block; + @include o-position-absolute; + height: 100%; + margin-left: 8px; + border-left: 1px dashed darken($o-hr-holidays-border-color, 10%); + } + + // Whole record + .o_hr_holidays_body { + margin-left: 8px; + padding-top: 20px; + + // Left side 'Level' + .o_hr_holidays_timeline { + @include o-position-absolute($top: 32px, $left: 6px); + width: 90px; + padding: 3px 0px; + border-radius: 3px; + background-color: white; + box-shadow: 0 1px 2px rgba(0,0,0,.1); + } + + // Actual kanban card + .o_hr_holidays_card { + position: relative; + margin-left: 22px; + margin-right: 2px; + width: 500px; + border-radius: 3px; + background: white; + box-shadow: 0 1px 2px rgba(0,0,0,.1); + + // Triangle + &:before { + content: ''; + @include o-position-absolute($top: 12px, $left: -17px); + margin-left: 10px; + width: 14px; + height: 14px; + background: white; + border-bottom: 1px solid $o-hr-holidays-border-color; + transform: rotate(45deg); + } + + // Circle + &:after { + content: ''; + @include o-position-absolute($top: 14px, $left: -28px); + width: 12px; + height: 12px; + border: 2px solid $o-brand-primary; + border-radius: 10px; + background: #F0F0F0 + } + + .content { + position: relative; + background-color: white; + padding: 5px 7px; + font-size: 14px; + } + } + } + } + } +} diff --git a/addons/hr_holidays/static/src/scss/time_off.scss b/addons/hr_holidays/static/src/scss/time_off.scss index 2f8816e2fcd..e880cb0ea0d 100644 --- a/addons/hr_holidays/static/src/scss/time_off.scss +++ b/addons/hr_holidays/static/src/scss/time_off.scss @@ -1,26 +1,37 @@ .o_timeoff_calendar .o_content { .o_timeoff_container { - height: 6rem; + height: auto; box-shadow: inset 0 -1px 0 $border-color; - + .o_calendar_container { - height: calc(100% - 6rem); - } } .o_calendar_container { - height: 100%; + height: auto; } .o_timeoff_card { border-right: $border-color solid 2px; text-align: center; - margin-top: 4px; div { line-height: 1; } } + .fc-dayGridYear-view { + .fc-bgevent { + border-radius: 25px; + } + > .fc-month-container { + > .fc-month { + .fc-dayGridMonth-view { + .fc-has-event { + background-color: unset; + } + } + } + } + } + .o_timeoff_card_last { border-right: 0px; } @@ -29,6 +40,10 @@ font-size: 15px; } + .o_timeoff_huge { + font-size: 30px; + } + .o_timeoff_purple { color: $o-enterprise-color; } @@ -42,4 +57,7 @@ font-weight: bold; } } + .fc-week-number { + color: #adb5bd; + } } diff --git a/addons/hr_holidays/static/src/xml/time_off_calendar.xml b/addons/hr_holidays/static/src/xml/time_off_calendar.xml index ce8b7be8cd1..a4b9d8e365b 100644 --- a/addons/hr_holidays/static/src/xml/time_off_calendar.xml +++ b/addons/hr_holidays/static/src/xml/time_off_calendar.xml @@ -3,37 +3,32 @@
-
- +
+ + - - - -
- - / HoursDays Available + +
+ HOURSDAYS AVAILABLE
- HoursDays taken +
+ HOURSDAYS TAKEN
- - - (Expire on ) -
- + - + / HoursDaysAvailable @@ -41,9 +36,6 @@ HoursDaysTaken - - (Expire on ) - @@ -93,17 +85,26 @@
- -
-
-
-
-

- - -

-
-
+ +
+ + +
+
+
+ + + +
+
Legend
+ Validated
+ To Approve
+ Refused
+
+
+
+ diff --git a/addons/hr_holidays/tests/test_access_rights.py b/addons/hr_holidays/tests/test_access_rights.py index dc1df2cc908..2d97aaf9377 100644 --- a/addons/hr_holidays/tests/test_access_rights.py +++ b/addons/hr_holidays/tests/test_access_rights.py @@ -17,8 +17,7 @@ class TestHrHolidaysAccessRightsCommon(TestHrHolidaysCommon): self.leave_type = self.env['hr.leave.type'].create({ 'name': 'Unlimited', 'leave_validation_type': 'hr', - 'allocation_type': 'no', - 'validity_start': False, + 'requires_allocation': 'no', }) self.rd_dept.manager_id = False self.hr_dept.manager_id = False @@ -36,29 +35,25 @@ class TestHrHolidaysAccessRightsCommon(TestHrHolidaysCommon): self.lt_no_validation = self.env['hr.leave.type'].create({ 'name': 'Validation = no_validation', 'leave_validation_type': 'hr', - 'allocation_type': 'no', - 'validity_start': False, + 'requires_allocation': 'no', }) self.lt_validation_hr = self.env['hr.leave.type'].create({ 'name': 'Validation = HR', 'leave_validation_type': 'hr', - 'allocation_type': 'no', - 'validity_start': False, + 'requires_allocation': 'no', }) self.lt_validation_manager = self.env['hr.leave.type'].create({ 'name': 'Validation = manager', 'leave_validation_type': 'hr', - 'allocation_type': 'no', - 'validity_start': False, + 'requires_allocation': 'no', }) self.lt_validation_both = self.env['hr.leave.type'].create({ 'name': 'Validation = both', 'leave_validation_type': 'hr', - 'allocation_type': 'no', - 'validity_start': False, + 'requires_allocation': 'no', }) self.draft_status = [ @@ -779,7 +774,7 @@ class TestMultiCompany(TestHrHolidaysCommon): 'name': 'Unlimited - Company New', 'company_id': self.new_company.id, 'leave_validation_type': 'hr', - 'allocation_type': 'no', + 'requires_allocation': 'no', }) self.rd_dept.manager_id = False self.hr_dept.manager_id = False diff --git a/addons/hr_holidays/tests/test_accrual_allocations.py b/addons/hr_holidays/tests/test_accrual_allocations.py index b46da059470..9c2e2fdbf45 100644 --- a/addons/hr_holidays/tests/test_accrual_allocations.py +++ b/addons/hr_holidays/tests/test_accrual_allocations.py @@ -1,251 +1,556 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime, time +import datetime +from freezegun import freeze_time from dateutil.relativedelta import relativedelta from odoo import fields - -from odoo.tools import mute_logger +from odoo.tests import tagged from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon +@tagged('post_install', '-at_install') class TestAccrualAllocations(TestHrHolidaysCommon): def setUp(self): super(TestAccrualAllocations, self).setUp() - - # Make sure we have the rights to create, validate and delete the leaves, leave types and allocations - LeaveType = self.env['hr.leave.type'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True) - - self.accrual_type = LeaveType.create({ - 'name': 'accrual', - 'allocation_type': 'fixed', - 'validity_start': False, + self.leave_type = self.env['hr.leave.type'].create({ + 'name': 'Paid Time Off', + 'time_type': 'leave', + 'requires_allocation': 'yes', }) - self.unpaid_type = LeaveType.create({ - 'name': 'unpaid', - 'allocation_type': 'no', - 'unpaid': True, - 'validity_start': False, - }) - - self.set_employee_create_date(self.employee_emp_id, '2010-02-03 00:00:00') - self.set_employee_create_date(self.employee_hruser_id, '2010-02-03 00:00:00') - - def set_employee_create_date(self, id, newdate): + def setAllocationCreateDate(self, allocation_id, date): """ This method is a hack in order to be able to define/redefine the create_date - of the employees. + of the allocations. This is done in SQL because ORM does not allow to write onto the create_date field. """ self.env.cr.execute(""" UPDATE - hr_employee + hr_leave_allocation SET create_date = '%s' WHERE id = %s - """ % (newdate, id)) + """ % (date, allocation_id)) - def test_accrual_base_no_leaves(self): - """ Test if we can allocate some leaves accrually to an employee """ - alloc = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ - 'name': 'Accrual allocation for employee', - 'employee_id': self.employee_emp_id, - 'holiday_status_id': self.accrual_type.id, - 'allocation_type': 'accrual', - 'number_of_days': 0, - 'number_per_interval': 1, - 'interval_number': 1, - 'unit_per_interval': 'days', - 'interval_unit': 'weeks', + def test_frequency_daily(self): + accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'daily', + 'maximum_leave': 10000 + })], }) - alloc.action_approve() - alloc._update_accrual() - - self.assertEqual(alloc.number_of_days, 1, 'Employee should have been allocated one leave day') - - def test_accrual_base_leaves(self): - """ Test if the accrual allocation take the unpaid leaves into account when allocating leaves """ - alloc = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ - 'name': 'Accrual allocation for employee with leaves', - 'employee_id': self.employee_hruser_id, - 'holiday_status_id': self.accrual_type.id, - 'allocation_type': 'accrual', - 'number_of_days': 0, - 'number_per_interval': 1, - 'interval_number': 1, - 'unit_per_interval': 'days', - 'interval_unit': 'weeks', - }) - - alloc.action_approve() - - employee = self.env['hr.employee'].browse(self.employee_hruser_id) - # Getting the previous work date - df = employee.resource_calendar_id.plan_days(-2, fields.Datetime.now()).date() - - leave = self.env['hr.leave'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ - 'name': 'Leave for hruser', - 'employee_id': self.employee_hruser_id, - 'holiday_status_id': self.unpaid_type.id, - 'date_from': datetime.combine(df, time(0, 0, 0)), - 'date_to': datetime.combine(df + relativedelta(days=1), time(0, 0, 0)), - 'number_of_days': 1, - }) - - leave.action_approve() - - alloc._update_accrual() - - self.assertEqual(alloc.number_of_days, .8, 'As employee took some unpaid leaves last week, he should be allocated only .8 days') - - def test_accrual_many(self): - """ - Test different configuration of accrual allocations - """ - Allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True) - - alloc_0 = Allocation.create({ - 'name': '1 day per 2 weeks', - 'employee_id': self.employee_emp_id, - 'holiday_status_id': self.accrual_type.id, - 'allocation_type': 'accrual', - 'number_of_days': 0, - 'number_per_interval': 1, - 'interval_number': 2, - 'unit_per_interval': 'days', - 'interval_unit': 'weeks', - }) - - alloc_1 = Allocation.create({ - 'name': '4 hours per week', - 'employee_id': self.employee_emp_id, - 'holiday_status_id': self.accrual_type.id, - 'allocation_type': 'accrual', - 'number_of_days': 0, - 'number_per_interval': 4, - 'interval_number': 1, - 'unit_per_interval': 'hours', - 'interval_unit': 'weeks', - }) - - alloc_2 = Allocation.create({ - 'name': '2 day per 1 month', - 'employee_id': self.employee_emp_id, - 'holiday_status_id': self.accrual_type.id, - 'allocation_type': 'accrual', - 'number_of_days': 0, - 'number_per_interval': 2, - 'interval_number': 1, - 'unit_per_interval': 'days', - 'interval_unit': 'months', - }) - - alloc_3 = Allocation.create({ - 'name': '20 days per year', - 'employee_id': self.employee_emp_id, - 'holiday_status_id': self.accrual_type.id, - 'allocation_type': 'accrual', - 'number_of_days': 0, - 'number_per_interval': 20, - 'interval_number': 1, - 'unit_per_interval': 'days', - 'interval_unit': 'years', - }) - - (alloc_0 | alloc_1 | alloc_2 | alloc_3).action_approve() - - Allocation._update_accrual() - - self.assertEqual(alloc_0.number_of_days, 1) - self.assertEqual(alloc_1.number_of_days, .5) - self.assertEqual(alloc_2.number_of_days, 2) - self.assertEqual(alloc_3.number_of_days, 20) - - def test_accrual_new_employee(self): - """ - Test if accrual allocation takes into account the creation date - of an employee - """ - Allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True) - - self.set_employee_create_date(self.employee_emp_id, fields.Datetime.to_string(fields.Datetime.now())) - - alloc_0 = Allocation.create({ - 'name': 'one shot one kill', - 'employee_id': self.employee_emp_id, - 'holiday_status_id': self.accrual_type.id, - 'allocation_type': 'accrual', - 'number_of_days': 0, - 'number_per_interval': 1, - 'interval_number': 1, - 'unit_per_interval': 'days', - 'interval_unit': 'weeks', - }) - - alloc_0.action_approve() - - Allocation._update_accrual() - - self.assertEqual(alloc_0.number_of_days, 0, 'Employee is new he should not get any accrual leaves') - - def test_accrual_multi(self): - """ Test if the cron does not allocate leaves every time it's called but only when necessary """ - alloc = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ - 'name': '2 days per week', - 'employee_id': self.employee_emp_id, - 'holiday_status_id': self.accrual_type.id, - 'allocation_type': 'accrual', - 'number_of_days': 0, - 'number_per_interval': 1, - 'interval_number': 2, - 'unit_per_interval': 'days', - 'interval_unit': 'weeks', - }) - alloc.action_approve() - alloc._update_accrual() - alloc._update_accrual() - - self.assertEqual(alloc.number_of_days, 1, 'Cron only allocates 1 days every two weeks') - - def test_accrual_validation(self): - """ - Test if cron does not allocate past it's validity date - """ - Allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True) - - alloc_0 = Allocation.create({ - 'name': '20 days per year', - 'employee_id': self.employee_emp_id, - 'holiday_status_id': self.accrual_type.id, - 'number_of_days': 0, - 'number_per_interval': 20, - 'interval_number': 1, - 'unit_per_interval': 'days', - 'interval_unit': 'years', - 'date_to': fields.Datetime.from_string('2015-02-03 00:00:00'), - }) - - alloc_0.action_approve() - - Allocation._update_accrual() - - self.assertEqual(alloc_0.number_of_days, 0, 'Cron validity passed, should not allocate any leave') - - def test_accrual_balance_limit(self): - """ Test if accrual allocation does not allocate more than the balance limit""" allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ - 'name': 'accrual 5 max', - 'employee_id': self.employee_emp_id, - 'holiday_status_id': self.accrual_type.id, - 'allocation_type': 'accrual', - 'accrual_limit': 5, + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, 'number_of_days': 0, - 'number_per_interval': 6, - 'interval_number': 1, - 'unit_per_interval': 'days', - 'interval_unit': 'weeks', + 'allocation_type': 'accrual', }) - allocation.action_approve() + allocation.action_confirm() + allocation.action_validate() + self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.') + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.') allocation._update_accrual() + tomorrow = datetime.date.today() + relativedelta(days=2) + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet. The accrual starts tomorrow.') - self.assertEqual(allocation.number_of_days, 5, 'Should have allocated only 5 days as balance limit is 5') + freezer = freeze_time(tomorrow) + freezer.start() + allocation._update_accrual() + nextcall = datetime.date.today() + relativedelta(days=1) + self.assertEqual(allocation.number_of_days, 1, 'There should be 1 day allocated.') + self.assertEqual(allocation.nextcall, nextcall, 'The next call date of the cron should be in 2 days.') + allocation._update_accrual() + self.assertEqual(allocation.number_of_days, 1, 'There should be only 1 day allocated.') + freezer.stop() + + def test_frequency_weekly(self): + accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'weekly', + 'maximum_leave': 10000 + })], + }) + allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 0, + 'allocation_type': 'accrual', + }) + allocation.action_confirm() + allocation.action_validate() + self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.') + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.') + allocation._update_accrual() + nextWeek = self.env['hr.leave.accrual.level']._get_next_weekday(datetime.date.today(), 'mon') + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet. The accrual starts tomorrow.') + + freezer = freeze_time(nextWeek) + freezer.start() + allocation._update_accrual() + nextWeek = self.env['hr.leave.accrual.level']._get_next_weekday(datetime.date.today(), 'mon') + self.assertEqual(allocation.number_of_days, 1, 'There should be 1 day allocated.') + self.assertEqual(allocation.nextcall, nextWeek, 'The next call date of the cron should be in 2 weeks') + freezer.stop() + + def test_frequency_bimonthly(self): + freezer = freeze_time('2021-09-01') + freezer.start() + accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'bimonthly', + 'first_day': 1, + 'second_day': 15, + 'maximum_leave': 10000, + })], + }) + allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 0, + 'allocation_type': 'accrual', + }) + self.setAllocationCreateDate(allocation.id, '2021-09-01 00:00:00') + allocation.action_confirm() + allocation.action_validate() + self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.') + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.') + allocation._update_accrual() + next_date = datetime.date(2021, 9, 15) + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet. The accrual starts tomorrow.') + freezer.stop() + + freezer = freeze_time(next_date) + freezer.start() + next_date = datetime.date(2021, 10, 1) + allocation._update_accrual() + self.assertEqual(allocation.number_of_days, 1, 'There should be 1 day allocated.') + self.assertEqual(allocation.nextcall, next_date, 'The next call date of the cron should be October 1st') + freezer.stop() + + def test_frequency_monthly(self): + freezer = freeze_time('2021-09-01') + freezer.start() + accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'monthly', + 'maximum_leave': 10000 + })], + }) + allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 0, + 'allocation_type': 'accrual', + }) + self.setAllocationCreateDate(allocation.id, '2021-09-01 00:00:00') + allocation.action_confirm() + allocation.action_validate() + self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.') + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.') + allocation._update_accrual() + next_date = datetime.date(2021, 10, 1) + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet. The accrual starts tomorrow.') + freezer.stop() + + freezer = freeze_time(next_date) + freezer.start() + next_date = datetime.date(2021, 11, 1) + allocation._update_accrual() + self.assertEqual(allocation.number_of_days, 1, 'There should be 1 day allocated.') + self.assertEqual(allocation.nextcall, next_date, 'The next call date of the cron should be November 1st') + freezer.stop() + + def test_frequency_biyearly(self): + freezer = freeze_time('2021-09-01') + freezer.start() + accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'biyearly', + 'maximum_leave': 10000, + })], + }) + #this sets up an accrual on the 1st of January and the 1st of July + allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 0, + 'allocation_type': 'accrual', + }) + self.setAllocationCreateDate(allocation.id, '2021-09-01 00:00:00') + allocation.action_confirm() + allocation.action_validate() + self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.') + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.') + allocation._update_accrual() + next_date = datetime.date(2022, 1, 1) + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet. The accrual starts tomorrow.') + freezer.stop() + + freezer = freeze_time(next_date) + freezer.start() + next_date = datetime.date(2022, 7, 1) + allocation._update_accrual() + self.assertEqual(allocation.number_of_days, 1, 'There should be 1 day allocated.') + self.assertEqual(allocation.nextcall, next_date, 'The next call date of the cron should be July 1st') + freezer.stop() + + def test_frequency_yearly(self): + freezer = freeze_time('2021-09-01') + freezer.start() + accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'yearly', + 'maximum_leave': 10000, + })], + }) + #this sets up an accrual on the 1st of January + allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 0, + 'allocation_type': 'accrual', + }) + self.setAllocationCreateDate(allocation.id, '2021-09-01 00:00:00') + allocation.action_confirm() + allocation.action_validate() + self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.') + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.') + allocation._update_accrual() + next_date = datetime.date(2022, 1, 1) + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet. The accrual starts tomorrow.') + freezer.stop() + + freezer = freeze_time(next_date) + freezer.start() + next_date = datetime.date(2023, 1, 1) + allocation._update_accrual() + self.assertEqual(allocation.number_of_days, 1, 'There should be 1 day allocated.') + self.assertEqual(allocation.nextcall, next_date, 'The next call date of the cron should be January 1st 2023') + freezer.stop() + + def test_check_gain(self): + # 2 accruals, one based on worked time, one not + # check gain + freezer = freeze_time('2021-08-30') + freezer.start() + calendar_emp = self.env['resource.calendar'].create({ + 'name': '40 Hours', + 'tz': self.employee_emp.tz, + 'attendance_ids': [ + (0, 0, { + 'name': '%s_%d' % ('40 Hours', index), + 'hour_from': 8, + 'hour_to': 12, + 'dayofweek': str(index), + 'day_period': 'morning' + }, { + 'name': '%s_%d' % ('40 Hours', index), + 'hour_from': 13, + 'hour_to': 18, + 'dayofweek': str(index), + 'day_period': 'afternoon' + }) for index in range(5) + ], + }) + self.employee_emp.resource_calendar_id = calendar_emp.id + + accrual_plan_not_based_on_worked_time = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 5, + 'added_value_type': 'days', + 'frequency': 'weekly', + 'maximum_leave': 10000, + })], + }) + accrual_plan_based_on_worked_time = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 5, + 'added_value_type': 'days', + 'frequency': 'weekly', + 'maximum_leave': 10000, + 'is_based_on_worked_time': True, + })], + }) + allocation_not_worked_time = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan_not_based_on_worked_time.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 0, + 'allocation_type': 'accrual', + 'state': 'validate', + }) + allocation_worked_time = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan_based_on_worked_time.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 0, + 'allocation_type': 'accrual', + 'state': 'validate', + }) + self.setAllocationCreateDate(allocation_not_worked_time.id, '2021-08-01 00:00:00') + self.setAllocationCreateDate(allocation_worked_time.id, '2021-08-01 00:00:00') + holiday_type = self.env['hr.leave.type'].create({ + 'name': 'Paid Time Off', + 'requires_allocation': 'no', + 'responsible_id': self.user_hrmanager_id, + }) + leave = self.env['hr.leave'].create({ + 'name': 'leave', + 'employee_id': self.employee_emp.id, + 'holiday_status_id': holiday_type.id, + 'date_from': '2021-09-02 00:00:00', + 'date_to': '2021-09-02 23:59:59', + }) + leave.action_validate() + self.assertFalse(allocation_not_worked_time.nextcall, 'There should be no nextcall set on the allocation.') + self.assertFalse(allocation_worked_time.nextcall, 'There should be no nextcall set on the allocation.') + self.assertEqual(allocation_not_worked_time.number_of_days, 0, 'There should be no days allocated yet.') + self.assertEqual(allocation_worked_time.number_of_days, 0, 'There should be no days allocated yet.') + freezer.stop() + next_date = datetime.date(2021, 9, 6) + freezer = freeze_time(next_date) + freezer.start() + # next_date = datetime.date(2021, 9, 13) + self.env['hr.leave.allocation']._update_accrual() + self.assertEqual(allocation_not_worked_time.number_of_days, 5, 'There should be 5 days allocated.') + # 3.75 -> starts 1 day after allocation date -> 31/08-3/09 => 4 days - 1 days time off => (3 / 4) * 5 days + self.assertEqual(allocation_worked_time.number_of_days, 3.75, 'There should be 5 days allocated.') + self.assertEqual(allocation_not_worked_time.nextcall, datetime.date(2021, 9, 13), 'The next call date of the cron should be the September 13th') + self.assertEqual(allocation_worked_time.nextcall, datetime.date(2021, 9, 13), 'The next call date of the cron should be the September 13th') + freezer.stop() + + freezer = freeze_time(next_date + relativedelta(days=7)) + freezer.start() + next_date = datetime.date(2021, 9, 20) + self.env['hr.leave.allocation']._update_accrual() + self.assertEqual(allocation_not_worked_time.number_of_days, 10, 'There should be 10 days allocated.') + self.assertEqual(allocation_not_worked_time.nextcall, next_date, 'The next call date of the cron should be September 20th') + self.assertEqual(allocation_worked_time.number_of_days, 8.75, 'There should be 9 days allocated.') + self.assertEqual(allocation_worked_time.nextcall, next_date, 'The next call date of the cron should be September 20th') + freezer.stop() + + def test_check_max_value(self): + accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'daily', + 'maximum_leave': 1, + })], + }) + allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 0, + 'allocation_type': 'accrual', + }) + allocation.action_confirm() + allocation.action_validate() + allocation._update_accrual() + tomorrow = datetime.date.today() + relativedelta(days=2) + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet. The accrual starts tomorrow.') + + freezer = freeze_time(tomorrow) + freezer.start() + allocation._update_accrual() + nextcall = datetime.date.today() + relativedelta(days=1) + allocation._update_accrual() + self.assertEqual(allocation.number_of_days, 1, 'There should be only 1 day allocated.') + freezer.stop() + + freezer = freeze_time(nextcall) + freezer.start() + allocation._update_accrual() + nextcall = datetime.date.today() + relativedelta(days=1) + #The maximum value is 1 so this shouldn't change anything + allocation._update_accrual() + self.assertEqual(allocation.number_of_days, 1, 'There should be only 1 day allocated.') + freezer.stop() + + def test_accrual_transition_immediately(self): + #1 accrual with 2 levels and level transition immediately + accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'transition_mode': 'immediately', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'weekly', + 'maximum_leave': 1, + }), (0, 0, { + 'start_count': 10, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'weekly', + 'maximum_leave': 1, + })], + }) + allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 0, + 'allocation_type': 'accrual', + }) + allocation.action_confirm() + allocation.action_validate() + next_date = datetime.date.today() + relativedelta(days=11) + second_level = self.env['hr.leave.accrual.level'].search([('accrual_plan_id', '=', accrual_plan.id), ('start_count', '=', 10)]) + self.assertEqual(allocation._get_current_accrual_plan_level_id(next_date), second_level, 'The second level should be selected') + + def test_accrual_transition_after_period(self): + # 1 accrual with 2 levels and level transition after + accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'transition_mode': 'end_of_accrual', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'weekly', + 'maximum_leave': 1, + }), (0, 0, { + 'start_count': 10, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'weekly', + 'maximum_leave': 1, + })], + }) + allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 0, + 'allocation_type': 'accrual', + }) + allocation.action_confirm() + allocation.action_validate() + next_date = datetime.date.today() + relativedelta(days=11) + second_level = self.env['hr.leave.accrual.level'].search([('accrual_plan_id', '=', accrual_plan.id), ('start_count', '=', 10)]) + self.assertEqual(allocation._get_current_accrual_plan_level_id(next_date), second_level, 'The second level should be selected') + + def test_unused_accrual_lost(self): + #1 accrual with 2 levels and level transition immediately + accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'daily', + 'maximum_leave': 1, + 'action_with_unused_accruals': 'lost', + })], + }) + allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 10, + 'allocation_type': 'accrual', + }) + allocation.action_confirm() + allocation.action_validate() + + freezer = freeze_time('2022-01-01') + freezer.start() + allocation._update_accrual() + self.assertEqual(allocation.number_of_days, 0, 'There number of days should be reset') + freezer.stop() + + def test_unused_accrual_postponed(self): + # 1 accrual with 2 levels and level transition after + # This also tests retroactivity + accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'days', + 'frequency': 'daily', + 'maximum_leave': 25, + 'action_with_unused_accruals': 'postponed', + })], + }) + allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'accrual_plan_id': accrual_plan.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 10, + 'allocation_type': 'accrual', + }) + allocation.action_confirm() + allocation.action_validate() + + freezer = freeze_time('2022-01-01') + freezer.start() + allocation._update_accrual() + self.assertEqual(allocation.number_of_days, 25, 'The maximum number of days should be reached and kept.') + freezer.stop() diff --git a/addons/hr_holidays/tests/test_allocation_access_rights.py b/addons/hr_holidays/tests/test_allocation_access_rights.py index 2d4c973eafb..b68ffc200dc 100644 --- a/addons/hr_holidays/tests/test_allocation_access_rights.py +++ b/addons/hr_holidays/tests/test_allocation_access_rights.py @@ -4,6 +4,7 @@ from odoo import tests from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon from odoo.exceptions import AccessError, UserError +import time @tests.tagged('access_rights', 'post_install', '-at_install') @@ -16,31 +17,33 @@ class TestAllocationRights(TestHrHolidaysCommon): self.employee_emp.parent_id = False self.employee_emp.leave_manager_id = False - self.lt_validation_hr = self.env['hr.leave.type'].create({ + self.lt_no_validation = self.env['hr.leave.type'].create({ 'name': 'Validation = HR', - 'allocation_validation_type': 'hr', - 'allocation_type': 'fixed_allocation', - 'validity_start': False, + 'allocation_validation_type': 'officer', + 'requires_allocation': 'no', + 'employee_requests': 'yes', }) self.lt_validation_manager = self.env['hr.leave.type'].create({ 'name': 'Validation = manager', - 'allocation_validation_type': 'manager', - 'allocation_type': 'fixed_allocation', - 'validity_start': False, + 'allocation_validation_type': 'officer', + 'requires_allocation': 'yes', + 'employee_requests': 'yes', }) - self.lt_validation_both = self.env['hr.leave.type'].create({ - 'name': 'Validation = both', - 'allocation_validation_type': 'both', - 'allocation_type': 'fixed_allocation', - 'validity_start': False, + self.lt_allocation_manager = self.env['hr.leave.type'].create({ + 'name': 'Validation = manager', + 'allocation_validation_type': 'set', + 'requires_allocation': 'yes', + 'employee_requests': 'no', }) def request_allocation(self, user, values={}): values = dict(values, **{ 'name': 'Allocation', 'number_of_days': 1, + 'date_from': time.strftime('%Y-01-01'), + 'date_to': time.strftime('%Y-12-31'), }) return self.env['hr.leave.allocation'].with_user(user).create(values) @@ -51,18 +54,17 @@ class TestAccessRightsSimpleUser(TestAllocationRights): """ A simple user can request an allocation but not approve it """ values = { 'employee_id': self.employee_emp.id, - 'holiday_status_id': self.lt_validation_hr.id, + 'holiday_status_id': self.lt_validation_manager.id, } allocation = self.request_allocation(self.user_employee.id, values) with self.assertRaises(UserError): - allocation.action_approve() + allocation.action_validate() def test_simple_user_request_fixed_allocation(self): - """ A simple user cannot request an allocation if set by HR """ - self.lt_validation_hr.allocation_type = 'fixed' + """ A simple user cannot request an allocation if employee requests is not enabled """ values = { 'employee_id': self.employee_emp.id, - 'holiday_status_id': self.lt_validation_hr.id, + 'holiday_status_id': self.lt_allocation_manager.id, } with self.assertRaises(AccessError): self.request_allocation(self.user_employee.id, values) @@ -71,12 +73,14 @@ class TestAccessRightsSimpleUser(TestAllocationRights): """ A simple user can reset to draft only his own allocation """ values = { 'employee_id': self.employee_emp.id, - 'holiday_status_id': self.lt_validation_hr.id, + 'holiday_status_id': self.lt_validation_manager.id, } allocation = self.request_allocation(self.user_employee.id, values) - self.assertEqual(allocation.state, 'confirm') + self.assertEqual(allocation.state, 'draft') + allocation.action_confirm() + self.assertEqual(allocation.state, 'confirm', "It should be confirmed") allocation.action_draft() - self.assertEqual(allocation.state, 'draft', "It should be reset to draft state") + self.assertEqual(allocation.state, 'draft', "It should have been reset to draft") class TestAccessRightsEmployeeManager(TestAllocationRights): @@ -104,7 +108,8 @@ class TestAccessRightsEmployeeManager(TestAllocationRights): 'holiday_status_id': self.lt_validation_manager.id, } allocation = self.request_allocation(self.user_employee.id, values) - allocation.action_approve() + allocation.action_confirm() + allocation.action_validate() self.assertEqual(allocation.state, 'validate', "The allocation should be validated") def test_manager_refuse_request_allocation(self): @@ -114,6 +119,7 @@ class TestAccessRightsEmployeeManager(TestAllocationRights): 'holiday_status_id': self.lt_validation_manager.id, } allocation = self.request_allocation(self.user_employee.id, values) + allocation.action_confirm() allocation.action_refuse() self.assertEqual(allocation.state, 'refuse', "The allocation should be validated") @@ -134,61 +140,37 @@ class TestAccessRightsEmployeeManager(TestAllocationRights): 'holiday_status_id': self.lt_validation_manager.id, } allocation = self.request_allocation(self.user_employee.id, values) - with self.assertRaises(UserError): - allocation.action_approve() - - def test_manager_only_first_approval(self): - """ A manager can only do the first approval """ - values = { - 'employee_id': self.managed_employee.id, - 'holiday_status_id': self.lt_validation_both.id, - } - allocation = self.request_allocation(self.user_employee.id, values) - allocation.action_approve() with self.assertRaises(UserError): allocation.action_validate() - class TestAccessRightsHolidayUser(TestAllocationRights): def test_holiday_user_request_allocation(self): """ A holiday user can request and approve an allocation for any employee """ values = { 'employee_id': self.employee_emp.id, - 'holiday_status_id': self.lt_validation_hr.id, + 'holiday_status_id': self.lt_validation_manager.id, } allocation = self.request_allocation(self.user_hruser.id, values) - allocation.action_approve() + allocation.action_confirm() + allocation.action_validate() self.assertEqual(allocation.state, 'validate', "It should have been validated") def test_holiday_user_request_fixed_allocation(self): """ A holiday user can request and approve an allocation if set by HR """ - self.lt_validation_hr.allocation_type = 'fixed' values = { 'employee_id': self.employee_emp.id, - 'holiday_status_id': self.lt_validation_hr.id, + 'holiday_status_id': self.lt_allocation_manager.id, } allocation = self.request_allocation(self.user_hruser.id, values) - allocation.action_approve() - self.assertEqual(allocation.state, 'validate', "It should have been validated") - - def test_holiday_user_both_second_approval(self): - """ A holiday user can only do the second approval when double validation """ - values = { - 'employee_id': self.employee_emp.id, - 'holiday_status_id': self.lt_validation_both.id, - } - allocation = self.request_allocation(self.user_hruser.id, values) - with self.assertRaises(UserError): - allocation.action_approve() - allocation.sudo().action_approve() # First approval by someone else + allocation.action_confirm() allocation.action_validate() self.assertEqual(allocation.state, 'validate', "It should have been validated") def test_holiday_user_batch_allocation(self): """ A holiday user cannot create a batch allocation """ values = { - 'holiday_status_id': self.lt_validation_hr.id, + 'holiday_status_id': self.lt_validation_manager.id, 'holiday_type': 'company', 'mode_company_id': self.user_employee.company_id.id, } @@ -199,11 +181,12 @@ class TestAccessRightsHolidayUser(TestAllocationRights): """ A holiday user cannot approve his own allocation """ values = { 'employee_id': self.employee_hruser.id, - 'holiday_status_id': self.lt_validation_hr.id, + 'holiday_status_id': self.lt_validation_manager.id, } allocation = self.request_allocation(self.user_hruser.id, values) + allocation.action_confirm() with self.assertRaises(UserError): - allocation.action_approve() + allocation.action_validate() class TestAccessRightsHolidayManager(TestAllocationRights): @@ -212,32 +195,22 @@ class TestAccessRightsHolidayManager(TestAllocationRights): """ A holiday manager can approve his own allocation """ values = { 'employee_id': self.employee_hrmanager.id, - 'holiday_status_id': self.lt_validation_hr.id, + 'holiday_status_id': self.lt_validation_manager.id, } allocation = self.request_allocation(self.user_hrmanager.id, values) - allocation.action_approve() - self.assertEqual(allocation.state, 'validate', "It should have been validated") - - def test_holiday_manager_both_validation(self): - """ A holiday manager can perform both validation """ - values = { - 'employee_id': self.employee_emp.id, - 'holiday_status_id': self.lt_validation_both.id, - } - allocation = self.request_allocation(self.user_hrmanager.id, values) - allocation.action_approve() - self.assertEqual(allocation.state, 'validate1', "It should have been validated one time") + allocation.action_confirm() allocation.action_validate() - self.assertEqual(allocation.state, 'validate', "It should have been completely validated") + self.assertEqual(allocation.state, 'validate', "It should have been validated") def test_holiday_manager_refuse_validated(self): """ A holiday manager can refuse a validated allocation """ values = { 'employee_id': self.employee_emp.id, - 'holiday_status_id': self.lt_validation_hr.id, + 'holiday_status_id': self.lt_validation_manager.id, } allocation = self.request_allocation(self.user_hrmanager.id, values) - allocation.action_approve() + allocation.action_confirm() + allocation.action_validate() self.assertEqual(allocation.state, 'validate', "It should have been validated") allocation.action_refuse() self.assertEqual(allocation.state, 'refuse', "It should have been refused") diff --git a/addons/hr_holidays/tests/test_automatic_leave_dates.py b/addons/hr_holidays/tests/test_automatic_leave_dates.py index d3774e0974f..17089dfe5e9 100644 --- a/addons/hr_holidays/tests/test_automatic_leave_dates.py +++ b/addons/hr_holidays/tests/test_automatic_leave_dates.py @@ -14,8 +14,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon): self.leave_type = self.env['hr.leave.type'].create({ 'name': 'Automatic Test', 'time_type': 'leave', - 'allocation_type': 'no', - 'validity_start': False, + 'requires_allocation': 'no', }) def test_no_attendances(self): diff --git a/addons/hr_holidays/tests/test_change_department.py b/addons/hr_holidays/tests/test_change_department.py index 83ebb1581ee..b68dd4804a5 100644 --- a/addons/hr_holidays/tests/test_change_department.py +++ b/addons/hr_holidays/tests/test_change_department.py @@ -14,8 +14,7 @@ class TestChangeDepartment(TestHrHolidaysCommon): HolidayStatusManagerGroup = self.env['hr.leave.type'].with_user(self.user_hrmanager_id) self.holidays_status_1 = HolidayStatusManagerGroup.create({ 'name': 'NotLimitedHR', - 'allocation_type': 'no', - 'validity_start': False, + 'requires_allocation': 'no', }) def create_holiday(name, start, end): diff --git a/addons/hr_holidays/tests/test_company_leave.py b/addons/hr_holidays/tests/test_company_leave.py index b5979dc19ab..02919e7509b 100644 --- a/addons/hr_holidays/tests/test_company_leave.py +++ b/addons/hr_holidays/tests/test_company_leave.py @@ -20,6 +20,7 @@ class TestCompanyLeave(TransactionCase): 'name': 'Bank Holiday', 'responsible_id': cls.env.user.id, 'company_id': cls.company.id, + 'requires_allocation': 'no', }) cls.paid_time_off = cls.env['hr.leave.type'].create({ @@ -27,6 +28,7 @@ class TestCompanyLeave(TransactionCase): 'request_unit': 'day', 'leave_validation_type': 'both', 'company_id': cls.company.id, + 'requires_allocation': 'no', }) cls.employee = cls.env['hr.employee'].create({ @@ -313,7 +315,7 @@ class TestCompanyLeave(TransactionCase): }) company_leave._compute_date_from_to() - with self.assertQueryCount(__system__=845, admin=865): + with self.assertQueryCount(__system__=846, admin=865): # Original query count: 1987 # Without tracking/activity context keys: 5154 company_leave.action_validate() diff --git a/addons/hr_holidays/tests/test_holidays_flow.py b/addons/hr_holidays/tests/test_holidays_flow.py index b63c4486a5d..987ef2084c7 100644 --- a/addons/hr_holidays/tests/test_holidays_flow.py +++ b/addons/hr_holidays/tests/test_holidays_flow.py @@ -25,19 +25,17 @@ class TestHolidaysFlow(TestHrHolidaysCommon): HolidayStatusManagerGroup = HolidaysStatus.with_user(self.user_hrmanager_id) HolidayStatusManagerGroup.create({ 'name': 'WithMeetingType', - 'allocation_type': 'no', + 'requires_allocation': 'no', }) self.holidays_status_hr = HolidayStatusManagerGroup.create({ 'name': 'NotLimitedHR', - 'allocation_type': 'no', + 'requires_allocation': 'no', 'leave_validation_type': 'hr', - 'validity_start': False, }) self.holidays_status_manager = HolidayStatusManagerGroup.create({ 'name': 'NotLimitedManager', - 'allocation_type': 'no', + 'requires_allocation': 'no', 'leave_validation_type': 'manager', - 'validity_start': False, }) HolidaysEmployeeGroup = Requests.with_user(self.user_employee_id) @@ -86,9 +84,10 @@ class TestHolidaysFlow(TestHrHolidaysCommon): holiday_status_paid_time_off = self.env['hr.leave.type'].create({ 'name': 'Paid Time Off', - 'allocation_type': 'fixed', + 'requires_allocation': 'yes', + 'employee_requests': 'no', + 'allocation_validation_type': 'set', 'leave_validation_type': 'both', - 'validity_start': time.strftime('%Y-%m-01'), 'responsible_id': self.env.ref('base.user_admin').id, }) @@ -99,12 +98,16 @@ class TestHolidaysFlow(TestHrHolidaysCommon): 'number_of_days': 20, 'employee_id': self.employee_emp_id, 'state': 'validate', + 'date_from': time.strftime('%Y-%m-01'), + 'date_to': time.strftime('%Y-12-31'), }, { 'name': 'Paid Time off for David', 'holiday_status_id': holiday_status_paid_time_off.id, 'number_of_days': 20, 'employee_id': self.ref('hr.employee_admin'), 'state': 'validate', + 'date_from': time.strftime('%Y-%m-01'), + 'date_to': time.strftime('%Y-12-31'), } ]) @@ -122,16 +125,15 @@ class TestHolidaysFlow(TestHrHolidaysCommon): HolidayStatusManagerGroup = HolidaysStatus.with_user(self.user_hrmanager_id) HolidayStatusManagerGroup.create({ 'name': 'WithMeetingType', - 'allocation_type': 'no', - 'validity_start': False, + 'requires_allocation': 'no', }) self.holidays_status_limited = HolidayStatusManagerGroup.create({ 'name': 'Limited', - 'allocation_type': 'fixed', - 'allocation_validation_type': 'both', + 'requires_allocation': 'yes', + 'employee_requests': 'no', + 'allocation_validation_type': 'set', 'leave_validation_type': 'both', - 'validity_start': False, }) HolidaysEmployeeGroup = Requests.with_user(self.user_employee_id) @@ -141,9 +143,11 @@ class TestHolidaysFlow(TestHrHolidaysCommon): 'employee_id': self.employee_emp_id, 'holiday_status_id': self.holidays_status_limited.id, 'number_of_days': 2, + 'state': 'confirm', + 'date_from': time.strftime('%Y-%m-01'), + 'date_to': time.strftime('%Y-12-31'), }) # HrUser validates the first step - aloc1_user_group.action_approve() # HrManager validates the second step aloc1_user_group.with_user(self.user_hrmanager_id).action_validate() @@ -165,11 +169,6 @@ class TestHolidaysFlow(TestHrHolidaysCommon): hol_status_2_employee_group.invalidate_cache() _check_holidays_status(hol_status_2_employee_group, 2.0, 0.0, 2.0, 1.0) - # HrManager validates the first step - hol2_user_group.with_user(self.user_hrmanager_id).action_approve() - self.assertEqual(hol2.state, 'validate1', - 'hr_holidays: first validation should lead to validate1 state') - # HrManager validates the second step hol2_user_group.with_user(self.user_hrmanager_id).action_validate() self.assertEqual(hol2.state, 'validate', @@ -216,7 +215,6 @@ class TestHolidaysFlow(TestHrHolidaysCommon): hol3.action_confirm() self.assertEqual(hol3.state, 'confirm', 'hr_holidays: confirming should lead to confirm state') # I validate the holiday request by clicking on "To Approve" button. - hol3.action_approve() hol3.action_validate() self.assertEqual(hol3.state, 'validate', 'hr_holidays: validation should lead to validate state') # Check left days for casual leave: 19 days left @@ -243,9 +241,10 @@ class TestHolidaysFlow(TestHrHolidaysCommon): holiday_status_paid_time_off = self.env['hr.leave.type'].create({ 'name': 'Paid Time Off', - 'allocation_type': 'fixed', + 'requires_allocation': 'yes', + 'employee_requests': 'no', + 'allocation_validation_type': 'set', 'leave_validation_type': 'both', - 'validity_start': time.strftime('%Y-%m-01'), 'responsible_id': self.env.ref('base.user_admin').id, }) @@ -255,6 +254,8 @@ class TestHolidaysFlow(TestHrHolidaysCommon): 'number_of_days': 20, 'employee_id': self.ref('hr.employee_admin'), 'state': 'validate', + 'date_from': time.strftime('%Y-%m-01'), + 'date_to': time.strftime('%Y-12-31'), }) leave_vals = { diff --git a/addons/hr_holidays/tests/test_hr_leave_type.py b/addons/hr_holidays/tests/test_hr_leave_type.py index 852603a2d06..6a35e412ed0 100644 --- a/addons/hr_holidays/tests/test_hr_leave_type.py +++ b/addons/hr_holidays/tests/test_hr_leave_type.py @@ -15,8 +15,7 @@ class TestHrLeaveType(TestHrHolidaysCommon): leave_type = self.env['hr.leave.type'].create({ 'name': 'Paid Time Off', 'time_type': 'leave', - 'allocation_type': 'no', - 'validity_start': False, + 'requires_allocation': 'no', }) leave_1 = self.env['hr.leave'].create({ @@ -39,5 +38,5 @@ class TestHrLeaveType(TestHrHolidaysCommon): with self.assertRaises(AccessError): self.env['hr.leave.type'].with_user(self.user_hruser_id).create({ 'name': 'UserCheats', - 'allocation_type': 'no', + 'requires_allocation': 'no', }) diff --git a/addons/hr_holidays/tests/test_leave_requests.py b/addons/hr_holidays/tests/test_leave_requests.py index c0c4f3ea79f..7c3be743969 100644 --- a/addons/hr_holidays/tests/test_leave_requests.py +++ b/addons/hr_holidays/tests/test_leave_requests.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import datetime, date +import time from dateutil.relativedelta import relativedelta from pytz import timezone, UTC @@ -34,22 +35,19 @@ class TestLeaveRequests(TestHrHolidaysCommon): self.holidays_type_1 = LeaveType.create({ 'name': 'NotLimitedHR', - 'allocation_type': 'no', + 'requires_allocation': 'no', 'leave_validation_type': 'hr', - 'validity_start': False, }) self.holidays_type_2 = LeaveType.create({ 'name': 'Limited', - 'allocation_type': 'fixed_allocation', + 'requires_allocation': 'yes', + 'employee_requests': 'yes', 'leave_validation_type': 'hr', - 'validity_start': False, }) self.holidays_type_3 = LeaveType.create({ 'name': 'TimeNotLimited', - 'allocation_type': 'no', + 'requires_allocation': 'no', 'leave_validation_type': 'manager', - 'validity_start': fields.Datetime.from_string('2017-01-01 00:00:00'), - 'validity_stop': fields.Datetime.from_string('2017-06-01 00:00:00'), }) self.set_employee_create_date(self.employee_emp_id, '2010-02-03 00:00:00') @@ -111,8 +109,11 @@ class TestLeaveRequests(TestHrHolidaysCommon): 'employee_id': self.employee_emp_id, 'holiday_status_id': self.holidays_type_2.id, 'number_of_days': 2, + 'state': 'confirm', + 'date_from': time.strftime('%Y-1-1'), + 'date_to': time.strftime('%Y-12-31'), }) - aloc1_user_group.action_approve() + aloc1_user_group.action_validate() holiday_status = self.holidays_type_2.with_user(self.user_employee_id) self._check_holidays_status(holiday_status, 2.0, 0.0, 2.0, 2.0) @@ -137,10 +138,21 @@ class TestLeaveRequests(TestHrHolidaysCommon): @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') def test_accrual_validity_time_valid(self): """ Employee ask leave during a valid validity time """ + + self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).create({ + 'name': 'Sick Time Off', + 'holiday_status_id': self.holidays_type_2.id, + 'employee_id': self.employee_emp.id, + 'date_from': fields.Datetime.from_string('2017-01-01 00:00:00'), + 'date_to': fields.Datetime.from_string('2017-06-01 00:00:00'), + 'number_of_days': 10, + 'state': 'validate', + }) + self.env['hr.leave'].with_user(self.user_employee_id).create({ 'name': 'Valid time period', 'employee_id': self.employee_emp_id, - 'holiday_status_id': self.holidays_type_3.id, + 'holiday_status_id': self.holidays_type_2.id, 'date_from': fields.Datetime.from_string('2017-03-03 06:00:00'), 'date_to': fields.Datetime.from_string('2017-03-11 19:00:00'), 'number_of_days': 1, @@ -148,12 +160,12 @@ class TestLeaveRequests(TestHrHolidaysCommon): @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') def test_accrual_validity_time_not_valid(self): - """ Employee ask leav during a not valid validity time """ + """ Employee ask leave when there's no valid allocation """ with self.assertRaises(ValidationError): - self.env['hr.leave'].with_user(self.user_employee_id).create({ + self.env['hr.leave'].with_user(self.user_employee).create({ 'name': 'Sick Time Off', 'employee_id': self.employee_emp_id, - 'holiday_status_id': self.holidays_type_3.id, + 'holiday_status_id': self.holidays_type_2.id, 'date_from': fields.Datetime.from_string('2017-07-03 06:00:00'), 'date_to': fields.Datetime.from_string('2017-07-11 19:00:00'), 'number_of_days': 1, @@ -182,6 +194,8 @@ class TestLeaveRequests(TestHrHolidaysCommon): allocation_form = Form(self.env['hr.leave.allocation'].with_user(self.user_employee)) allocation_form.name = 'New Allocation Request' allocation_form.holiday_status_id = self.holidays_type_2 + allocation_form.date_from = date(2019, 5, 6) + allocation_form.date_to = date(2019, 5, 6) allocation = allocation_form.save() @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') @@ -307,8 +321,11 @@ class TestLeaveRequests(TestHrHolidaysCommon): 'holiday_status_id': leave_type.id, 'number_of_days': 20, 'employee_id': employee.id, + 'state': 'confirm', + 'date_from': time.strftime('2018-1-1'), + 'date_to': time.strftime('%Y-1-1'), }) - allocation.action_approve() + allocation.action_validate() leave1 = self.env['hr.leave'].create({ 'name': 'Holiday 1 week', @@ -374,7 +391,7 @@ class TestLeaveRequests(TestHrHolidaysCommon): 'name': 'Sick', 'request_unit': 'hour', 'leave_validation_type': 'both', - 'allocation_type': 'no', + 'requires_allocation': 'no', }) leave1 = self.env['hr.leave'].create({ 'name': 'Sick 1 week during christmas snif', @@ -428,13 +445,13 @@ class TestLeaveRequests(TestHrHolidaysCommon): # 12 13 14 15 16 17 18 # 19 20 21 22 23 24 25 # 26 27 28 29 30 31 - local_date_from = datetime(2019, 1, 1, 7, 0, 0) - local_date_to = datetime(2019, 1, 1, 19, 0, 0) + local_date_from = datetime(2020, 1, 1, 7, 0, 0) + local_date_to = datetime(2020, 1, 1, 19, 0, 0) for tz in timezones_to_test: self._test_leave_with_tz(tz, local_date_from, local_date_to, 1) # We, Th, Fr, Mo, Tu, We => 6 days - local_date_from = datetime(2019, 1, 1, 7, 0, 0) - local_date_to = datetime(2019, 1, 8, 19, 0, 0) + local_date_from = datetime(2020, 1, 1, 7, 0, 0) + local_date_to = datetime(2020, 1, 8, 19, 0, 0) for tz in timezones_to_test: - self._test_leave_with_tz(tz, local_date_from, local_date_to, 6) + self._test_leave_with_tz(tz, local_date_from, local_date_to, 6)#TODO JUD check why this fails diff --git a/addons/hr_holidays/tests/test_out_of_office.py b/addons/hr_holidays/tests/test_out_of_office.py index 42301a085a5..a71fd55b3fc 100644 --- a/addons/hr_holidays/tests/test_out_of_office.py +++ b/addons/hr_holidays/tests/test_out_of_office.py @@ -18,8 +18,7 @@ class TestOutOfOffice(TestHrHolidaysCommon): self.leave_type = self.env['hr.leave.type'].create({ 'name': 'Legal Leaves', 'time_type': 'leave', - 'allocation_type': 'no', - 'validity_start': False, + 'requires_allocation': 'no', }) def test_leave_ooo(self): @@ -66,8 +65,7 @@ class TestOutOfOfficePerformance(TestHrHolidaysCommon, TransactionCaseWithUserDe self.leave_type = self.env['hr.leave.type'].create({ 'name': 'Legal Leaves', 'time_type': 'leave', - 'allocation_type': 'no', - 'validity_start': False, + 'requires_allocation': 'no', }) self.leave_date_end = (datetime.today() + relativedelta(days=3)) self.leave = self.env['hr.leave'].create({ diff --git a/addons/hr_holidays/views/hr_holidays_views.xml b/addons/hr_holidays/views/hr_holidays_views.xml index 7ff814f70b5..46292c6724d 100644 --- a/addons/hr_holidays/views/hr_holidays_views.xml +++ b/addons/hr_holidays/views/hr_holidays_views.xml @@ -28,21 +28,21 @@ @@ -65,7 +65,7 @@ id="menu_hr_holidays_report" name="Reporting" parent="menu_hr_holidays_root" - groups="hr_holidays.group_hr_holidays_manager" + groups="hr_holidays.group_hr_holidays_user" sequence="4"/> + + + + + + + + + hr.leave.accrual.level.form + hr.leave.accrual.level + +
+ + + + + +
+
+
+ + hr.leave.accrual.plan.tree + hr.leave.accrual.plan + + + + + + + + + + hr.leave.accrual.plan.form + hr.leave.accrual.plan + +
+ +
+ +
+ + + + + + + + + + + + + + +
+
Rules
+
+

No rules added

+

+ Click on the 'Edit' button to add new rules. +

+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ Level +
+ + + + + + + + +
+
+ +
+
+
+ + Starts after allocation start date + + + Starts immediately after allocation start date + +
+
+ Adds + + + on + + + on the and on the + + + on the and on the + + + on + +
+
+ Limit of +
+
+ At the end of the year, unused accruals will be postponedlost +
+
+
+
+
+
+
+
+
+
+
+
+ + + Accrual Plans + ir.actions.act_window + hr.leave.accrual.plan + tree,form + + +
diff --git a/addons/hr_holidays/views/hr_leave_allocation_views.xml b/addons/hr_holidays/views/hr_leave_allocation_views.xml index 1ece8001a32..b4229d1a4e3 100644 --- a/addons/hr_holidays/views/hr_leave_allocation_views.xml +++ b/addons/hr_holidays/views/hr_leave_allocation_views.xml @@ -9,8 +9,7 @@ - - + @@ -41,11 +40,13 @@ ]"/> - + + + @@ -70,9 +71,8 @@ class="oe_stat_button" icon="fa-calendar" type="action" - attrs="{'invisible': [('holiday_type', '!=', 'employee')]}" - name="%(hr_leave_action_action_approve_department)d" - context="{'search_default_employee_id': [employee_id], 'search_default_holiday_status_id': [holiday_status_id], 'default_holiday_status_id': holiday_status_id}" + attrs="{'invisible': ['|', '|', ('id', '=', False), ('holiday_type', '!=', 'employee'), ('employee_id', '=', False)]}" + name="%(hr_leave_action_holiday_allocation_id)d" help="Time off Taken/Total Allocated">
@@ -92,19 +92,31 @@ - + + + +
+
+
+ + +
-
-
+ attrs="{'readonly': ['|', '|', ('type_request_unit', '=', 'hour'), ('state', 'not in', ('draft', 'confirm')), ('allocation_type', '=', 'accrual')], 'invisible': [('type_request_unit', '=', 'hour')]}"/> + attrs="{'readonly': ['|', '|', ('type_request_unit', '!=', 'hour'), ('state', 'not in', ('draft', 'confirm')), ('allocation_type', '=', 'accrual')], 'invisible': [('type_request_unit', '!=', 'hour')]}"/> Days Hours
@@ -133,11 +145,9 @@ 16 -
- - -
@@ -438,29 +461,6 @@ list code - action = records.action_approve() + action = records.action_validate() - - - hr.leave.view.form.dashboard - hr.leave.allocation - - primary - 100 - - - 1 - -
- 1 -
- -
-
-
-
-
- diff --git a/addons/hr_holidays/views/hr_leave_type_views.xml b/addons/hr_holidays/views/hr_leave_type_views.xml index 1ba99173d09..e5d61e974c3 100644 --- a/addons/hr_holidays/views/hr_leave_type_views.xml +++ b/addons/hr_holidays/views/hr_leave_type_views.xml @@ -7,7 +7,6 @@ - @@ -22,16 +21,22 @@
- - +
@@ -40,42 +45,33 @@

- + +

Time Off Requests

- - -
- - - - - - - + + 'invisible': [('leave_validation_type', 'in', ['no_validation', 'manager']), '|', ('requires_allocation', '=', 'no'), ('allocation_validation_type', '=', 'officer')]}"/> + - - - - - - - - - - - - - + +

Allocation Requests

+ + + +
+
+ + + + + +

Visual

+ + +
@@ -111,12 +107,9 @@ - - - - - - + + +
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml index 9efb92b4467..ee551f354b8 100644 --- a/addons/hr_holidays/views/hr_leave_views.xml +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -1,17 +1,6 @@ - - Report to Payslip - - - code - - if records: - records.write({'payslip_status': True}) - - - Manager Approval @@ -64,13 +53,12 @@ - - - - + @@ -83,11 +71,13 @@ + + @@ -106,6 +96,7 @@ +
@@ -150,7 +141,7 @@
-
+
@@ -161,6 +152,11 @@ Refuse
+
+ +
@@ -205,6 +201,7 @@
+
-