diff --git a/addons/hr_holidays/__manifest__.py b/addons/hr_holidays/__manifest__.py index 9bf1da4a2ed..946035e1309 100644 --- a/addons/hr_holidays/__manifest__.py +++ b/addons/hr_holidays/__manifest__.py @@ -29,6 +29,7 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p 'data/report_paperformat.xml', 'data/hr_holidays_data.xml', 'data/mail_data.xml', + 'data/ir_cron_data.xml', 'security/hr_holidays_security.xml', 'security/ir.model.access.csv', @@ -37,7 +38,6 @@ 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_views.xml', 'views/mail_activity_views.xml', 'views/res_users_views.xml', @@ -47,6 +47,8 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p 'report/hr_holidays_templates.xml', 'report/hr_holidays_reports.xml', 'report/hr_leave_reports.xml', + + 'views/hr_views.xml', ], 'demo': [ 'data/hr_holidays_demo.xml', diff --git a/addons/hr_holidays/data/hr_holidays_data.xml b/addons/hr_holidays/data/hr_holidays_data.xml index a08a0eddb3d..20ae0403cd6 100644 --- a/addons/hr_holidays/data/hr_holidays_data.xml +++ b/addons/hr_holidays/data/hr_holidays_data.xml @@ -30,8 +30,8 @@ Unpaid - brown + diff --git a/addons/hr_holidays/data/hr_holidays_demo.xml b/addons/hr_holidays/data/hr_holidays_demo.xml index f3c7407534a..ae743c74f0d 100644 --- a/addons/hr_holidays/data/hr_holidays_demo.xml +++ b/addons/hr_holidays/data/hr_holidays_demo.xml @@ -3,19 +3,28 @@ - Overtime Compensation - - blue - - - hour + Overtime Compensation + + blue + + + hour - Parental Leaves - - brown - both + Parental Leaves + + brown + both + + + + + + wheat + + + diff --git a/addons/hr_holidays/data/ir_cron_data.xml b/addons/hr_holidays/data/ir_cron_data.xml new file mode 100644 index 00000000000..a162d5c51f5 --- /dev/null +++ b/addons/hr_holidays/data/ir_cron_data.xml @@ -0,0 +1,14 @@ + + + + + Accrual Leave: Updates the number of leaves + + code + model._update_accrual() + 1 + days + -1 + + + diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr.py index 17b7cd93785..d59458c49c3 100644 --- a/addons/hr_holidays/models/hr.py +++ b/addons/hr_holidays/models/hr.py @@ -6,6 +6,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError +from odoo.tools.float_utils import float_round class Department(models.Model): @@ -112,7 +113,7 @@ class Employee(models.Model): def _compute_remaining_leaves(self): remaining = self._get_remaining_leaves() for employee in self: - employee.remaining_leaves = remaining.get(employee.id, 0.0) + employee.remaining_leaves = float_round(remaining.get(employee.id, 0.0), precision_digits=2) @api.multi def _inverse_remaining_leaves(self): @@ -132,7 +133,7 @@ class Employee(models.Model): if not status: continue # if a status is found, then compute remaing leave for current employee - difference = employee.remaining_leaves - actual_remaining.get(employee.id, 0) + difference = float_round(employee.remaining_leaves - actual_remaining.get(employee.id, 0), precision_digits=2) if difference > 0: leave = self.env['hr.leave.allocation'].create({ 'name': _('Allocation for %s') % employee.name, @@ -179,7 +180,7 @@ class Employee(models.Model): ], fields=['number_of_days', 'employee_id'], groupby=['employee_id']) mapping = dict([(leave['employee_id'][0], leave['number_of_days']) for leave in all_leaves]) for employee in self: - employee.leaves_count = mapping.get(employee.id) + employee.leaves_count = float_round(mapping.get(employee.id, 0), precision_digits=2) @api.multi def _compute_show_leaves(self): diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index f0359744ebb..9be317b69ca 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -44,7 +44,7 @@ class HolidaysRequest(models.Model): def default_get(self, fields_list): defaults = super(HolidaysRequest, self).default_get(fields_list) - LeaveType = self.env['hr.leave.type'].with_context(employee_id=defaults.get('employee_id'), default_date_from=defaults.get('date_from')) + LeaveType = self.env['hr.leave.type'].with_context(employee_id=defaults.get('employee_id'), default_date_from=defaults.get('date_from', fields.Datetime.now())) lt = LeaveType.search([('valid', '=', True), ('employee_applicability', 'in', ['leave', 'both'])]) defaults['holiday_status_id'] = lt[0].id if len(lt) > 0 else defaults.get('holiday_status_id') @@ -325,7 +325,7 @@ class HolidaysRequest(models.Model): @api.constrains('state', 'number_of_days_temp', 'holiday_status_id') def _check_holidays(self): for holiday in self: - if holiday.holiday_type != 'employee' or not holiday.employee_id or holiday.holiday_status_id.limit: + if holiday.holiday_type != 'employee' or not holiday.employee_id or holiday.holiday_status_id.limit or holiday.holiday_status_id.negative: continue leave_days = holiday.holiday_status_id.get_days(holiday.employee_id.id)[holiday.holiday_status_id.id] if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1 or \ diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index e5b63e4215d..32c656a77a9 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -5,10 +5,17 @@ import logging +import pytz + +from datetime import datetime, time +from dateutil.relativedelta import relativedelta + from odoo import api, fields, models from odoo.exceptions import UserError from odoo.tools.translate import _ +from odoo.addons.resource.models.resource import HOURS_PER_DAY + _logger = logging.getLogger(__name__) @@ -54,6 +61,10 @@ class HolidaysAllocation(models.Model): "\nThe status is 'To Approve', when leave request is confirmed by user." + "\nThe status is 'Refused', when leave request is refused by manager." + "\nThe status is 'Approved', when leave request is approved by manager.") + date_from = fields.Datetime('Start Date', readonly=True, index=True, copy=False, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange') + date_to = fields.Datetime('End Date', readonly=True, copy=False, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange') holiday_status_id = fields.Many2one("hr.leave.type", string="Leave Type", required=True, readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, domain="[('valid', '=', True), ('employee_applicability', 'in', ['allocation', 'both']), ('limit', '=', False)]", default=_default_holiday_status_id) @@ -86,6 +97,19 @@ class HolidaysAllocation(models.Model): can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve') type_request_unit = fields.Selection(related='holiday_status_id.request_unit') + accrual = fields.Boolean("Accrual", related='holiday_status_id.accrual', store=True, readonly=True) + number_per_interval = fields.Float("Number of unit per interval", readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) + interval_number = fields.Integer("Number of unit between two intervals", readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) + unit_per_interval = fields.Selection([ + ('hours', 'Hour(s)'), + ('days', 'Day(s)') + ], string="Unit of time added at each interval", default='hours', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) + interval_unit = fields.Selection([ + ('weeks', 'Week(s)'), + ('months', 'Month(s)'), + ('years', 'Year(s)') + ], string="Unit of time between two intervals", default='weeks', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) + nextcall = fields.Date("Date of the next accrual allocation", default=False, readonly=True) _sql_constraints = [ ('type_value', "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) or (holiday_type='category' AND category_id IS NOT NULL) or (holiday_type='department' AND department_id IS NOT NULL) )", @@ -93,14 +117,80 @@ class HolidaysAllocation(models.Model): ('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days 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([('accrual', '=', True), ('state', '=', 'validate'), + '|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()), + '|', ('nextcall', '=', False), ('nextcall', '<=', today)]) + + for holiday in holidays: + values = {} + + delta = relativedelta(days=0) + + if holiday.interval_unit == 'weeks': + delta = relativedelta(weeks=holiday.interval_number) + if holiday.interval_unit == 'months': + delta = relativedelta(months=holiday.interval_number) + if holiday.interval_unit == 'years': + delta = relativedelta(years=holiday.interval_number) + + values['nextcall'] = (holiday.nextcall if holiday.nextcall else today) + delta + + period_start = datetime.combine(today, time(0, 0, 0)) - delta + period_end = datetime.combine(today, time(0, 0, 0)) + + # We have to check when the employee has been created + # in order to not allocate him/her too much leaves + creation_date = fields.Datetime.from_string(holiday.employee_id.create_date) + + # If employee is created after the period, we cancel the computation + if period_end <= creation_date: + holiday.write(values) + continue + + # If employee created during the period, taking the date at which he has been created + if period_start <= creation_date: + period_start = creation_date + + worked = holiday.employee_id.get_work_days_data(period_start, period_end, domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')])['days'] + left = holiday.employee_id.get_leave_days_data(period_start, period_end, domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')])['days'] + prorata = worked / (left + worked) if worked else 0 + + days_to_give = holiday.number_per_interval + if holiday.unit_per_interval == 'hours': + # As we encode everything in days in the database we need to convert + # the number of hours into days for this we use the + # mean number of hours set on the employee's calendar + days_to_give = days_to_give / holiday.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY + + values['number_of_days_temp'] = holiday.number_of_days_temp + days_to_give * prorata + + if holiday.holiday_status_id.balance_limit > 0: + values['number_of_days_temp'] = min(values['number_of_days_temp'], holiday.holiday_status_id.balance_limit) + + values['number_of_hours'] = values['number_of_days_temp'] * holiday.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY + + holiday.write(values) + @api.multi - @api.depends('number_of_days_temp', 'type_request_unit', 'number_of_hours') + @api.depends('number_of_days_temp', 'type_request_unit', 'number_of_hours', 'holiday_status_id', 'employee_id') def _compute_number_of_days(self): for holiday in self: + number_of_days = holiday.number_of_days_temp if holiday.type_request_unit == 'hour': - holiday.number_of_days = holiday.number_of_hours / holiday.employee_id.resource_calendar_id.hours_per_day - else: - holiday.number_of_days = holiday.number_of_days_temp + # In this case we need the number of days to reflect the number of hours taken + number_of_days = holiday.number_of_hours / holiday.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY + if holiday.holiday_status_id.balance_limit > 0: + number_of_days = min(number_of_days, holiday.holiday_status_id.balance_limit) + + holiday.number_of_days = number_of_days @api.multi def _compute_can_reset(self): @@ -149,6 +239,23 @@ class HolidaysAllocation(models.Model): def _onchange_number_of_hours(self): self.number_of_days_temp = self.number_of_hours / self.employee_id.resource_calendar_id.hours_per_day + @api.onchange('holiday_status_id') + def _onchange_holiday_status_id(self): + self.date_to = self.holiday_status_id.validity_stop + + if self.accrual: + self.number_of_days_temp = 0 + + if self.holiday_status_id.request_unit == 'hour': + self.unit_per_interval = 'hours' + else: + self.unit_per_interval = 'days' + else: + self.interval_number = 0 + self.interval_unit = 'weeks' + self.number_per_interval = 0 + self.unit_per_interval = 'hours' + #################################################### # ORM Overrides methods #################################################### @@ -185,6 +292,8 @@ class HolidaysAllocation(models.Model): @api.model def create(self, values): """ Override to avoid automatic logging of creation """ + if values.get('accrual', False): + values['date_from'] = fields.Datetime.now() 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}) diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index c31e0355d9f..17d5c9b190f 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -8,6 +8,7 @@ import logging from odoo import api, fields, models from odoo.exceptions import ValidationError from odoo.tools.translate import _ +from odoo.tools.float_utils import float_round _logger = logging.getLogger(__name__) @@ -76,29 +77,61 @@ class HolidaysType(models.Model): valid = 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', 'Leave'), ('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', 'Half-day'), ('hour', 'Hours')], default='day', string='Take Leaves in', required=True) + accrual = fields.Boolean('Is Accrual', default=False, + help='This option forces this type of leave to be allocated accrually') + + unpaid = fields.Boolean('Is Unpaid', default=False) + + negative = fields.Boolean('Allow Negative', help="This option allows to take more leaves than allocated") + + balance_limit = fields.Float('Max Balance Limit', default=0, help="The maximum quantity of allocated days on this allocation, zero meaning infinite amount") + + _sql_constraints = [ + ('no_negative_balance_limit', "CHECK(balance_limit >= 0)", "The max balance limit cannot be negative"), + ('no_accrual_unpaid', 'CHECK(NOT (accrual AND unpaid))', "A leave type cannot be accrual and considered as unpaid leaves") + ] + @api.multi @api.constrains('validity_start', 'validity_stop') def _check_validity_dates(self): - for htype in self: - if htype.validity_start and htype.validity_stop and \ - htype.validity_start > htype.validity_stop: + 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.multi + @api.constrains('balance_limit', 'accrual') + def _check_balance_limit(self): + for leave_type in self: + if not leave_type.accrual and leave_type.balance_limit > 0: + raise ValidationError(_("Max balance limit can only be set for accrual leaves")) + @api.onchange('limit') def _onchange_limit(self): if self.limit: self.employee_applicability = 'leave' + self.accrual = False + + @api.onchange('accrual') + def _onchange_accrual(self): + if self.accrual: + self.limit = False + self.employee_applicability = 'both' + else: + self.negative = False + self.balance_limit = 0 @api.multi @api.depends('validity_start', 'validity_stop', 'limit') def _compute_valid(self): - dt = self._context.get('default_date_from', fields.Date.today()) + dt = self._context.get('default_date_from') or fields.Datetime.now() for holiday_type in self: if holiday_type.validity_start and holiday_type.validity_stop: @@ -109,7 +142,8 @@ class HolidaysType(models.Model): holiday_type.valid = True def _search_valid(self, operator, value): - dt = self._context.get('default_date_from', fields.Date.today()) or fields.Date.today() + dt = self._context.get('default_date_from') or fields.Datetime.now() + signs = ['>=', '<='] if operator == '=' else ['<=', '>='] return ['|', ('validity_stop', operator, False), '&', @@ -181,7 +215,7 @@ class HolidaysType(models.Model): if not record.limit: name = "%(name)s (%(count)s)" % { 'name': name, - 'count': _('%g remaining out of %g') % (record.virtual_remaining_leaves or 0.0, record.max_leaves or 0.0) + 'count': _('%g remaining out of %g') % (float_round(record.virtual_remaining_leaves, precision_digits=2) or 0.0, float_round(record.max_leaves, precision_digits=2) or 0.0) } res.append((record.id, name)) return res diff --git a/addons/hr_holidays/report/hr_leave_reports.xml b/addons/hr_holidays/report/hr_leave_reports.xml index 5afe663eaf5..4694fe392dd 100644 --- a/addons/hr_holidays/report/hr_leave_reports.xml +++ b/addons/hr_holidays/report/hr_leave_reports.xml @@ -150,7 +150,7 @@ {'search_default_employee_id': [active_id], 'default_employee_id': active_id, 'search_default_group_type': 1, 'search_default_year': 1} [('holiday_type','=','employee'), ('holiday_status_id.limit', '=', False), ('state','!=', 'refuse')] - + + + @@ -52,6 +54,7 @@
+