diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py index e36e6246ff3..06437298ed5 100644 --- a/addons/hr/models/hr.py +++ b/addons/hr/models/hr.py @@ -229,16 +229,17 @@ class Employee(models.Model): if self.user_id: self.update(self._sync_user(self.user_id)) - @api.onchange('user_id', 'resource_calendar_id') + @api.onchange('resource_calendar_id') def _onchange_timezone(self): - if self.user_id or self.resource_calendar_id: - self.tz = self.user_id.tz or self.resource_calendar_id.tz + if self.resource_calendar_id and not self.tz: + self.tz = self.resource_calendar_id.tz def _sync_user(self, user): return dict( name=user.name, image=user.image, work_email=user.email, + tz=user.tz, ) @api.model @@ -254,6 +255,8 @@ class Employee(models.Model): account_id = vals.get('bank_account_id') or self.bank_account_id.id if account_id: self.env['res.partner.bank'].browse(account_id).partner_id = vals['address_home_id'] + if vals.get('user_id'): + vals.update(self._sync_user(self.env['res.users'].browse(vals['user_id']))) tools.image_resize_images(vals) return super(Employee, self).write(vals) diff --git a/addons/hr/models/res_users.py b/addons/hr/models/res_users.py index b47d2160ba9..2e5acfe8ee7 100644 --- a/addons/hr/models/res_users.py +++ b/addons/hr/models/res_users.py @@ -2,11 +2,9 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, models, fields -from odoo import SUPERUSER_ID class User(models.Model): - _inherit = ['res.users'] employee_ids = fields.One2many('hr.employee', 'user_id', string='Related employees') @@ -19,11 +17,11 @@ class User(models.Model): @api.multi def write(self, vals): - """ When renaming admin user, we want its new name propagated to its related employees """ + """ Synchronize user and its related employee """ result = super(User, self).write(vals) - Employee = self.env['hr.employee'] - if vals.get('name'): - for user in self.filtered(lambda user: user.id == SUPERUSER_ID): - employees = Employee.search([('user_id', '=', user.id)]) - employees.write({'name': vals['name']}) + employee_values = {} + for fname in [f for f in ['name', 'email', 'image', 'tz'] if f in vals]: + employee_values[fname] = vals[fname] + if employee_values: + self.env['hr.employee'].sudo().search([('user_id', 'in', self.ids)]).write(employee_values) return result diff --git a/addons/hr_holidays/data/hr_holidays_data.xml b/addons/hr_holidays/data/hr_holidays_data.xml index 20ae0403cd6..57b2fdb57c5 100644 --- a/addons/hr_holidays/data/hr_holidays_data.xml +++ b/addons/hr_holidays/data/hr_holidays_data.xml @@ -1,11 +1,10 @@ - - + - + fixed black @@ -14,14 +13,16 @@ Sick Leaves - + no red + Compensatory Days - + fixed_allocation + hour lavender @@ -30,8 +31,11 @@ Unpaid + fixed_allocation + both brown + diff --git a/addons/hr_holidays/data/hr_holidays_demo.xml b/addons/hr_holidays/data/hr_holidays_demo.xml index 0ab4e8a574e..c28ed7bc6fe 100644 --- a/addons/hr_holidays/data/hr_holidays_demo.xml +++ b/addons/hr_holidays/data/hr_holidays_demo.xml @@ -4,7 +4,7 @@ Overtime Compensation - + fixed_allocation blue @@ -13,24 +13,23 @@ Parental Leaves - + fixed brown both - + fixed wheat - Legal Leaves for Peter Parker - 20 + 20 confirm @@ -38,7 +37,7 @@ International Tour - 7 + 7 confirm @@ -46,15 +45,14 @@ Overtime Compensation - 160 - 20 + 20 Summer Vacation - 7 + 7 @@ -83,7 +81,7 @@ - 3 + 3 @@ -92,7 +90,7 @@ - 3 + 3 confirm diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr.py index 5c19745e412..f879a0c0ef4 100644 --- a/addons/hr_holidays/models/hr.py +++ b/addons/hr_holidays/models/hr.py @@ -61,10 +61,10 @@ class Department(models.Model): class Employee(models.Model): - _inherit = "hr.employee" - remaining_leaves = fields.Float(compute='_compute_remaining_leaves', string='Remaining Legal Leaves', inverse='_inverse_remaining_leaves', + remaining_leaves = fields.Float( + compute='_compute_remaining_leaves', string='Remaining Legal Leaves', help='Total number of legal leaves allocated to this employee, change this value to create allocation/leave request. ' 'Total based on all the leave types without overriding limit.') current_leave_state = fields.Selection(compute='_compute_leave_status', string="Current Leave Status", @@ -97,14 +97,14 @@ class Employee(models.Model): state, employee_id FROM hr_leave_allocation UNION - SELECT holiday_status_id, number_of_days, + SELECT holiday_status_id, (number_of_days * -1) as number_of_days, state, employee_id FROM hr_leave ) h join hr_leave_type s ON (s.id=h.holiday_status_id) WHERE h.state='validate' AND - s.limit=False AND + (s.allocation_type='fixed' OR s.allocation_type='fixed_allocation') 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()) @@ -115,39 +115,6 @@ class Employee(models.Model): for employee in self: employee.remaining_leaves = float_round(remaining.get(employee.id, 0.0), precision_digits=2) - @api.multi - def _inverse_remaining_leaves(self): - status_list = self.env['hr.leave.type'].search([('limit', '=', False)]) - # Create leaves (adding remaining leaves) or raise (reducing remaining leaves) - actual_remaining = self._get_remaining_leaves() - for employee in self.filtered(lambda employee: employee.remaining_leaves): - # check the status list. This is done here and not before the loop to avoid raising - # exception on employee creation (since we are in a computed field). - if len(status_list) != 1: - raise UserError(_("The feature behind the field 'Remaining Legal Leaves' can only be used when there is only one " - "leave type with the option 'Allow to Override Limit' unchecked. (%s Found). " - "Otherwise, the update is ambiguous as we cannot decide on which leave type the update has to be done. " - "\n You may prefer to use the classic menus 'Leave Requests' and 'Allocation Requests' located in Leaves Application " - "to manage the leave days of the employees if the configuration does not allow to use this field.") % (len(status_list))) - status = status_list[0] if status_list else None - if not status: - continue - # if a status is found, then compute remaing leave for current employee - 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, - 'employee_id': employee.id, - 'holiday_status_id': status.id, - 'holiday_type': 'employee', - 'number_of_days_temp': difference - }) - leave.action_approve() - if leave.validation_type == 'both': - leave.action_validate() - elif difference < 0: - raise UserError(_('You cannot reduce validated allocation requests.')) - @api.multi def _compute_leave_status(self): # Used SUPERUSER_ID to forcefully get status of other user's leave, to bypass record rule @@ -175,7 +142,6 @@ class Employee(models.Model): def _compute_leaves_count(self): all_leaves = self.env['hr.leave.report'].read_group([ ('employee_id', 'in', self.ids), - ('holiday_status_id.limit', '=', False), ('state', '=', 'validate') ], fields=['number_of_days', 'employee_id'], groupby=['employee_id']) mapping = dict([(leave['employee_id'][0], leave['number_of_days']) for leave in all_leaves]) diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 078c0d7f460..902ba6afd28 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -6,16 +6,15 @@ import logging import math -from datetime import timedelta, datetime, time +from datetime import datetime from pytz import timezone, UTC from odoo import api, fields, models +from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools import float_compare from odoo.tools.translate import _ -from odoo.addons.resource.models.resource import float_to_time - _logger = logging.getLogger(__name__) @@ -60,17 +59,13 @@ class HolidaysRequest(models.Model): _inherit = ['mail.thread', 'mail.activity.mixin'] _mail_post_access = 'read' - def _default_domain_holiday_status(self): - if self.user_has_groups('hr_holidays.group_hr_holidays_manager'): - return [('valid', '=', True)] - return [('valid', '=', True), ('employee_applicability', 'in', ['leave', 'both'])] - @api.model def default_get(self, fields_list): defaults = super(HolidaysRequest, self).default_get(fields_list) + defaults = self._default_get_request_parameters(defaults) 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(self._default_domain_holiday_status()) + lt = LeaveType.search([('valid', '=', True)]) defaults['holiday_status_id'] = lt[0].id if len(lt) > 0 else defaults.get('holiday_status_id') return defaults @@ -78,6 +73,26 @@ class HolidaysRequest(models.Model): def _default_employee(self): return self.env.context.get('default_employee_id') or self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + def _default_get_request_parameters(self, values): + new_values = dict(values) + global_from, global_to = False, False + # TDE FIXME: consider a mapping on several days that is not the standard + # calendar widget 7-19 in user's TZ is some custom input + if values.get('date_from'): + user_tz = self.env.user.tz or 'UTC' + localized_dt = timezone('UTC').localize(values['date_from']).astimezone(timezone(user_tz)) + global_from = localized_dt.time().hour == 7 and localized_dt.time().minute == 0 + new_values['request_date_from'] = values['date_from'].date() + if values.get('date_to'): + user_tz = self.env.user.tz or 'UTC' + localized_dt = timezone('UTC').localize(values['date_to']).astimezone(timezone(user_tz)) + global_to = localized_dt.time().hour == 19 and localized_dt.time().minute == 0 + new_values['request_date_to'] = values['date_to'].date() + if global_from and global_to: + new_values['request_unit_custom'] = True + return new_values + + # description name = fields.Char('Description') state = fields.Selection([ ('draft', 'To Submit'), @@ -87,233 +102,261 @@ class HolidaysRequest(models.Model): ('validate1', 'Second Approval'), ('validate', 'Approved') ], string='Status', readonly=True, track_visibility='onchange', copy=False, default='confirm', - help="The status is set to 'To Submit', when a leave request is created." + - "\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.") - payslip_status = fields.Boolean('Reported in last payslips', - help='Green this button when the leave has been taken into account in the payslip.') + help="The status is set to 'To Submit', when a leave request is created." + + "\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.") + payslip_status = fields.Boolean('Reported in last payslips', help='Green this button when the leave has been taken into account in the payslip.') report_note = fields.Text('HR Comments') user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, store=True, default=lambda self: self.env.uid, readonly=True) - date_from = fields.Datetime('Start Date', readonly=True, index=True, copy=False, required=True, - states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange') - date_to = fields.Datetime('End Date', readonly=True, copy=False, required=True, - 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, + # leave type configuration + holiday_status_id = fields.Many2one( + "hr.leave.type", string="Leave Type", required=True, readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, - domain=lambda self: self._default_domain_holiday_status()) - employee_id = fields.Many2one('hr.employee', string='Employee', index=True, readonly=True, + domain=[('valid', '=', True)]) + leave_type_request_unit = fields.Selection(related='holiday_status_id.request_unit', readonly=True) + validation_type = fields.Selection('Validation Type', related='holiday_status_id.validation_type') + # HR data + employee_id = fields.Many2one( + 'hr.employee', string='Employee', index=True, readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, default=_default_employee, track_visibility='onchange') manager_id = fields.Many2one('hr.employee', string='Manager', readonly=True) + department_id = fields.Many2one( + 'hr.department', string='Department', readonly=True, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) - number_of_days_temp = fields.Float( - 'Duration (Days)', copy=False, readonly=True, + # duration + date_from = fields.Datetime( + 'Start Date', readonly=True, index=True, copy=False, required=True, + default=fields.Datetime.now, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange') + date_to = fields.Datetime( + 'End Date', readonly=True, copy=False, required=True, + default=fields.Datetime.now, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange') + number_of_days = fields.Float( + 'Duration (Days)', copy=False, readonly=True, track_visibility='onchange', states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help='Number of days of the leave request according to your working schedule.') - number_of_days = fields.Float('Number of Days', compute='_compute_number_of_days', store=True, track_visibility='onchange') - number_of_hours = fields.Float( - 'Duration (Hours)', copy=False, readonly=True, compute='_compute_number_of_hours', - help='Number of hours of the leave request according to your working schedule.') + number_of_days_display = fields.Float( + 'Duration in days', compute='_compute_number_of_days_display', copy=False, readonly=True, + help='Number of days of the leave request. Used for interface.') + number_of_hours_display = fields.Float( + 'Duration in hours', compute='_compute_number_of_hours_display', copy=False, readonly=True, + help='Number of hours of the leave request according to your working schedule. Used for interface.') + # details meeting_id = fields.Many2one('calendar.event', string='Meeting') - parent_id = fields.Many2one('hr.leave', string='Parent', copy=False) linked_request_ids = fields.One2many('hr.leave', 'parent_id', string='Linked Requests') - department_id = fields.Many2one('hr.department', string='Department', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) - category_id = fields.Many2one('hr.employee.category', string='Employee Tag', readonly=True, - states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help='Category of Employee') holiday_type = fields.Selection([ ('employee', 'By Employee'), + ('company', 'By Company'), ('department', 'By Department'), - ('category', 'By Employee Tag') - ], string='Allocation Mode', readonly=True, required=True, default='employee', + ('category', 'By Employee Tag')], + 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') - first_approver_id = fields.Many2one('hr.employee', string='First Approval', readonly=True, copy=False, + category_id = fields.Many2one( + 'hr.employee.category', string='Employee Tag', readonly=True, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help='Category of Employee') + mode_company_id = fields.Many2one( + 'res.company', string='Company', readonly=True, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) + first_approver_id = fields.Many2one( + 'hr.employee', string='First Approval', readonly=True, copy=False, help='This area is automatically filled by the user who validate the leave', oldname='manager_id') - second_approver_id = fields.Many2one('hr.employee', string='Second Approval', readonly=True, copy=False, oldname='manager_id2', + second_approver_id = fields.Many2one( + 'hr.employee', string='Second Approval', readonly=True, copy=False, oldname='manager_id2', help='This area is automaticly filled by the user who validate the leave with second level (If Leave type need second validation)') - validation_type = fields.Selection('Validation Type', related='holiday_status_id.validation_type') can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve') - # Those fields are mostly for the interface only - + # UX fields + leave_type_request_unit = fields.Selection(related='holiday_status_id.request_unit', readonly=True) # Interface fields used when not using hour-based computation request_date_from = fields.Date('Request Start Date') request_date_to = fields.Date('Request End Date') - - leave_type_request_unit = fields.Selection(related='holiday_status_id.request_unit', readonly=True) - - # These fields are only used only when the leave is taken in half days - request_date_from_period = fields.Selection([('am', 'Morning'), - ('pm', 'Afternoon')], string="Date Period Start", default='am') - request_date_to_period = fields.Selection([('am', 'Morning'), - ('pm', 'Afternoon')], string="Date Period End", default='pm') - - request_unit_all = fields.Selection([('half', 'Half-day'), - ('day', '1 Day'), - ('period', 'Period')]) - # Duplicate field because we cannot hide some entries of a selection field - request_unit_day = fields.Selection([('day', '1 Day'), - ('period', 'Period')], default='day') + # Interface fields used when using hour-based computation + request_hour_from = fields.Selection([ + (0, '12:00 PM'), (0.5, '0:30 AM'), + (1, '1:00 AM'), (1.5, '1:30 AM'), + (2, '2:00 AM'), (2.5, '2:30 AM'), + (3, '3:00 AM'), (3.5, '3:30 AM'), + (4, '4:00 AM'), (4.5, '4:30 AM'), + (5, '5:00 AM'), (5.5, '5:30 AM'), + (6, '6:00 AM'), (6.5, '6:30 AM'), + (7, '7:00 AM'), (7.5, '7:30 AM'), + (8, '8:00 AM'), (8.5, '8:30 AM'), + (9, '9:00 AM'), (9.5, '9:30 AM'), + (10, '10:00 AM'), (10.5, '10:30 AM'), + (11, '11:00 AM'), (11.5, '11:30 AM'), + (12, '12:00 AM'), (12.5, '0:30 PM'), + (13, '1:00 PM'), (13.5, '1:30 PM'), + (14, '2:00 PM'), (14.5, '2:30 PM'), + (15, '3:00 PM'), (15.5, '3:30 PM'), + (16, '4:00 PM'), (16.5, '4:30 PM'), + (17, '5:00 PM'), (17.5, '5:30 PM'), + (18, '6:00 PM'), (18.5, '6:30 PM'), + (19, '7:00 PM'), (19.5, '7:30 PM'), + (20, '8:00 PM'), (20.5, '8:30 PM'), + (21, '9:00 PM'), (21.5, '9:30 PM'), + (22, '10:00 PM'), (22.5, '10:30 PM'), + (23, '11:00 PM'), (23.5, '11:30 PM')], string='Hour from') + request_hour_to = fields.Selection([ + (0, '12:00 PM'), (0.5, '0:30 AM'), + (1, '1:00 AM'), (1.5, '1:30 AM'), + (2, '2:00 AM'), (2.5, '2:30 AM'), + (3, '3:00 AM'), (3.5, '3:30 AM'), + (4, '4:00 AM'), (4.5, '4:30 AM'), + (5, '5:00 AM'), (5.5, '5:30 AM'), + (6, '6:00 AM'), (6.5, '6:30 AM'), + (7, '7:00 AM'), (7.5, '7:30 AM'), + (8, '8:00 AM'), (8.5, '8:30 AM'), + (9, '9:00 AM'), (9.5, '9:30 AM'), + (10, '10:00 AM'), (10.5, '10:30 AM'), + (11, '11:00 AM'), (11.5, '11:30 AM'), + (12, '12:00 AM'), (12.5, '0:30 PM'), + (13, '1:00 PM'), (13.5, '1:30 PM'), + (14, '2:00 PM'), (14.5, '2:30 PM'), + (15, '3:00 PM'), (15.5, '3:30 PM'), + (16, '4:00 PM'), (16.5, '4:30 PM'), + (17, '5:00 PM'), (17.5, '5:30 PM'), + (18, '6:00 PM'), (18.5, '6:30 PM'), + (19, '7:00 PM'), (19.5, '7:30 PM'), + (20, '8:00 PM'), (20.5, '8:30 PM'), + (21, '9:00 PM'), (21.5, '9:30 PM'), + (22, '10:00 PM'), (22.5, '10:30 PM'), + (23, '11:00 PM'), (23.5, '11:30 PM')], string='Hour to') + # used only when the leave is taken in half days + request_date_from_period = fields.Selection([ + ('am', 'Morning'), ('pm', 'Afternoon')], + string="Date Period Start", default='am') + # request type + request_unit_half = fields.Boolean('Half Day') + request_unit_hours = fields.Boolean('Custom Hours') + request_unit_custom = fields.Boolean('Days-long custom hours') _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) )", - "The employee, department or employee category of this request is missing. Please make sure that your user login is linked to an employee."), + ('type_value', + "CHECK((holiday_type='employee' AND employee_id IS NOT NULL) 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) )", + "The employee, department, company or employee category of this request is missing. Please make sure that your user login is linked to an employee."), ('date_check2', "CHECK ((date_from <= date_to))", "The start date must be anterior to the end date."), - ('date_check', "CHECK ( number_of_days_temp >= 0 )", "If you want to change the number of days you should use the 'period' mode"), + ('duration_check', "CHECK ( number_of_days >= 0 )", "If you want to change the number of days you should use the 'period' mode"), ] - @api.onchange('request_unit_all', 'request_date_from_period', 'request_date_to_period', - 'holiday_status_id', 'request_date_from', 'request_date_to', 'employee_id') + @api.onchange('request_date_from_period', 'request_hour_from', 'request_hour_to', + 'request_date_from', 'request_date_to', + 'employee_id') def _onchange_request_parameters(self): - date_from = False - date_to = False - - if not self.employee_id: + if not self.request_date_from: + self.date_from = False return - if self.request_date_from: - if self.date_from: - date_from = fields.Datetime.to_string(datetime.combine(fields.Date.from_string(self.request_date_from), fields.Datetime.from_string(self.date_from).time())) - else: - date_from = datetime.combine(self.request_date_from, time.min) + if self.request_unit_half or self.request_unit_hours: + self.request_date_to = self.request_date_from - if self.request_date_to: - if self.date_to: - date_to = fields.Datetime.to_string(datetime.combine(fields.Date.from_string(self.request_date_to), fields.Datetime.from_string(self.date_to).time())) - else: - date_to = datetime.combine(self.request_date_to, time.max) - - if not self.request_date_from or not self.request_date_to: - if date_from: - self.date_from = date_from - if date_to: - self.date_to = date_to - self.number_of_days_temp = 0 + if not self.request_date_to: + self.date_to = False return - domain = [('calendar_id', '=', self.employee_id.resource_calendar_id.id)] + domain = [('calendar_id', '=', self.employee_id.resource_calendar_id.id or self.env.user.company_id.resource_calendar_id.id)] attendances = self.env['resource.calendar.attendance'].search(domain, order='dayofweek, day_period DESC') - first_day = fields.Date.from_string(date_from) - last_day = fields.Date.from_string(date_to) - - if self.request_unit_all in ['day', 'half']: - last_day = first_day - # find first attendance coming after first_day - attendance_from = next((att for att in attendances if int(att.dayofweek) >= first_day.weekday()), attendances[0]) + attendance_from = next((att for att in attendances if int(att.dayofweek) >= self.request_date_from.weekday()), attendances[0]) # find last attendance coming before last_day - attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= last_day.weekday()), attendances[-1]) + attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= self.request_date_to.weekday()), attendances[-1]) - if self.request_unit_all == 'day' or (self.request_unit_all == 'period' and self.leave_type_request_unit == 'day'): + if self.request_unit_half: + if self.request_date_from_period == 'am': + hour_from = float_to_time(attendance_from.hour_from) + hour_to = float_to_time(attendance_from.hour_to) + else: + hour_from = float_to_time(attendance_to.hour_from) + hour_to = float_to_time(attendance_to.hour_to) + elif self.request_unit_hours: + hour_from = float_to_time(self.request_hour_from) + hour_to = float_to_time(self.request_hour_to) + elif self.request_unit_custom: + hour_from = self.date_from.time() + hour_to = self.date_to.time() + else: hour_from = float_to_time(attendance_from.hour_from) hour_to = float_to_time(attendance_to.hour_to) - elif self.request_unit_all == 'half': - hour_from = float_to_time(attendance_from.hour_from if self.request_date_from_period == 'am' else attendance_to.hour_from) - hour_to = float_to_time(attendance_from.hour_to if self.request_date_from_period == 'am' else attendance_to.hour_to) + tz = self.env.user.tz if self.env.user.tz and not self.request_unit_custom else 'UTC' # custom -> already in UTC + self.date_from = timezone(tz).localize(datetime.combine(self.request_date_from, hour_from)).astimezone(UTC).replace(tzinfo=None) + self.date_to = timezone(tz).localize(datetime.combine(self.request_date_to, hour_to)).astimezone(UTC).replace(tzinfo=None) - elif self.request_unit_all == 'period' and self.leave_type_request_unit == 'half': - hour_from = float_to_time(attendance_from.hour_from if self.request_date_from_period == 'am' else attendance_from.hour_to) - hour_to = float_to_time(attendance_to.hour_from if self.request_date_to_period == 'am' else attendance_to.hour_to) + @api.onchange('request_unit_half') + def _onchange_request_unit_half(self): + if self.request_unit_half: + self.request_unit_hours = False + self.request_unit_custom = False + self._onchange_request_parameters() - if self.leave_type_request_unit == 'hour' and self.request_unit_all == 'period': - date_from = fields.Datetime.from_string(date_from) - date_to = fields.Datetime.from_string(date_to) - else: - tz = self.env.user.tz or 'UTC' - date_from = timezone(tz).localize(datetime.combine(first_day, hour_from)).astimezone(UTC) - date_to = timezone(tz).localize(datetime.combine(last_day, hour_to)).astimezone(UTC) + @api.onchange('request_unit_hours') + def _onchange_request_unit_hours(self): + if self.request_unit_hours: + self.request_unit_half = False + self.request_unit_custom = False + self._onchange_request_parameters() - self.date_from = date_from - self.date_to = date_to - - if not (self.leave_type_request_unit == 'hour' and self.request_unit_all == 'period'): - date_from = date_from - date_to = date_to - - self.number_of_days_temp = self._get_number_of_days(date_from, date_to, self.employee_id.id) - - @api.onchange('request_unit_day') - def _onchange_request_unit_day(self): - self.request_unit_all = self.request_unit_day + @api.onchange('request_unit_custom') + def _onchange_request_unit_custom(self): + if self.request_unit_custom: + self.request_unit_half = False + self.request_unit_hours = False + self._onchange_request_parameters() @api.onchange('holiday_type') def _onchange_type(self): if self.holiday_type == 'employee' and not self.employee_id: - if self.env.user.employee_ids: - self.employee_id = self.env.user.employee_ids[0] - elif self.holiday_type == 'department': - if self.env.user.employee_ids: - self.department_id = self.department_id or self.env.user.employee_ids[0].department_id - self.employee_id = None + self.employee_id = self.env.user.employee_ids[0].id + self.mode_company_id = False + self.category_id = False + elif self.holiday_type == 'company' and not self.mode_company_id: + self.employee_id = False + self.mode_company_id = self.env.user.company_id.id + self.category_id = False + elif self.holiday_type == 'department' and not self.department_id: + self.employee_id = False + self.mode_company_id = False + self.department_id = self.env.user.employee_ids[0].department_id.id + self.category_id = False elif self.holiday_type == 'category': - self.employee_id = None - self.department_id = None - - @api.onchange('date_from') - def _onchange_date_from(self): - """ If there are no date set for date_to, automatically set one 8 hours later than - the date_from. Also update the number_of_days. - """ - date_from = fields.Datetime.from_string(self.date_from) - date_to = fields.Datetime.from_string(self.date_to) - - # No date_to set so far: automatically compute one 8 hours later - if date_from and not date_to: - date_to = date_from + timedelta(hours=self.employee_id.resource_calendar_id.hours_per_day) - self.date_to = date_to - - self.request_date_from = date_from.date() if date_from else False - self.request_date_to = date_to.date() if date_to else False - - if (date_from and date_to) and (date_from.day < date_to.day): - self.request_unit_all = 'period' - self.request_unit_day = 'period' - - # Compute and update the number of days - if (date_to and date_from) and (date_from <= date_to): - self.number_of_days_temp = self._get_number_of_days(date_from, date_to, self.employee_id.id) - else: - self.number_of_days_temp = 0 - - @api.onchange('date_to') - def _onchange_date_to(self): - """ Update the number_of_days. """ - date_from = fields.Datetime.from_string(self.date_from) - date_to = fields.Datetime.from_string(self.date_to) - - self.request_date_from = date_from.date() if date_from else False - self.request_date_to = date_to.date() if date_to else False - - if (date_from and date_to) and (date_from.day < date_to.day): - self.request_unit_all = 'period' - self.request_unit_day = 'period' - - # Compute and update the number of days - if (date_to and date_from) and (date_from <= date_to): - self.number_of_days_temp = self._get_number_of_days(date_from, date_to, self.employee_id.id) - else: - self.number_of_days_temp = 0 + self.employee_id = False + self.mode_company_id = False + self.department_id = False @api.onchange('employee_id') def _onchange_employee_id(self): - self.manager_id = self.employee_id and self.employee_id.parent_id - self.department_id = self.employee_id.department_id + self.manager_id = self.employee_id.parent_id.id + if self.employee_id: + self.department_id = self.employee_id.department_id + + @api.onchange('date_from', 'date_to') + def _onchange_leave_dates(self): + if self.date_from and self.date_to: + self.number_of_days = self._get_number_of_days(self.date_from, self.date_to, self.employee_id.id) + else: + self.number_of_days = 0 @api.multi - @api.depends('number_of_days_temp') - def _compute_number_of_days(self): + @api.depends('number_of_days') + def _compute_number_of_days_display(self): for holiday in self: - holiday.number_of_days = -holiday.number_of_days_temp + holiday.number_of_days_display = holiday.number_of_days @api.multi - @api.depends('number_of_days_temp') - def _compute_number_of_hours(self): + @api.depends('number_of_days') + def _compute_number_of_hours_display(self): for holiday in self: - holiday.number_of_hours = holiday.number_of_days_temp * self.employee_id.resource_calendar_id.hours_per_day + calendar = self.employee_id.resource_calendar_id or self.env.user.company_id.resource_calendar_id + holiday.number_of_hours_display = holiday.number_of_days * (calendar.hours_per_day or HOURS_PER_DAY) @api.multi @api.depends('state', 'employee_id', 'department_id') @@ -353,10 +396,10 @@ class HolidaysRequest(models.Model): if nholidays: raise ValidationError(_('You can not have 2 leaves that overlaps on the same day.')) - @api.constrains('state', 'number_of_days_temp', 'holiday_status_id') + @api.constrains('state', 'number_of_days', '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 or holiday.holiday_status_id.negative: + if holiday.holiday_type != 'employee' or not holiday.employee_id or holiday.holiday_status_id.allocation_type == 'no': 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 \ @@ -382,9 +425,21 @@ class HolidaysRequest(models.Model): res = [] for leave in self: if self.env.context.get('short_name'): - res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days_temp))) + res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days))) else: - res.append((leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name or leave.category_id.name, leave.holiday_status_id.name, leave.number_of_days_temp))) + if leave.holiday_type == 'company': + target = leave.mode_company_id.name + elif leave.holiday_type == 'department': + target = leave.department_id.name + elif leave.holiday_type == 'category': + target = leave.category_id.name + else: + target = leave.employee_id.name + res.append( + (leave.id, + _("%s on %s :%.2f day(s)") % + (target, leave.holiday_status_id.name, leave.number_of_days)) + ) return res @api.multi @@ -499,11 +554,12 @@ class HolidaysRequest(models.Model): @api.multi def _prepare_holidays_meeting_values(self): self.ensure_one() + calendar = self.employee_id.resource_calendar_id or self.env.user.company_id.resource_calendar_id meeting_values = { 'name': self.display_name, 'categ_ids': [(6, 0, [ self.holiday_status_id.categ_id.id])] if self.holiday_status_id.categ_id else [], - 'duration': self.number_of_days_temp * self.employee_id.resource_calendar_id.hours_per_day, + 'duration': self.number_of_days * (calendar.hours_per_day or HOURS_PER_DAY), 'description': self.notes, 'user_id': self.user_id.id, 'start': self.date_from, @@ -528,7 +584,7 @@ class HolidaysRequest(models.Model): 'date_from': self.date_from, 'date_to': self.date_to, 'notes': self.notes, - 'number_of_days_temp': self.number_of_days_temp, + 'number_of_days': self.number_of_days, 'parent_id': self.id, 'employee_id': employee.id } @@ -586,12 +642,20 @@ class HolidaysRequest(models.Model): holiday.write({'first_approver_id': current_employee.id}) if holiday.holiday_type == 'employee': holiday._validate_leave_request() - elif holiday.holiday_type in ['category', 'department']: + elif holiday.holiday_type in ['company', 'category', 'department']: leaves = self.env['hr.leave'] - employees = holiday.category_id.employee_ids if holiday.holiday_type == 'category' else holiday.department_id.member_ids + if holiday.holiday_type == 'category': + employees = holiday.category_id.employee_ids + elif holiday.holiday_type == 'company': + employees = self.env['hr.employee'].search([('company_id', '=', self.mode_company_id.id)]) + else: + holiday.department_id.member_ids for employee in employees: values = holiday._prepare_holiday_values(employee) - leaves += self.with_context(mail_notify_force_send=False).create(values) + leaves += self.with_context( + mail_notify_force_send=False, + mail_activity_automation_skip=True + ).create(values) # TODO is it necessary to interleave the calls? leaves.action_approve() if leaves and leaves[0].validation_type == 'both': diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index e7ff67c498c..54c49a27ae1 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -5,17 +5,14 @@ import logging -import pytz - from datetime import datetime, time from dateutil.relativedelta import relativedelta from odoo import api, fields, models +from odoo.addons.resource.models.resource import HOURS_PER_DAY from odoo.exceptions import AccessError, UserError from odoo.tools.translate import _ -from odoo.addons.resource.models.resource import HOURS_PER_DAY - _logger = logging.getLogger(__name__) @@ -26,18 +23,15 @@ class HolidaysAllocation(models.Model): _inherit = ['mail.thread', 'mail.activity.mixin'] _mail_post_access = 'read' - def _default_domain_holiday_status_id(self): - if self.user_has_groups('hr_holidays.group_hr_holidays_manager'): - return [('valid', '=', True), ('limit', '=', False)] - return [('valid', '=', True), ('employee_applicability', 'in', ['allocation', 'both']), ('limit', '=', False)] - def _default_employee(self): return self.env.context.get('default_employee_id') or self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) def _default_holiday_status_id(self): - LeaveType = self.env['hr.leave.type'].with_context(employee_id=self._default_employee().id) - lt = LeaveType.search(self._default_domain_holiday_status_id()) - return lt[:1] + if self.user_has_groups('hr_holidays.group_hr_holidays_user'): + domain = [('valid', '=', True)] + else: + domain = [('valid', '=', True), ('allocation_type', 'in', ('no', 'fixed_allocation'))] + return self.env['hr.leave.type'].search(domain, limit=1) name = fields.Char('Description') state = fields.Selection([ @@ -48,47 +42,74 @@ class HolidaysAllocation(models.Model): ('validate1', 'Second Approval'), ('validate', 'Approved') ], string='Status', readonly=True, track_visibility='onchange', copy=False, default='confirm', - help="The status is set to 'To Submit', when a leave request is created." + - "\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, + help="The status is set to 'To Submit', when a leave request is created." + + "\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, + 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, + holiday_status_id = fields.Many2one( + "hr.leave.type", string="Leave Type", required=True, readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, - domain=lambda self: self._default_domain_holiday_status_id(), default=_default_holiday_status_id) - employee_id = fields.Many2one('hr.employee', string='Employee', index=True, readonly=True, + domain=[('valid', '=', True)], default=_default_holiday_status_id) + employee_id = fields.Many2one( + 'hr.employee', string='Employee', index=True, readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, default=_default_employee, track_visibility='onchange') notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) - number_of_days_temp = fields.Float( - 'Duration (days)', copy=False, readonly=True, + # duration + number_of_days = fields.Float( + 'Number of Days', track_visibility='onchange', + help='Duration in days. Reference field to use when necessary.') + number_of_days_display = fields.Float( + 'Duration (days)', compute='_compute_number_of_days_display', inverse='_inverse_number_of_days_display', states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, - help='Number of days of the leave request according to your working schedule.') - number_of_days = fields.Float('Number of Days', compute='_compute_number_of_days', store=True, track_visibility='onchange') - number_of_hours = fields.Float('Duration (hours)', help="Number of hours of the leave allocation according to your working schedule.") + help="UX field allowing to see and modify the allocation duration, computed in days.") + number_of_hours_display = fields.Float( + 'Duration (hours)', compute='_compute_number_of_hours_display', inverse='_inverse_number_of_hours_display', + help="UX field allowing to see and modify the allocation duration, computed in hours.") + # details parent_id = fields.Many2one('hr.leave.allocation', string='Parent') linked_request_ids = fields.One2many('hr.leave.allocation', 'parent_id', string='Linked Requests') - department_id = fields.Many2one('hr.department', string='Department', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) - category_id = fields.Many2one('hr.employee.category', string='Employee Tag', readonly=True, - states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help='Category of Employee') - holiday_type = fields.Selection([ - ('employee', 'By Employee'), - ('department', 'By Department'), - ('category', 'By Employee Tag') - ], string='Allocation Mode', readonly=True, required=True, default='employee', - states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, - help='By Employee: Allocation for individual Employee, By Employee Tag: Allocation for group of employees in category') - first_approver_id = fields.Many2one('hr.employee', string='First Approval', readonly=True, copy=False, + first_approver_id = fields.Many2one( + 'hr.employee', string='First Approval', readonly=True, copy=False, help='This area is automatically filled by the user who validate the leave', oldname='manager_id') - second_approver_id = fields.Many2one('hr.employee', string='Second Approval', readonly=True, copy=False, oldname='manager_id2', + second_approver_id = fields.Many2one( + 'hr.employee', string='Second Approval', readonly=True, copy=False, oldname='manager_id2', help='This area is automaticly filled by the user who validate the leave with second level (If Leave type need second validation)') validation_type = fields.Selection('Validation Type', related='holiday_status_id.validation_type', readonly=True) 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', readonly=True) - accrual = fields.Boolean("Accrual", related='holiday_status_id.accrual', store=True, readonly=True) + # mode + holiday_type = fields.Selection([ + ('employee', 'By Employee'), + ('company', 'By Company'), + ('department', 'By Department'), + ('category', 'By Employee Tag')], + string='Allocation Mode', readonly=True, required=True, default='employee', + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, + help="Allow to create requests in batchs:\n- By Employee: for a specific employee" + "\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") + mode_company_id = fields.Many2one( + 'res.company', string='Company', readonly=True, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) + department_id = fields.Many2one( + 'hr.department', string='Department', readonly=True, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) + category_id = fields.Many2one( + 'hr.employee.category', string='Employee Tag', readonly=True, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) + # accrual configuration + accrual = fields.Boolean( + "Accrual", 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", readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, default=1) interval_number = fields.Integer("Number of unit between two intervals", readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, default=1) unit_per_interval = fields.Selection([ @@ -103,9 +124,13 @@ class HolidaysAllocation(models.Model): 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) )", - "The employee, department or employee category of this request is missing. Please make sure that your user login is linked to an employee."), - ('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days must be greater than 0."), + ('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) 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"), ] @@ -161,29 +186,35 @@ class HolidaysAllocation(models.Model): # 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 + 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 + 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.multi - @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': - # 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) + @api.depends('number_of_days') + def _compute_number_of_days_display(self): + for allocation in self: + allocation.number_of_days_display = allocation.number_of_days - holiday.number_of_days = number_of_days + @api.multi + def _inverse_number_of_days_display(self): + for allocation in self: + allocation.number_of_days = allocation.number_of_days_display + + @api.multi + @api.depends('number_of_days', 'employee_id') + def _compute_number_of_hours_display(self): + for allocation in self: + allocation.number_of_hours_display = allocation.number_of_days * (allocation.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY) + + @api.multi + def _inverse_number_of_hours_display(self): + for allocation in self: + allocation.number_of_days = allocation.number_of_hours_display / (allocation.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY) @api.multi @api.depends('state', 'employee_id', 'department_id') @@ -227,20 +258,15 @@ class HolidaysAllocation(models.Model): if self.holiday_type == 'employee': self.department_id = self.employee_id.department_id - @api.onchange('number_of_days_temp') - def _onchange_number_of_days_temp(self): - self.number_of_hours = self.number_of_days_temp * self.employee_id.resource_calendar_id.hours_per_day - - @api.onchange('number_of_hours') - 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 = datetime.combine(self.holiday_status_id.validity_stop, time.max) + if self.holiday_status_id.validity_stop and self.date_to: + new_date_to = datetime.combine(self.holiday_status_id.validity_stop, time.max) + if new_date_to < self.date_to: + self.date_to = new_date_to if self.accrual: - self.number_of_days_temp = 0 + self.number_of_days = 0 if self.holiday_status_id.request_unit == 'hour': self.unit_per_interval = 'hours' @@ -259,11 +285,24 @@ class HolidaysAllocation(models.Model): @api.multi def name_get(self): res = [] - for leave in self: - if leave.type_request_unit == 'hour': - res.append((leave.id, _("Allocation of %s : %.2f hour(s) To %s") % (leave.holiday_status_id.name, leave.number_of_hours, leave.employee_id.name))) + for allocation in self: + if allocation.holiday_type == 'company': + target = allocation.mode_company_id.name + elif allocation.holiday_type == 'department': + target = allocation.department_id.name + elif allocation.holiday_type == 'category': + target = allocation.category_id.name else: - res.append((leave.id, _("Allocation of %s : %.2f day(s) To %s") % (leave.holiday_status_id.name, leave.number_of_days_temp, leave.employee_id.name))) + target = allocation.employee_id.name + + res.append( + (allocation.id, + _("Allocation of %s : %.2f %s to %s") % + (allocation.holiday_status_id.name, + allocation.number_of_hours_display if allocation.type_request_unit == 'hour' else allocation.number_of_days, + 'hours' if allocation.type_request_unit == 'hour' else 'days', + target)) + ) return res @api.multi @@ -329,7 +368,7 @@ class HolidaysAllocation(models.Model): 'holiday_type': 'employee', 'holiday_status_id': self.holiday_status_id.id, 'notes': self.notes, - 'number_of_days_temp': self.number_of_days_temp, + 'number_of_days': self.number_of_days, 'parent_id': self.id, 'employee_id': employee.id } @@ -386,19 +425,32 @@ class HolidaysAllocation(models.Model): holiday.write({'second_approver_id': current_employee.id}) else: holiday.write({'first_approver_id': current_employee.id}) - if holiday.holiday_type in ['category', 'department']: - leaves = self.env['hr.leave.allocation'] - employees = holiday.category_id.employee_ids if holiday.holiday_type == 'category' else holiday.department_id.member_ids - for employee in employees: - values = holiday._prepare_holiday_values(employee) - leaves += self.with_context(mail_notify_force_send=False).create(values) - # TODO is it necessary to interleave the calls? - leaves.action_approve() - if leaves and leaves[0].validation_type == 'both': - leaves.action_validate() + + holiday._action_validate_create_childs() self.activity_update() return True + 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': + 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.holiday_status_id.validation_type == 'both': + childs.action_validate() + return childs + @api.multi def action_refuse(self): current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index 216e1002de7..828df4a5586 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -3,6 +3,7 @@ # Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com) +import datetime import logging from odoo import api, fields, models @@ -21,7 +22,8 @@ class HolidaysType(models.Model): name = fields.Char('Leave Type', required=True, translate=True) sequence = fields.Integer(default=100, help='The type with the smallest sequence is the default value in leave request') - categ_id = fields.Many2one('calendar.event.type', string='Meeting Type', + categ_id = fields.Many2one( + 'calendar.event.type', string='Meeting Type', help='Once a leave is validated, Odoo will create a corresponding meeting of this type in the calendar.') color_name = fields.Selection([ ('red', 'Red'), @@ -41,63 +43,47 @@ class HolidaysType(models.Model): ('wheat', 'Wheat'), ('ivory', 'Ivory')], string='Color in Report', required=True, default='red', help='This color will be used in the leaves summary located in Reporting > Leaves by Department.') - limit = fields.Boolean('Exceed Allocation', - help="If you select this check box, the system will allow the employees to ask" - "for leaves without allocating some beforehand") active = fields.Boolean('Active', default=True, - help="If the active field is set to false, it will allow you to hide the leave type without removing it.") - + help="If the active field is set to false, it will allow you to hide the leave type without removing it.") max_leaves = fields.Float(compute='_compute_leaves', string='Maximum Allowed', - help='This value is given by the sum of all leaves requests with a positive value.') - leaves_taken = fields.Float(compute='_compute_leaves', string='Leaves Already Taken', + help='This value is given by the sum of all leaves requests with a positive value.') + leaves_taken = fields.Float( + compute='_compute_leaves', string='Leaves Already Taken', help='This value is given by the sum of all leaves requests with a negative value.') - remaining_leaves = fields.Float(compute='_compute_leaves', string='Remaining Leaves', + remaining_leaves = fields.Float( + compute='_compute_leaves', string='Remaining Leaves', help='Maximum Leaves Allowed - Leaves Already Taken') - virtual_remaining_leaves = fields.Float(compute='_compute_leaves', string='Virtual Remaining Leaves', + virtual_remaining_leaves = fields.Float( + compute='_compute_leaves', string='Virtual Remaining Leaves', help='Maximum Leaves Allowed - Leaves Already Taken - Leaves Waiting Approval') - + group_days_allocation = fields.Float( + compute='_compute_group_days_allocation', string='Days Allocated') + group_days_leave = fields.Float( + compute='_compute_group_days_leave', string='Group Leaves') company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id) - - validation_type = fields.Selection([('hr', 'Human Resource officer'), - ('manager', 'Employee Manager'), - ('both', 'Double Validation')], - default='hr', - string='Validation By') - - employee_applicability = fields.Selection([('both', 'Both'), - ('leave', 'Can be requested'), - ('allocation', 'Can be allocated')], - default=lambda self: 'leave' if self.limit else 'both', string='Mode', - help='This leave type will be available on Leave / Allocation request based on selected value') - + validation_type = fields.Selection([ + ('hr', 'Human Resource officer'), + ('manager', 'Employee Manager'), + ('both', 'Double Validation')], default='hr', string='Validation By') + allocation_type = fields.Selection([ + ('fixed', 'Fixed by HR'), + ('fixed_allocation', 'Fixed by HR + allocation request'), + ('no', 'No allocation')], + default='fixed', string='Mode', + help='\tFixed by HR: allocated by HR and cannot be bypassed; users can request leaves;' + '\tFixed by HR + allocation request: allocated by HR and users can request leaves and allocations;' + '\tNo allocation: no allocation by default, users can freely request leaves;') validity_start = fields.Date("Start Date", default=fields.Date.today, - help='Adding validity to types of leaves so that it cannot be selected outside' - 'this time period') + help='Adding validity to types of leaves so that it cannot be selected outside this time period') validity_stop = fields.Date("End Date") - 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('Accrual', default=False, - help='This option forces this type of leave to be allocated accrually') - + request_unit = fields.Selection([ + ('day', 'Day'), ('hour', 'Hours')], + default='day', string='Take Leaves in', required=True) 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): @@ -107,29 +93,7 @@ class HolidaysType(models.Model): 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') + @api.depends('validity_start', 'validity_stop') def _compute_valid(self): dt = self._context.get('default_date_from') or fields.Datetime.now() @@ -169,10 +133,10 @@ class HolidaysType(models.Model): for request in requests: status_dict = result[request.holiday_status_id.id] - status_dict['virtual_remaining_leaves'] -= request.number_of_days_temp + status_dict['virtual_remaining_leaves'] -= request.number_of_days if request.state == 'validate': - status_dict['leaves_taken'] += request.number_of_days_temp - status_dict['remaining_leaves'] -= request.number_of_days_temp + status_dict['leaves_taken'] += request.number_of_days + status_dict['remaining_leaves'] -= request.number_of_days for allocation in allocations: status_dict = result[allocation.holiday_status_id.id] @@ -180,19 +144,25 @@ class HolidaysType(models.Model): # note: add only validated allocation even for the virtual # count; otherwise pending then refused allocation allow # the employee to create more leaves than possible - status_dict['virtual_remaining_leaves'] += allocation.number_of_days_temp - status_dict['max_leaves'] += allocation.number_of_days_temp - status_dict['remaining_leaves'] += allocation.number_of_days_temp + status_dict['virtual_remaining_leaves'] += allocation.number_of_days + status_dict['max_leaves'] += allocation.number_of_days + status_dict['remaining_leaves'] += allocation.number_of_days return result + def _get_contextual_employee_id(self): + if 'employee_id' in self._context: + employee_id = self._context['employee_id'] + elif 'default_employee_id' in self._context: + employee_id = self._context['default_employee_id'] + else: + employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.user.id)], limit=1).id + return employee_id + @api.multi def _compute_leaves(self): data_days = {} - if 'employee_id' in self._context: - employee_id = self._context['employee_id'] - else: - employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.user.id)], limit=1).id + employee_id = self._get_contextual_employee_id() if employee_id: data_days = self.get_days(employee_id) @@ -204,6 +174,30 @@ class HolidaysType(models.Model): holiday_status.remaining_leaves = result.get('remaining_leaves', 0) holiday_status.virtual_remaining_leaves = result.get('virtual_remaining_leaves', 0) + @api.multi + def _compute_group_days_allocation(self): + grouped_res = self.env['hr.leave.allocation'].read_group( + [('holiday_status_id', 'in', self.ids), ('holiday_type', '!=', 'employee'), ('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', 'number_of_days'], + ['holiday_status_id'], + ) + grouped_dict = dict((data['holiday_status_id'][0], data['number_of_days']) for data in grouped_res) + for allocation in self: + allocation.group_days_allocation = grouped_dict.get(allocation.id, 0) + + @api.multi + 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'), + ('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'], + ) + grouped_dict = dict((data['holiday_status_id'][0], data['holiday_status_id_count']) for data in grouped_res) + for allocation in self: + allocation.group_days_leave = grouped_dict.get(allocation.id, 0) + @api.multi def name_get(self): if not self._context.get('employee_id'): @@ -212,7 +206,7 @@ class HolidaysType(models.Model): res = [] for record in self: name = record.name - if not record.limit: + if record.allocation_type != 'no': name = "%(name)s (%(count)s)" % { 'name': name, '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) @@ -225,7 +219,7 @@ class HolidaysType(models.Model): """ Override _search to order the results, according to some employee. The order is the following - - limit (limited leaves first, such as Legal Leaves) + - allocation fixed first, then allowing allocation, then free allocation - virtual remaining leaves (higher the better, so using reverse on sorted) This override is necessary because those fields are not stored and depends @@ -233,9 +227,38 @@ class HolidaysType(models.Model): is an employee_id in context and that no other order has been given to the method. """ + employee_id = self._get_contextual_employee_id() leave_ids = super(HolidaysType, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) - if not count and not order and self._context.get('employee_id'): + if not count and not order and employee_id: leaves = self.browse(leave_ids) - sort_key = lambda l: (not l.limit, l.virtual_remaining_leaves) + sort_key = lambda l: (l.allocation_type == 'fixed', l.allocation_type == 'fixed_allocation', l.virtual_remaining_leaves) return leaves.sorted(key=sort_key, reverse=True).ids return leave_ids + + @api.multi + def action_see_days_allocated(self): + self.ensure_one() + action = self.env.ref('hr_holidays.hr_leave_allocation_action_all').read()[0] + action['domain'] = [ + ('holiday_type', '!=', 'employee'), + ('holiday_status_id', '=', self.ids[0]), + ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))) + ] + action['context'] = { + 'default_holiday_type': 'department', + 'default_holiday_status_id': self.ids[0], + } + return action + + @api.multi + def action_see_group_leaves(self): + self.ensure_one() + action = self.env.ref('hr_holidays.hr_leave_action_all').read()[0] + action['domain'] = [ + ('holiday_status_id', '=', self.ids[0]), + ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))) + ] + action['context'] = { + 'default_holiday_status_id': self.ids[0], + } + return action diff --git a/addons/hr_holidays/report/holidays_summary_report.py b/addons/hr_holidays/report/holidays_summary_report.py index 1f75ea51fae..3337acc7af9 100644 --- a/addons/hr_holidays/report/holidays_summary_report.py +++ b/addons/hr_holidays/report/holidays_summary_report.py @@ -74,7 +74,7 @@ class HrHolidaySummaryReport(models.AbstractModel): if date_from >= start_date and date_from <= end_date: res[(date_from-start_date).days]['color'] = holiday.holiday_status_id.color_name date_from += timedelta(1) - count += abs(holiday.number_of_days) + count += holiday.number_of_days self.sum = count return res diff --git a/addons/hr_holidays/report/hr_leave_report.py b/addons/hr_holidays/report/hr_leave_report.py index 50f4c1a69ca..cbef7ec4b20 100644 --- a/addons/hr_holidays/report/hr_leave_report.py +++ b/addons/hr_holidays/report/hr_leave_report.py @@ -65,7 +65,7 @@ class LeaveReport(models.Model): union all select request.employee_id as employee_id, request.name as name, - request.number_of_days as number_of_days, + (request.number_of_days * -1) as number_of_days, request.category_id as category_id, request.department_id as department_id, request.holiday_status_id as holiday_status_id, diff --git a/addons/hr_holidays/report/hr_leave_reports.xml b/addons/hr_holidays/report/hr_leave_reports.xml index 51f339ca65b..d4471fa8a72 100644 --- a/addons/hr_holidays/report/hr_leave_reports.xml +++ b/addons/hr_holidays/report/hr_leave_reports.xml @@ -121,13 +121,6 @@ 'search_default_group_type': 1, 'search_default_year': 1 , 'search_default_my_leaves': 1, - 'needaction_menu_ref': - [ - 'hr_holidays.menu_open_ask_holidays_new', - 'hr_holidays.menu_open_allocation_holidays', - 'hr_holidays.menu_request_approve_allocation', - 'hr_holidays.menu_open_employee_leave', - ] }

@@ -145,15 +138,18 @@ hr.employee form tree,form - {'search_default_employee_id': [active_id], 'default_employee_id': active_id, 'search_default_group_type': 1, + { + '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')] + [('holiday_type','=','employee')] diff --git a/addons/hr_holidays/tests/common.py b/addons/hr_holidays/tests/common.py index 467eff2037c..43c5df67181 100644 --- a/addons/hr_holidays/tests/common.py +++ b/addons/hr_holidays/tests/common.py @@ -9,38 +9,41 @@ class TestHrHolidaysBase(common.TransactionCase): def setUp(self): super(TestHrHolidaysBase, self).setUp() - Users = self.env['res.users'].with_context(no_reset_password=True) - - # Find Employee group - group_employee_id = self.ref('base.group_user') + self._quick_create_ctx = { + 'mail_create_nolog': True, + 'mail_create_nosubscribe': True, + 'mail_notrack': True, + } + self._quick_create_user_ctx = dict(self._quick_create_ctx, no_reset_password=True) # Test users to use through the various tests + Users = self.env['res.users'].with_context(self._quick_create_user_ctx) self.user_hruser = Users.create({ 'name': 'Armande HrUser', 'login': 'Armande', 'email': 'armande.hruser@example.com', - 'groups_id': [(6, 0, [group_employee_id, self.ref('hr_holidays.group_hr_holidays_user')])] + 'groups_id': [(6, 0, [self.ref('base.group_user'), self.ref('hr_holidays.group_hr_holidays_user')])] }) self.user_hruser_id = self.user_hruser.id self.user_hrmanager = Users.create({ 'name': 'Bastien HrManager', 'login': 'bastien', 'email': 'bastien.hrmanager@example.com', - 'groups_id': [(6, 0, [group_employee_id, self.ref('hr_holidays.group_hr_holidays_manager')])] + 'groups_id': [(6, 0, [self.ref('base.group_user'), self.ref('hr_holidays.group_hr_holidays_manager')])] }).id self.user_hrmanager_id = self.user_hrmanager self.user_employee = Users.create({ 'name': 'David Employee', 'login': 'david', 'email': 'david.employee@example.com', - 'groups_id': [(6, 0, [group_employee_id])] + 'groups_id': [(6, 0, [self.ref('base.group_user')])] }) self.user_employee_id = self.user_employee.id self.user_hrmanager_2_id = Users.create({ 'name': 'Florence HrManager', 'login': 'florence', 'email': 'florence.hrmanager@example.com', - 'groups_id': [(6, 0, [group_employee_id, self.ref('hr_holidays.group_hr_holidays_manager')])] + 'groups_id': [(6, 0, [self.ref('base.group_user'), self.ref('hr_holidays.group_hr_holidays_manager')])] }).id # Hr Data diff --git a/addons/hr_holidays/tests/test_access_rights.py b/addons/hr_holidays/tests/test_access_rights.py index 0631fa10006..cf4e8f76ad8 100644 --- a/addons/hr_holidays/tests/test_access_rights.py +++ b/addons/hr_holidays/tests/test_access_rights.py @@ -17,8 +17,8 @@ class TestLeavesRights(TestHrHolidaysBase): super(TestLeavesRights, self).setUp() self.leave_type = self.env['hr.leave.type'].create({ 'name': 'Unlimited', - 'limit': True, 'validation_type': 'hr', + 'allocation_type': 'no', }) self.rd_dept.manager_id = False self.hr_dept.manager_id = False @@ -30,7 +30,7 @@ class TestLeavesRights(TestHrHolidaysBase): 'employee_id': self.employee_emp.id, 'date_from': datetime.now(), 'date_to': datetime.now() + relativedelta(days=1), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') @@ -60,9 +60,9 @@ class TestLeavesRights(TestHrHolidaysBase): 'employee_id': self.employee_hruser.id, 'date_from': datetime.now(), 'date_to': datetime.now() + relativedelta(days=1), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) - res = other_leave.sudo(self.user_employee_id).read(['number_of_days_temp', 'state', 'name']) + res = other_leave.sudo(self.user_employee_id).read(['number_of_days', 'state', 'name']) self.assertEqual( res[0]['name'], '*****', 'Private information should have been stripped, received %s instead' % res[0]['name'] @@ -78,7 +78,7 @@ class TestLeavesRights(TestHrHolidaysBase): 'employee_id': self.employee_hruser.id, 'date_from': datetime.now(), 'date_to': datetime.now() + relativedelta(days=1), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) self.assertEqual( other_leave.sudo(self.user_employee_id).name, '*****', @@ -88,7 +88,7 @@ class TestLeavesRights(TestHrHolidaysBase): @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') def test_leave_read_by_user_own(self): """ Users should be able to read name field of own requests """ - res = self.employee_leave.read(['name', 'number_of_days_temp', 'state']) + res = self.employee_leave.read(['name', 'number_of_days', 'state']) self.assertEqual(res[0]['name'], 'Test') @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') @@ -112,7 +112,7 @@ class TestLeavesRights(TestHrHolidaysBase): 'employee_id': self.employee_hruser.id, 'date_from': datetime.now(), 'date_to': datetime.now() + relativedelta(days=1), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) with self.assertRaises(AccessError): other_leave.sudo(self.user_employee_id).write({'name': 'Crocodile Dundee is my man'}) @@ -131,7 +131,7 @@ class TestLeavesRights(TestHrHolidaysBase): 'employee_id': self.employee_hrmanager.id, 'date_from': datetime.now(), 'date_to': datetime.now() + relativedelta(days=1), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) manager_leave.sudo(self.user_hrmanager).action_draft() @@ -150,7 +150,7 @@ class TestLeavesRights(TestHrHolidaysBase): 'employee_id': self.employee_hruser.id, 'date_from': datetime.now(), 'date_to': datetime.now() + relativedelta(days=1), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) officer_leave.sudo(self.user_hruser).action_draft() @@ -175,7 +175,7 @@ class TestLeavesRights(TestHrHolidaysBase): 'employee_id': self.employee_hruser.id, 'date_from': datetime.now(), 'date_to': datetime.now() + relativedelta(days=1), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) with self.assertRaises(UserError): other_leave.sudo(self.user_employee_id).action_draft() @@ -316,9 +316,9 @@ class TestMultiCompany(TestHrHolidaysBase): }) self.leave_type = self.env['hr.leave.type'].create({ 'name': 'Unlimited - Company New', - 'limit': True, 'company_id': self.new_company.id, 'validation_type': 'hr', + 'allocation_type': 'no', }) self.rd_dept.manager_id = False self.hr_dept.manager_id = False @@ -330,7 +330,7 @@ class TestMultiCompany(TestHrHolidaysBase): 'employee_id': self.employee_emp.id, 'date_from': datetime.now(), 'date_to': datetime.now() + relativedelta(days=1), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') diff --git a/addons/hr_holidays/tests/test_accrual_allocations.py b/addons/hr_holidays/tests/test_accrual_allocations.py index 8250f88f0a7..c877643cc2e 100644 --- a/addons/hr_holidays/tests/test_accrual_allocations.py +++ b/addons/hr_holidays/tests/test_accrual_allocations.py @@ -19,13 +19,12 @@ class TestAccrualAllocations(TestHrHolidaysBase): self.accrual_type = LeaveType.create({ 'name': 'accrual', - 'limit': False, - 'accrual': True, + 'allocation_type': 'fixed', }) self.unpaid_type = LeaveType.create({ 'name': 'unpaid', - 'limit': True, + 'allocation_type': 'no', 'unpaid': True, }) @@ -45,67 +44,57 @@ class TestAccrualAllocations(TestHrHolidaysBase): """ % (newdate, id)) def test_accrual_base_no_leaves(self): - """ - Test if we can allocate some leaves accrually to an employee - """ - Allocation = self.env['hr.leave.allocation'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True) - - alloc_0 = Allocation.create({ + """ Test if we can allocate some leaves accrually to an employee """ + alloc = self.env['hr.leave.allocation'].sudo(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, - 'number_of_days_temp': 0, + 'accrual': True, + 'number_of_days': 0, 'number_per_interval': 1, 'interval_number': 1, 'unit_per_interval': 'days', 'interval_unit': 'weeks', }) + alloc.action_approve() + alloc._update_accrual() - alloc_0.action_approve() - - Allocation._update_accrual() - - self.assertEqual(alloc_0.number_of_days, 1, 'Employee should have been allocated one leave day') + 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 - """ - Allocation = self.env['hr.leave.allocation'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True) - Leave = self.env['hr.leave'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True) - - alloc_0 = Allocation.create({ + """ Test if the accrual allocation take the unpaid leaves into account when allocating leaves """ + alloc = self.env['hr.leave.allocation'].sudo(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, - 'number_of_days_temp': 0, + 'accrual': True, + 'number_of_days': 0, 'number_per_interval': 1, 'interval_number': 1, 'unit_per_interval': 'days', 'interval_unit': 'weeks', }) - alloc_0.action_approve() + 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_0 = Leave.create({ + leave = self.env['hr.leave'].sudo(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_temp': 1, + 'number_of_days': 1, }) - leave_0.action_approve() + leave.action_approve() - Allocation._update_accrual() + alloc._update_accrual() - self.assertEqual(alloc_0.number_of_days, .8, 'As employee took some unpaid leaves last week, he should be allocated only .8 days') + 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): """ @@ -117,7 +106,8 @@ class TestAccrualAllocations(TestHrHolidaysBase): 'name': '1 day per 2 weeks', 'employee_id': self.employee_emp_id, 'holiday_status_id': self.accrual_type.id, - 'number_of_days_temp': 0, + 'accrual': True, + 'number_of_days': 0, 'number_per_interval': 1, 'interval_number': 2, 'unit_per_interval': 'days', @@ -128,7 +118,8 @@ class TestAccrualAllocations(TestHrHolidaysBase): 'name': '4 hours per week', 'employee_id': self.employee_emp_id, 'holiday_status_id': self.accrual_type.id, - 'number_of_days_temp': 0, + 'accrual': True, + 'number_of_days': 0, 'number_per_interval': 4, 'interval_number': 1, 'unit_per_interval': 'hours', @@ -139,7 +130,8 @@ class TestAccrualAllocations(TestHrHolidaysBase): 'name': '2 day per 1 month', 'employee_id': self.employee_emp_id, 'holiday_status_id': self.accrual_type.id, - 'number_of_days_temp': 0, + 'accrual': True, + 'number_of_days': 0, 'number_per_interval': 2, 'interval_number': 1, 'unit_per_interval': 'days', @@ -150,7 +142,8 @@ class TestAccrualAllocations(TestHrHolidaysBase): 'name': '20 days per year', 'employee_id': self.employee_emp_id, 'holiday_status_id': self.accrual_type.id, - 'number_of_days_temp': 0, + 'accrual': True, + 'number_of_days': 0, 'number_per_interval': 20, 'interval_number': 1, 'unit_per_interval': 'days', @@ -179,7 +172,8 @@ class TestAccrualAllocations(TestHrHolidaysBase): 'name': 'one shot one kill', 'employee_id': self.employee_emp_id, 'holiday_status_id': self.accrual_type.id, - 'number_of_days_temp': 0, + 'accrual': True, + 'number_of_days': 0, 'number_per_interval': 1, 'interval_number': 1, 'unit_per_interval': 'days', @@ -193,29 +187,23 @@ class TestAccrualAllocations(TestHrHolidaysBase): 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 - """ - Allocation = self.env['hr.leave.allocation'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True) - - alloc_0 = Allocation.create({ + """ Test if the cron does not allocate leaves every time it's called but only when necessary """ + alloc = self.env['hr.leave.allocation'].sudo(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, - 'number_of_days_temp': 0, + 'accrual': True, + '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() - alloc_0.action_approve() - - Allocation._update_accrual() - Allocation._update_accrual() - - self.assertEqual(alloc_0.number_of_days, 1, 'Cron only allocates 1 days every two weeks') + self.assertEqual(alloc.number_of_days, 1, 'Cron only allocates 1 days every two weeks') def test_accrual_validation(self): """ @@ -227,7 +215,7 @@ class TestAccrualAllocations(TestHrHolidaysBase): 'name': '20 days per year', 'employee_id': self.employee_emp_id, 'holiday_status_id': self.accrual_type.id, - 'number_of_days_temp': 0, + 'number_of_days': 0, 'number_per_interval': 20, 'interval_number': 1, 'unit_per_interval': 'days', @@ -242,32 +230,20 @@ class TestAccrualAllocations(TestHrHolidaysBase): 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'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True) - LeaveType = self.env['hr.leave.type'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True) - - accrual_max_five = LeaveType.create({ - 'name': 'max 5 days', - 'limit': False, - 'accrual': True, - 'balance_limit': 5, - }) - - allocation = Allocation.create({ + """ Test if accrual allocation does not allocate more than the balance limit""" + allocation = self.env['hr.leave.allocation'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True).create({ 'name': 'accrual 5 max', 'employee_id': self.employee_emp_id, - 'holiday_status_id': accrual_max_five.id, - 'number_of_days_temp': 0, + 'holiday_status_id': self.accrual_type.id, + 'accrual': True, + 'accrual_limit': 5, + 'number_of_days': 0, 'number_per_interval': 6, 'interval_number': 1, 'unit_per_interval': 'days', 'interval_unit': 'weeks', }) - allocation.action_approve() - - Allocation._update_accrual() + allocation._update_accrual() self.assertEqual(allocation.number_of_days, 5, 'Should have allocated only 5 days as balance limit is 5') diff --git a/addons/hr_holidays/tests/test_holidays_flow.py b/addons/hr_holidays/tests/test_holidays_flow.py index 0c3cff00ad9..51bcafe049c 100644 --- a/addons/hr_holidays/tests/test_holidays_flow.py +++ b/addons/hr_holidays/tests/test_holidays_flow.py @@ -33,34 +33,34 @@ class TestHolidaysFlow(TestHrHolidaysBase): with self.assertRaises(AccessError): HolidaysStatus.sudo(self.user_hruser_id).create({ 'name': 'UserCheats', - 'limit': True, + 'allocation_type': 'no', }) # HrManager creates some holiday statuses HolidayStatusManagerGroup = HolidaysStatus.sudo(self.user_hrmanager_id) HolidayStatusManagerGroup.create({ 'name': 'WithMeetingType', - 'limit': True, + 'allocation_type': 'no', 'categ_id': self.env['calendar.event.type'].sudo(self.user_hrmanager_id).create({'name': 'NotLimitedMeetingType'}).id }) self.holidays_status_1 = HolidayStatusManagerGroup.create({ 'name': 'NotLimitedHR', - 'limit': True, + 'allocation_type': 'no', 'validation_type': 'hr', }) self.holidays_status_2 = HolidayStatusManagerGroup.create({ 'name': 'Limited', - 'limit': False, + 'allocation_type': 'fixed', 'validation_type': 'both', }) self.holidays_status_3 = HolidayStatusManagerGroup.create({ 'name': 'NotLimitedManager', - 'limit': True, + 'allocation_type': 'no', 'validation_type': 'manager', }) self.holiday_status_4 = HolidayStatusManagerGroup.create({ 'name': 'TimeNotLimited', - 'limit': True, + 'allocation_type': 'no', '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'), @@ -81,7 +81,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'holiday_status_id': self.holidays_status_1.id, 'date_from': (datetime.today() - relativedelta(days=1)), 'date_to': datetime.today(), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) hol1_user_group = hol1_employee_group.sudo(self.user_hruser_id) hol1_manager_group = hol1_employee_group.sudo(self.user_hrmanager_id) @@ -101,7 +101,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'holiday_status_id': self.holidays_status_3.id, 'date_from': (datetime.today() + relativedelta(days=12)), 'date_to': (datetime.today() + relativedelta(days=13)), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) hol12_user_group = hol12_employee_group.sudo(self.user_hruser_id) hol12_manager_group = hol12_employee_group.sudo(self.user_hrmanager_id) @@ -126,7 +126,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'holiday_status_id': self.holidays_status_1.id, 'date_from': (datetime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M'), 'date_to': datetime.today(), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) # Employee creates a leave request in a limited category -> crash, not enough days left @@ -137,7 +137,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'holiday_status_id': self.holidays_status_2.id, 'date_from': (datetime.today() + relativedelta(days=1)).strftime('%Y-%m-%d %H:%M'), 'date_to': (datetime.today() + relativedelta(days=2)), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) # Clean transaction @@ -148,7 +148,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'name': 'Days for limited category', 'employee_id': self.employee_emp_id, 'holiday_status_id': self.holidays_status_2.id, - 'number_of_days_temp': 2, + 'number_of_days': 2, }) # HrUser validates the first step aloc1_user_group.action_approve() @@ -166,7 +166,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'holiday_status_id': self.holidays_status_2.id, 'date_from': (datetime.today() + relativedelta(days=2)).strftime('%Y-%m-%d %H:%M'), 'date_to': (datetime.today() + relativedelta(days=3)), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) hol2_user_group = hol2.sudo(self.user_hruser_id) # Check left days: - 1 virtual remaining day @@ -205,7 +205,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): hol2_manager_group.write({ 'date_from': (datetime.today() + relativedelta(days=4)).strftime('%Y-%m-%d %H:%M'), 'date_to': (datetime.today() + relativedelta(days=7)), - 'number_of_days_temp': 4, + 'number_of_days': 4, }) with self.assertRaises(ValidationError): hol2_manager_group.action_confirm() @@ -220,7 +220,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'date_from': datetime.today().strftime('%Y-%m-10 10:00:00'), 'date_to': datetime.today().strftime('%Y-%m-11 19:00:00'), 'employee_id': employee_id, - 'number_of_days_temp': 1, + 'number_of_days': 1, }) # I find a small mistake on my leave request to I click on "Refuse" button to correct a mistake. hol3.action_refuse() @@ -242,7 +242,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'date_from': fields.Datetime.from_string('2017-03-03 06:00:00'), 'date_to': fields.Datetime.from_string('2017-03-11 19:00:00'), 'employee_id': employee_id, - 'number_of_days_temp': 1, + 'number_of_days': 1, }).unlink() with self.assertRaises(ValidationError): @@ -252,7 +252,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'date_from': fields.Datetime.from_string('2017-07-03 06:00:00'), 'date_to': fields.Datetime.from_string('2017-07-11 19:00:00'), 'employee_id': employee_id, - 'number_of_days_temp': 1, + 'number_of_days': 1, }) hol41 = HolidaysEmployeeGroup.create({ @@ -261,7 +261,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'holiday_status_id': self.holidays_status_1.id, 'date_from': (datetime.today() + relativedelta(days=9)).strftime('%Y-%m-%d %H:%M'), 'date_to': (datetime.today() + relativedelta(days=10)), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) # A simple user should be able to reset it's own leave @@ -274,7 +274,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'holiday_status_id': self.holidays_status_1.id, 'date_from': (datetime.today() + relativedelta(days=9)).strftime('%Y-%m-%d %H:%M'), 'date_to': (datetime.today() + relativedelta(days=10)), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) # A manager should be able to reset someone else's leave @@ -288,7 +288,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'holiday_status_id': self.holidays_status_1.id, 'date_from': (datetime.today() + relativedelta(days=15)).strftime('%Y-%m-%d %H:%M'), 'date_to': (datetime.today() + relativedelta(days=16)), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) hol51.action_approve() @@ -300,7 +300,7 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'holiday_status_id': self.holidays_status_1.id, 'date_from': (datetime.today() + relativedelta(days=15)).strftime('%Y-%m-%d %H:%M'), 'date_to': (datetime.today() + relativedelta(days=16)), - 'number_of_days_temp': 1, + 'number_of_days': 1, }) hol52.action_approve() diff --git a/addons/hr_holidays/tests/test_hr_leave_type.py b/addons/hr_holidays/tests/test_hr_leave_type.py index ebd2851ff60..495d1aa2e4b 100644 --- a/addons/hr_holidays/tests/test_hr_leave_type.py +++ b/addons/hr_holidays/tests/test_hr_leave_type.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + from datetime import datetime from dateutil.relativedelta import relativedelta @@ -7,35 +8,25 @@ from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase class TestHrLeaveType(TestHrHolidaysBase): - def setUp(self): - super(TestHrLeaveType, self).setUp() - - LeaveType = self.env['hr.leave.type'] - - self.leave_leave = LeaveType.create({ - 'name': 'Legal Leaves', - 'limit': True, - 'time_type': 'leave', - }) def test_time_type(self): - Leave = self.env['hr.leave'] - RLeave = self.env['resource.calendar.leaves'] - - leave_1 = Leave.create({ - 'name': 'Doctor Appointment', - 'employee_id': self.employee_hruser_id, - 'holiday_status_id': self.leave_leave.id, - 'date_from': (datetime.today() - relativedelta(days=1)), - 'date_to': datetime.today(), - 'number_of_days_temp': 1, + leave_type = self.env['hr.leave.type'].create({ + 'name': 'Legal Leaves', + 'time_type': 'leave', + 'allocation_type': 'no', }) + leave_1 = self.env['hr.leave'].create({ + 'name': 'Doctor Appointment', + 'employee_id': self.employee_hruser_id, + 'holiday_status_id': leave_type.id, + 'date_from': (datetime.today() - relativedelta(days=1)), + 'date_to': datetime.today(), + 'number_of_days': 1, + }) leave_1.action_approve() - self.assertEqual(RLeave.search([('holiday_id', '=', leave_1.id)]).time_type, 'leave') - - leave_1.action_refuse() - leave_1.action_draft() - - leave_1.unlink() + self.assertEqual( + self.env['resource.calendar.leaves'].search([('holiday_id', '=', leave_1.id)]).time_type, + 'leave' + ) diff --git a/addons/hr_holidays/views/hr_leave_allocation_views.xml b/addons/hr_holidays/views/hr_leave_allocation_views.xml index dc332b1a574..1befd24ebb5 100644 --- a/addons/hr_holidays/views/hr_leave_allocation_views.xml +++ b/addons/hr_holidays/views/hr_leave_allocation_views.xml @@ -47,65 +47,48 @@ - - Allocation Request + + hr.leave.allocation.view.form hr.leave.allocation + 32

-
-

+

- + -