From a0f4f6548bdb9a26151fc8a292d152fc67d1a13c Mon Sep 17 00:00:00 2001 From: RomainLibert Date: Fri, 12 Jan 2018 14:58:57 +0100 Subject: [PATCH] [REF] hr_holidays: Split/Rename models/views according to guidelines Purpose ======= Follow the coding guidelines (https://www.odoo.com/documentation/11.0/reference/guidelines.html) Specification ============= * each model has now its own .py file * view files have better names * reordered the methods for readability * reimplemented email subtypes --- addons/hr_holidays/__manifest__.py | 6 +- addons/hr_holidays/data/hr_holidays_data.xml | 95 +- addons/hr_holidays/models/__init__.py | 4 +- addons/hr_holidays/models/hr_holidays.py | 904 ------------------ addons/hr_holidays/models/hr_leave.py | 458 +++++++++ .../hr_holidays/models/hr_leave_allocation.py | 311 ++++++ addons/hr_holidays/models/hr_leave_type.py | 147 +++ addons/hr_holidays/report/__init__.py | 2 +- .../report/holidays_summary_report.py | 1 - .../hr_holidays/report/hr_holidays_views.xml | 215 ----- ...eport_all_leaves.py => hr_leave_report.py} | 0 .../hr_holidays/report/hr_leave_reports.xml | 164 ++++ .../hr_holidays/views/hr_holidays_views.xml | 815 ---------------- .../views/hr_leave_allocation_views.xml | 215 +++++ .../hr_holidays/views/hr_leave_type_views.xml | 113 +++ addons/hr_holidays/views/hr_leave_views.xml | 437 +++++++++ addons/hr_holidays/views/hr_views.xml | 54 ++ 17 files changed, 1986 insertions(+), 1955 deletions(-) delete mode 100644 addons/hr_holidays/models/hr_holidays.py create mode 100644 addons/hr_holidays/models/hr_leave.py create mode 100644 addons/hr_holidays/models/hr_leave_allocation.py create mode 100644 addons/hr_holidays/models/hr_leave_type.py delete mode 100644 addons/hr_holidays/report/hr_holidays_views.xml rename addons/hr_holidays/report/{report_all_leaves.py => hr_leave_report.py} (100%) create mode 100644 addons/hr_holidays/report/hr_leave_reports.xml delete mode 100644 addons/hr_holidays/views/hr_holidays_views.xml create mode 100644 addons/hr_holidays/views/hr_leave_allocation_views.xml create mode 100644 addons/hr_holidays/views/hr_leave_type_views.xml create mode 100644 addons/hr_holidays/views/hr_leave_views.xml diff --git a/addons/hr_holidays/__manifest__.py b/addons/hr_holidays/__manifest__.py index 7361860c627..ed14ca78a0c 100644 --- a/addons/hr_holidays/__manifest__.py +++ b/addons/hr_holidays/__manifest__.py @@ -33,7 +33,9 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p 'security/ir.model.access.csv', 'views/resource_views.xml', - 'views/hr_holidays_views.xml', + 'views/hr_leave_type_views.xml', + 'views/hr_leave_allocation_views.xml', + 'views/hr_leave_views.xml', 'views/hr_views.xml', 'wizard/hr_holidays_summary_department_views.xml', @@ -41,7 +43,7 @@ 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_holidays_views.xml', + 'report/hr_leave_reports.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 c6c155a385f..235bd6e036d 100644 --- a/addons/hr_holidays/data/hr_holidays_data.xml +++ b/addons/hr_holidays/data/hr_holidays_data.xml @@ -31,65 +31,128 @@ - + Confirmed hr.leave Request created and waiting confirmation - + Waiting Second Validation hr.leave Request validated, waiting second validation - + Approved hr.leave Request approved - + Refused hr.leave Request refused + + + Confirmed + hr.leave.allocation + Request created and waiting confirmation + + + + Waiting Second Validation + hr.leave.allocation + + Request validated, waiting second validation + + + + Approved + hr.leave.allocation + Request approved + + + + Refused + hr.leave.allocation + + Request refused + + - - Leaves/Allocations Confirmed + + Leaves Confirmed hr.department - + department_id - - Leaves/Allocations First Approval + + Leaves First Approval hr.department - + department_id - - Leaves/Allocation Approved + + Leaves Approved hr.department - + department_id - - Leaves/Allocation Refused + + Leaves Refused hr.department - + + department_id + + + + + Allocations Confirmed + hr.department + + + department_id + + + + + Allocations First Approval + hr.department + + + department_id + + + + + Allocation Approved + hr.department + + + department_id + + + + + Allocation Refused + hr.department + + department_id diff --git a/addons/hr_holidays/models/__init__.py b/addons/hr_holidays/models/__init__.py index 334982c4a1d..621aeccd08b 100644 --- a/addons/hr_holidays/models/__init__.py +++ b/addons/hr_holidays/models/__init__.py @@ -3,4 +3,6 @@ from . import resource from . import hr -from . import hr_holidays +from . import hr_leave +from . import hr_leave_allocation +from . import hr_leave_type diff --git a/addons/hr_holidays/models/hr_holidays.py b/addons/hr_holidays/models/hr_holidays.py deleted file mode 100644 index a21be760c68..00000000000 --- a/addons/hr_holidays/models/hr_holidays.py +++ /dev/null @@ -1,904 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com) - -import logging -import math -from datetime import timedelta - -from odoo import api, fields, models -from odoo.exceptions import UserError, AccessError, ValidationError -from odoo.tools import float_compare -from odoo.tools.translate import _ - -_logger = logging.getLogger(__name__) - - -HOURS_PER_DAY = 8 - - -class HolidaysType(models.Model): - _name = "hr.leave.type" - _description = "Leave Type" - - name = fields.Char('Leave Type', required=True, translate=True) - 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'), - ('blue', 'Blue'), - ('lightgreen', 'Light Green'), - ('lightblue', 'Light Blue'), - ('lightyellow', 'Light Yellow'), - ('magenta', 'Magenta'), - ('lightcyan', 'Light Cyan'), - ('black', 'Black'), - ('lightpink', 'Light Pink'), - ('brown', 'Brown'), - ('violet', 'Violet'), - ('lightcoral', 'Light Coral'), - ('lightsalmon', 'Light Salmon'), - ('lavender', 'Lavender'), - ('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('Allow to Override Limit', - help='If you select this check box, the system allows the employees to take more leaves ' - 'than the available ones for this type and will not take them into account for the ' - '"Remaining Legal Leaves" defined on the employee form.') - 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.") - - 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 negative value.') - 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', - help='Maximum Leaves Allowed - Leaves Already Taken - Leaves Waiting Approval') - - double_validation = fields.Boolean(string='Apply Double Validation', - help="When selected, the Allocation/Leave Requests for this type require a second validation to be approved.") - company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id) - - @api.multi - def get_days(self, employee_id): - # need to use `dict` constructor to create a dict per id - result = dict((id, dict(max_leaves=0, leaves_taken=0, remaining_leaves=0, virtual_remaining_leaves=0)) for id in self.ids) - - requests = self.env['hr.leave'].search([ - ('employee_id', '=', employee_id), - ('state', 'in', ['confirm', 'validate1', 'validate']), - ('holiday_status_id', 'in', self.ids) - ]) - - allocations = self.env['hr.leave.allocation'].search([ - ('employee_id', '=', employee_id), - ('state', 'in', ['confirm', 'validate1', 'validate']), - ('holiday_status_id', 'in', self.ids) - ]) - - for request in requests: - status_dict = result[request.holiday_status_id.id] - status_dict['virtual_remaining_leaves'] -= request.number_of_days_temp - if request.state == 'validate': - status_dict['leaves_taken'] += request.number_of_days_temp - status_dict['remaining_leaves'] -= request.number_of_days_temp - - for allocation in allocations: - status_dict = result[allocation.holiday_status_id.id] - if allocation.state == 'validate': - # 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 - - return result - - @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 - - if employee_id: - data_days = self.get_days(employee_id) - - for holiday_status in self: - result = data_days.get(holiday_status.id, {}) - holiday_status.max_leaves = result.get('max_leaves', 0) - holiday_status.leaves_taken = result.get('leaves_taken', 0) - holiday_status.remaining_leaves = result.get('remaining_leaves', 0) - holiday_status.virtual_remaining_leaves = result.get('virtual_remaining_leaves', 0) - - @api.multi - def name_get(self): - if not self._context.get('employee_id'): - # leave counts is based on employee_id, would be inaccurate if not based on correct employee - return super(HolidaysType, self).name_get() - res = [] - for record in self: - name = record.name - 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) - } - res.append((record.id, name)) - return res - - @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): - """ Override _search to order the results, according to some employee. - The order is the following - - - limit (limited leaves first, such as Legal Leaves) - - virtual remaining leaves (higher the better, so using reverse on sorted) - - This override is necessary because those fields are not stored and depends - on an employee_id given in context. This sort will be done when there - is an employee_id in context and that no other order has been given - to the method. - """ - 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'): - leaves = self.browse(leave_ids) - sort_key = lambda l: (not l.limit, l.virtual_remaining_leaves) - return leaves.sorted(key=sort_key, reverse=True).ids - return leave_ids - - -class HolidaysRequest(models.Model): - _name = "hr.leave" - _description = "Leave" - _order = "date_from desc" - _inherit = ['mail.thread'] - - 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) - - name = fields.Char('Description') - state = fields.Selection([ - ('draft', 'To Submit'), - ('cancel', 'Cancelled'), - ('confirm', 'To Approve'), - ('refuse', 'Refused'), - ('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.') - 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, - states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) - 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', related='employee_id.parent_id', string='Manager', readonly=True, store=True) - notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) - number_of_days_temp = fields.Float( - 'Allocation', copy=False, readonly=True, - 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') - meeting_id = fields.Many2one('calendar.event', string='Meeting') - - parent_id = fields.Many2one('hr.leave', string='Parent') - 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'), - ('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/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, - 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', - help='This area is automaticly filled by the user who validate the leave with second level (If Leave type need second validation)') - double_validation = fields.Boolean('Apply Double Validation', related='holiday_status_id.double_validation') - can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') - - @api.multi - @api.depends('number_of_days_temp') - def _compute_number_of_days(self): - for holiday in self: - holiday.number_of_days = -holiday.number_of_days_temp - - @api.multi - def _compute_can_reset(self): - """ User can reset a leave request if it is its own leave request - or if he is an Hr Manager. - """ - user = self.env.user - group_hr_manager = self.env.ref('hr_holidays.group_hr_holidays_manager') - for holiday in self: - if group_hr_manager in user.groups_id or holiday.employee_id and holiday.employee_id.user_id == user: - holiday.can_reset = True - - @api.constrains('date_from', 'date_to') - def _check_date(self): - for holiday in self: - domain = [ - ('date_from', '<=', holiday.date_to), - ('date_to', '>=', holiday.date_from), - ('employee_id', '=', holiday.employee_id.id), - ('id', '!=', holiday.id), - ('state', 'not in', ['cancel', 'refuse']), - ] - nholidays = self.search_count(domain) - if nholidays: - raise ValidationError(_('You can not have 2 leaves that overlaps on same day!')) - - @api.constrains('state', 'number_of_days_temp') - def _check_holidays(self): - for holiday in self: - if holiday.holiday_type != 'employee' or not holiday.employee_id or holiday.holiday_status_id.limit: - 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 \ - float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1: - raise ValidationError(_('The number of remaining leaves is not sufficient for this leave type.\n' - 'Please verify also the leaves waiting for validation.')) - - _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_check2', "CHECK ((date_from <= date_to))", "The start date must be anterior to the end date."), - ('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days must be greater than 0."), - ] - - @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 - elif self.holiday_type == 'category': - self.employee_id = None - self.department_id = None - - @api.onchange('employee_id') - def _onchange_employee(self): - if self.holiday_type == 'employee': - self.department_id = self.employee_id.department_id - - def _get_number_of_days(self, date_from, date_to, employee_id): - """ Returns a float equals to the timedelta between two dates given as string.""" - from_dt = fields.Datetime.from_string(date_from) - to_dt = fields.Datetime.from_string(date_to) - - if employee_id: - employee = self.env['hr.employee'].browse(employee_id) - return employee.get_work_days_count(from_dt, to_dt) - - time_delta = to_dt - from_dt - return math.ceil(time_delta.days + float(time_delta.seconds) / 86400) - - @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 = self.date_from - date_to = self.date_to - - # No date_to set so far: automatically compute one 8 hours later - if date_from and not date_to: - date_to_with_delta = fields.Datetime.from_string(date_from) + timedelta(hours=HOURS_PER_DAY) - self.date_to = str(date_to_with_delta) - - # 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 = self.date_from - date_to = self.date_to - - # 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 - - #################################################### - # ORM Overrides methods - #################################################### - - @api.multi - def name_get(self): - 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))) - 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))) - return res - - @api.multi - def add_follower(self, employee_id): - employee = self.env['hr.employee'].browse(employee_id) - if employee.user_id: - self.message_subscribe_users(user_ids=employee.user_id.ids) - - @api.model - def create(self, values): - """ Override to avoid automatic logging of creation """ - 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}) - holiday = super(HolidaysRequest, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(values) - holiday.add_follower(employee_id) - return holiday - - @api.multi - def write(self, values): - employee_id = values.get('employee_id', False) - result = super(HolidaysRequest, self).write(values) - self.add_follower(employee_id) - return result - - @api.multi - def unlink(self): - for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']): - raise UserError(_('You cannot delete a leave which is in %s state.') % (holiday.state,)) - return super(HolidaysRequest, self).unlink() - - @api.multi - def copy_data(self, default=None): - raise UserError(_('A leave cannot be duplicated.')) - - #################################################### - # Business methods - #################################################### - - @api.multi - def _create_resource_leave(self): - """ This method will create entry in resource calendar leave object at the time of holidays validated """ - for leave in self: - self.env['resource.calendar.leaves'].create({ - 'name': leave.name, - 'date_from': leave.date_from, - 'holiday_id': leave.id, - 'date_to': leave.date_to, - 'resource_id': leave.employee_id.resource_id.id, - 'calendar_id': leave.employee_id.resource_calendar_id.id - }) - return True - - @api.multi - def _remove_resource_leave(self): - """ This method will create entry in resource calendar leave object at the time of holidays cancel/removed """ - return self.env['resource.calendar.leaves'].search([('holiday_id', 'in', self.ids)]).unlink() - - @api.multi - def action_draft(self): - for holiday in self: - if not holiday.can_reset: - raise UserError(_('Only an HR Manager or the concerned employee can reset to draft.')) - if holiday.state not in ['confirm', 'refuse']: - raise UserError(_('Leave request state must be "Refused" or "To Approve" in order to reset to Draft.')) - holiday.write({ - 'state': 'draft', - 'first_approver_id': False, - 'second_approver_id': False, - }) - linked_requests = holiday.mapped('linked_request_ids') - for linked_request in linked_requests: - linked_request.action_draft() - linked_requests.unlink() - return True - - @api.multi - def action_confirm(self): - if self.filtered(lambda holiday: holiday.state != 'draft'): - raise UserError(_('Leave request must be in Draft state ("To Submit") in order to confirm it.')) - return self.write({'state': 'confirm'}) - - # TODO: Do we have to remove this method? - @api.multi - def _check_security_action_approve(self): - if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): - raise UserError(_('Only an HR Officer or Manager can approve leave requests.')) - - @api.multi - def action_approve(self): - # if double_validation: this method is the first approval approval - # if not double_validation: this method calls action_validate() below - self._check_security_action_approve() - - current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) - if any(holiday.state != 'confirm' for holiday in self): - raise UserError(_('Leave request must be confirmed ("To Approve") in order to approve it.')) - - self.filtered(lambda hol: hol.double_validation).write({'state': 'validate1', 'first_approver_id': current_employee.id}) - self.filtered(lambda hol: not hol.double_validation).action_validate() - return True - - @api.multi - def _prepare_holiday_values(self, employee): - self.ensure_one() - values = { - 'name': self.name, - 'holiday_type': 'employee', - 'holiday_status_id': self.holiday_status_id.id, - 'date_from': self.date_from, - 'date_to': self.date_to, - 'notes': self.notes, - 'number_of_days_temp': self.number_of_days_temp, - 'parent_id': self.id, - 'employee_id': employee.id - } - return values - - @api.multi - def _check_security_action_validate(self): - if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): - raise UserError(_('Only an HR Officer or Manager can approve leave requests.')) - - @api.multi - def action_validate(self): - self._check_security_action_validate() - - current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) - for holiday in self: - if holiday.state not in ['confirm', 'validate1']: - raise UserError(_('Leave request must be confirmed in order to approve it.')) - if holiday.state == 'validate1' and not holiday.env.user.has_group('hr_holidays.group_hr_holidays_manager'): - raise UserError(_('Only an HR Manager can apply the second approval on leave requests.')) - - holiday.write({'state': 'validate'}) - if holiday.double_validation: - holiday.write({'second_approver_id': current_employee.id}) - else: - holiday.write({'first_approver_id': current_employee.id}) - if holiday.holiday_type == 'employee': - holiday._validate_leave_request() - elif holiday.holiday_type in ['category', 'department']: - leaves = self.env['hr.leave'] - 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].double_validation: - leaves.action_validate() - return True - - def _validate_leave_request(self): - """ Validate leave requests (holiday_type='employee') - by creating a calendar event and a resource leaves. """ - for holiday in self.filtered(lambda request: request.holiday_type == 'employee'): - meeting_values = holiday._prepare_holidays_meeting_values() - meeting = self.env['calendar.event'].with_context(no_mail_to_attendees=True).create(meeting_values) - holiday.write({'meeting_id': meeting.id}) - holiday._create_resource_leave() - - @api.multi - def _prepare_holidays_meeting_values(self): - self.ensure_one() - 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 * HOURS_PER_DAY, - 'description': self.notes, - 'user_id': self.user_id.id, - 'start': self.date_from, - 'stop': self.date_to, - 'allday': False, - 'state': 'open', # to block that meeting date in the calendar - 'privacy': 'confidential' - } - # Add the partner_id (if exist) as an attendee - if self.user_id and self.user_id.partner_id: - meeting_values['partner_ids'] = [ - (4, self.user_id.partner_id.id)] - return meeting_values - - @api.multi - def action_refuse(self): - self._check_security_action_refuse() - - current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) - for holiday in self: - if holiday.state not in ['confirm', 'validate', 'validate1']: - raise UserError(_('Leave request must be confirmed or validated in order to refuse it.')) - - if holiday.state == 'validate1': - holiday.write({'state': 'refuse', 'first_approver_id': current_employee.id}) - else: - holiday.write({'state': 'refuse', 'second_approver_id': current_employee.id}) - # Delete the meeting - if holiday.meeting_id: - holiday.meeting_id.unlink() - # If a category that created several holidays, cancel all related - holiday.linked_request_ids.action_refuse() - self._remove_resource_leave() - return True - - @api.multi - def _check_security_action_refuse(self): - if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): - raise UserError(_('Only an HR Officer or Manager can refuse leave requests.')) - - #################################################### - # Messaging methods - #################################################### - - @api.multi - def _track_subtype(self, init_values): - if 'state' in init_values and self.state == 'validate': - return 'hr_holidays.mt_holidays_approved' - elif 'state' in init_values and self.state == 'validate1': - return 'hr_holidays.mt_holidays_first_validated' - elif 'state' in init_values and self.state == 'confirm': - return 'hr_holidays.mt_holidays_confirmed' - elif 'state' in init_values and self.state == 'refuse': - return 'hr_holidays.mt_holidays_refused' - return super(HolidaysRequest, self)._track_subtype(init_values) - - @api.multi - def _notification_recipients(self, message, groups): - """ Handle HR users and officers recipients that can validate or refuse holidays - directly from email. """ - groups = super(HolidaysRequest, self)._notification_recipients(message, groups) - - self.ensure_one() - hr_actions = [] - if self.state == 'confirm': - app_action = self._notification_link_helper('controller', controller='/hr_holidays_request/validate') - hr_actions += [{'url': app_action, 'title': _('Approve')}] - if self.state in ['confirm', 'validate', 'validate1']: - ref_action = self._notification_link_helper('controller', controller='/hr_holidays_request/refuse') - hr_actions += [{'url': ref_action, 'title': _('Refuse')}] - - new_group = ( - 'group_hr_holidays_user', lambda partner: bool(partner.user_ids) and any(user.has_group('hr_holidays.group_hr_holidays_user') for user in partner.user_ids), { - 'actions': hr_actions, - }) - - return [new_group] + groups - - @api.multi - def _message_notification_recipients(self, message, recipients): - result = super(HolidaysRequest, self)._message_notification_recipients(message, recipients) - title = _("See Leave") - for res in result: - if result[res].get('button_access'): - result[res]['button_access']['title'] = title - return result - - -class HolidaysAllocation(models.Model): - _name = "hr.leave.allocation" - _description = "Allocation" - _inherit = ['mail.thread'] - - 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) - - name = fields.Char('Description') - state = fields.Selection([ - ('draft', 'To Submit'), - ('cancel', 'Cancelled'), - ('confirm', 'To Approve'), - ('refuse', 'Refused'), - ('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.") - holiday_status_id = fields.Many2one("hr.leave.type", string="Leave Type", required=True, readonly=True, - states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) - 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( - 'Allocation', copy=False, readonly=True, - 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') - 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, - 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', - help='This area is automaticly filled by the user who validate the leave with second level (If Leave type need second validation)') - double_validation = fields.Boolean('Apply Double Validation', related='holiday_status_id.double_validation') - can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') - - @api.multi - @api.depends('number_of_days_temp') - def _compute_number_of_days(self): - for holiday in self: - holiday.number_of_days = holiday.number_of_days_temp - - @api.multi - def _compute_can_reset(self): - """ User can reset a leave request if it is its own leave request - or if he is an Hr Manager. - """ - user = self.env.user - group_hr_manager = self.env.ref('hr_holidays.group_hr_holidays_manager') - for holiday in self: - if group_hr_manager in user.groups_id or holiday.employee_id and holiday.employee_id.user_id == user: - holiday.can_reset = 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."), - ] - - @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 - elif self.holiday_type == 'category': - self.employee_id = None - self.department_id = None - - @api.onchange('employee_id') - def _onchange_employee(self): - if self.holiday_type == 'employee': - self.department_id = self.employee_id.department_id - - #################################################### - # ORM Overrides methods - #################################################### - - @api.multi - def name_get(self): - res = [] - for leave in self: - 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))) - return res - - def _check_state_access_right(self, vals): - if vals.get('state') and vals['state'] not in ['draft', 'confirm', 'cancel'] and not self.env['res.users'].has_group('hr_holidays.group_hr_holidays_user'): - return False - return True - - @api.multi - def add_follower(self, employee_id): - employee = self.env['hr.employee'].browse(employee_id) - if employee.user_id: - self.message_subscribe_users(user_ids=employee.user_id.ids) - - @api.model - def create(self, values): - """ Override to avoid automatic logging of creation """ - employee_id = values.get('employee_id', False) - if not self._check_state_access_right(values): - raise AccessError(_('You cannot set a leave request as \'%s\'. Contact a human resource manager.') % values.get('state')) - if not values.get('department_id'): - values.update({'department_id': self.env['hr.employee'].browse(employee_id).department_id.id}) - holiday = super(HolidaysAllocation, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(values) - holiday.add_follower(employee_id) - return holiday - - @api.multi - def write(self, values): - employee_id = values.get('employee_id', False) - if not self._check_state_access_right(values): - raise AccessError(_('You cannot set a leave request as \'%s\'. Contact a human resource manager.') % values.get('state')) - result = super(HolidaysAllocation, self).write(values) - self.add_follower(employee_id) - return result - - @api.multi - def unlink(self): - for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']): - raise UserError(_('You cannot delete a leave which is in %s state.') % (holiday.state,)) - return super(HolidaysAllocation, self).unlink() - - @api.multi - def copy_data(self, default=None): - raise UserError(_('A leave cannot be duplicated.')) - - #################################################### - # Business methods - #################################################### - - @api.multi - def action_draft(self): - for holiday in self: - if not holiday.can_reset: - raise UserError(_('Only an HR Manager or the concerned employee can reset to draft.')) - if holiday.state not in ['confirm', 'refuse']: - raise UserError(_('Leave request state must be "Refused" or "To Approve" in order to reset to Draft.')) - holiday.write({ - 'state': 'draft', - 'first_approver_id': False, - 'second_approver_id': False, - }) - linked_requests = holiday.mapped('linked_request_ids') - for linked_request in linked_requests: - linked_request.action_draft() - linked_requests.unlink() - return True - - @api.multi - def action_confirm(self): - if self.filtered(lambda holiday: holiday.state != 'draft'): - raise UserError(_('Leave request must be in Draft state ("To Submit") in order to confirm it.')) - return self.write({'state': 'confirm'}) - - @api.multi - def _check_security_action_approve(self): - if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): - raise UserError(_('Only an HR Officer or Manager can approve leave requests.')) - - @api.multi - def action_approve(self): - # if double_validation: this method is the first approval approval - # if not double_validation: this method calls action_validate() below - self._check_security_action_approve() - - current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) - if any(holiday.state != 'confirm' for holiday in self): - raise UserError(_('Leave request must be confirmed ("To Approve") in order to approve it.')) - - self.filtered(lambda hol: hol.double_validation).write({'state': 'validate1', 'first_approver_id': current_employee.id}) - self.filtered(lambda hol: not hol.double_validation).action_validate() - return True - - @api.multi - def _prepare_holiday_values(self, employee): - self.ensure_one() - values = { - 'name': self.name, - 'holiday_type': 'employee', - 'holiday_status_id': self.holiday_status_id.id, - 'notes': self.notes, - 'number_of_days_temp': self.number_of_days_temp, - 'parent_id': self.id, - 'employee_id': employee.id - } - return values - - @api.multi - def _check_security_action_validate(self): - if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): - raise UserError(_('Only an HR Officer or Manager can approve leave requests.')) - - @api.multi - def action_validate(self): - self._check_security_action_validate() - - current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) - for holiday in self: - if holiday.state not in ['confirm', 'validate1']: - raise UserError(_('Leave request must be confirmed in order to approve it.')) - if holiday.state == 'validate1' and not holiday.env.user.has_group('hr_holidays.group_hr_holidays_manager'): - raise UserError(_('Only an HR Manager can apply the second approval on leave requests.')) - - holiday.write({'state': 'validate'}) - if holiday.double_validation: - 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].double_validation: - leaves.action_validate() - return True - - @api.multi - def action_refuse(self): - self._check_security_action_refuse() - - current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) - for holiday in self: - if holiday.state not in ['confirm', 'validate', 'validate1']: - raise UserError(_('Leave request must be confirmed or validated in order to refuse it.')) - - if holiday.state == 'validate1': - holiday.write({'state': 'refuse', 'first_approver_id': current_employee.id}) - else: - holiday.write({'state': 'refuse', 'second_approver_id': current_employee.id}) - # If a category that created several holidays, cancel all related - holiday.linked_request_ids.action_refuse() - return True - - @api.multi - def _check_security_action_refuse(self): - if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): - raise UserError(_('Only an HR Officer or Manager can refuse leave requests.')) - - #################################################### - # Messaging methods - #################################################### - - @api.multi - def _track_subtype(self, init_values): - if 'state' in init_values and self.state == 'validate': - return 'hr_holidays.mt_holidays_approved' - elif 'state' in init_values and self.state == 'validate1': - return 'hr_holidays.mt_holidays_first_validated' - elif 'state' in init_values and self.state == 'confirm': - return 'hr_holidays.mt_holidays_confirmed' - elif 'state' in init_values and self.state == 'refuse': - return 'hr_holidays.mt_holidays_refused' - return super(HolidaysAllocation, self)._track_subtype(init_values) - - @api.multi - def _notification_recipients(self, message, groups): - """ Handle HR users and officers recipients that can validate or refuse holidays - directly from email. """ - groups = super(HolidaysAllocation, self)._notification_recipients(message, groups) - - self.ensure_one() - hr_actions = [] - if self.state == 'confirm': - app_action = self._notification_link_helper('controller', controller='/hr_holidays_allocation/validate') - hr_actions += [{'url': app_action, 'title': _('Approve')}] - if self.state in ['confirm', 'validate', 'validate1']: - ref_action = self._notification_link_helper('controller', controller='/hr_holidays_allocation/refuse') - hr_actions += [{'url': ref_action, 'title': _('Refuse')}] - - new_group = ( - 'group_hr_holidays_user', lambda partner: bool(partner.user_ids) and any(user.has_group('hr_holidays.group_hr_holidays_user') for user in partner.user_ids), { - 'actions': hr_actions, - }) - - return [new_group] + groups - - @api.multi - def _message_notification_recipients(self, message, recipients): - result = super(HolidaysAllocation, self)._message_notification_recipients(message, recipients) - title = _("See Allocation") - for res in result: - if result[res].get('button_access'): - result[res]['button_access']['title'] = title - return result diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py new file mode 100644 index 00000000000..66f209b40f2 --- /dev/null +++ b/addons/hr_holidays/models/hr_leave.py @@ -0,0 +1,458 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com) + +import logging +import math +from datetime import timedelta + +from odoo import api, fields, models +from odoo.exceptions import UserError, AccessError, ValidationError +from odoo.tools import float_compare +from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) + + +HOURS_PER_DAY = 8 + +class HolidaysRequest(models.Model): + _name = "hr.leave" + _description = "Leave" + _order = "date_from desc" + _inherit = ['mail.thread'] + + 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) + + name = fields.Char('Description') + state = fields.Selection([ + ('draft', 'To Submit'), + ('cancel', 'Cancelled'), + ('confirm', 'To Approve'), + ('refuse', 'Refused'), + ('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.') + 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, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) + 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', related='employee_id.parent_id', string='Manager', readonly=True, store=True) + notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) + number_of_days_temp = fields.Float( + 'Allocation', copy=False, readonly=True, + 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') + meeting_id = fields.Many2one('calendar.event', string='Meeting') + + parent_id = fields.Many2one('hr.leave', string='Parent') + 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'), + ('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/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, + 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', + help='This area is automaticly filled by the user who validate the leave with second level (If Leave type need second validation)') + double_validation = fields.Boolean('Apply Double Validation', related='holiday_status_id.double_validation') + can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') + + _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_check2', "CHECK ((date_from <= date_to))", "The start date must be anterior to the end date."), + ('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days must be greater than 0."), + ] + + @api.multi + @api.depends('number_of_days_temp') + def _compute_number_of_days(self): + for holiday in self: + holiday.number_of_days = -holiday.number_of_days_temp + + @api.multi + def _compute_can_reset(self): + """ User can reset a leave request if it is its own leave request + or if he is an Hr Manager. + """ + user = self.env.user + group_hr_manager = self.env.ref('hr_holidays.group_hr_holidays_manager') + for holiday in self: + if group_hr_manager in user.groups_id or holiday.employee_id and holiday.employee_id.user_id == user: + holiday.can_reset = True + + @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 + elif self.holiday_type == 'category': + self.employee_id = None + self.department_id = None + + @api.onchange('employee_id') + def _onchange_employee(self): + if self.holiday_type == 'employee': + self.department_id = self.employee_id.department_id + + @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 = self.date_from + date_to = self.date_to + + # No date_to set so far: automatically compute one 8 hours later + if date_from and not date_to: + date_to_with_delta = fields.Datetime.from_string(date_from) + timedelta(hours=HOURS_PER_DAY) + self.date_to = str(date_to_with_delta) + + # 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 = self.date_from + date_to = self.date_to + + # 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.constrains('date_from', 'date_to') + def _check_date(self): + for holiday in self: + domain = [ + ('date_from', '<=', holiday.date_to), + ('date_to', '>=', holiday.date_from), + ('employee_id', '=', holiday.employee_id.id), + ('id', '!=', holiday.id), + ('state', 'not in', ['cancel', 'refuse']), + ] + nholidays = self.search_count(domain) + if nholidays: + raise ValidationError(_('You can not have 2 leaves that overlaps on same day!')) + + @api.constrains('state', 'number_of_days_temp') + def _check_holidays(self): + for holiday in self: + if holiday.holiday_type != 'employee' or not holiday.employee_id or holiday.holiday_status_id.limit: + 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 \ + float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1: + raise ValidationError(_('The number of remaining leaves is not sufficient for this leave type.\n' + 'Please verify also the leaves waiting for validation.')) + + def _get_number_of_days(self, date_from, date_to, employee_id): + """ Returns a float equals to the timedelta between two dates given as string.""" + from_dt = fields.Datetime.from_string(date_from) + to_dt = fields.Datetime.from_string(date_to) + + if employee_id: + employee = self.env['hr.employee'].browse(employee_id) + return employee.get_work_days_count(from_dt, to_dt) + + time_delta = to_dt - from_dt + return math.ceil(time_delta.days + float(time_delta.seconds) / 86400) + + #################################################### + # ORM Overrides methods + #################################################### + + @api.multi + def name_get(self): + 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))) + 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))) + return res + + @api.multi + def add_follower(self, employee_id): + employee = self.env['hr.employee'].browse(employee_id) + if employee.user_id: + self.message_subscribe_users(user_ids=employee.user_id.ids) + + @api.model + def create(self, values): + """ Override to avoid automatic logging of creation """ + 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}) + holiday = super(HolidaysRequest, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(values) + holiday.add_follower(employee_id) + return holiday + + @api.multi + def write(self, values): + employee_id = values.get('employee_id', False) + result = super(HolidaysRequest, self).write(values) + self.add_follower(employee_id) + return result + + @api.multi + def unlink(self): + for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']): + raise UserError(_('You cannot delete a leave which is in %s state.') % (holiday.state,)) + return super(HolidaysRequest, self).unlink() + + @api.multi + def copy_data(self, default=None): + raise UserError(_('A leave cannot be duplicated.')) + + #################################################### + # Business methods + #################################################### + + @api.multi + def _create_resource_leave(self): + """ This method will create entry in resource calendar leave object at the time of holidays validated """ + for leave in self: + self.env['resource.calendar.leaves'].create({ + 'name': leave.name, + 'date_from': leave.date_from, + 'holiday_id': leave.id, + 'date_to': leave.date_to, + 'resource_id': leave.employee_id.resource_id.id, + 'calendar_id': leave.employee_id.resource_calendar_id.id + }) + return True + + @api.multi + def _remove_resource_leave(self): + """ This method will create entry in resource calendar leave object at the time of holidays cancel/removed """ + return self.env['resource.calendar.leaves'].search([('holiday_id', 'in', self.ids)]).unlink() + + def _validate_leave_request(self): + """ Validate leave requests (holiday_type='employee') + by creating a calendar event and a resource leaves. """ + for holiday in self.filtered(lambda request: request.holiday_type == 'employee'): + meeting_values = holiday._prepare_holidays_meeting_values() + meeting = self.env['calendar.event'].with_context(no_mail_to_attendees=True).create(meeting_values) + holiday.write({'meeting_id': meeting.id}) + holiday._create_resource_leave() + + @api.multi + def _prepare_holidays_meeting_values(self): + self.ensure_one() + 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 * HOURS_PER_DAY, + 'description': self.notes, + 'user_id': self.user_id.id, + 'start': self.date_from, + 'stop': self.date_to, + 'allday': False, + 'state': 'open', # to block that meeting date in the calendar + 'privacy': 'confidential' + } + # Add the partner_id (if exist) as an attendee + if self.user_id and self.user_id.partner_id: + meeting_values['partner_ids'] = [ + (4, self.user_id.partner_id.id)] + return meeting_values + + @api.multi + def _prepare_holiday_values(self, employee): + self.ensure_one() + values = { + 'name': self.name, + 'holiday_type': 'employee', + 'holiday_status_id': self.holiday_status_id.id, + 'date_from': self.date_from, + 'date_to': self.date_to, + 'notes': self.notes, + 'number_of_days_temp': self.number_of_days_temp, + 'parent_id': self.id, + 'employee_id': employee.id + } + return values + + @api.multi + def action_draft(self): + for holiday in self: + if not holiday.can_reset: + raise UserError(_('Only an HR Manager or the concerned employee can reset to draft.')) + if holiday.state not in ['confirm', 'refuse']: + raise UserError(_('Leave request state must be "Refused" or "To Approve" in order to reset to Draft.')) + holiday.write({ + 'state': 'draft', + 'first_approver_id': False, + 'second_approver_id': False, + }) + linked_requests = holiday.mapped('linked_request_ids') + for linked_request in linked_requests: + linked_request.action_draft() + linked_requests.unlink() + return True + + @api.multi + def action_confirm(self): + if self.filtered(lambda holiday: holiday.state != 'draft'): + raise UserError(_('Leave request must be in Draft state ("To Submit") in order to confirm it.')) + return self.write({'state': 'confirm'}) + + @api.multi + def _check_security_action_approve(self): + if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): + raise UserError(_('Only an HR Officer or Manager can approve leave requests.')) + + @api.multi + def action_approve(self): + # if double_validation: this method is the first approval approval + # if not double_validation: this method calls action_validate() below + self._check_security_action_approve() + + current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + if any(holiday.state != 'confirm' for holiday in self): + raise UserError(_('Leave request must be confirmed ("To Approve") in order to approve it.')) + + self.filtered(lambda hol: hol.double_validation).write({'state': 'validate1', 'first_approver_id': current_employee.id}) + self.filtered(lambda hol: not hol.double_validation).action_validate() + return True + + @api.multi + def _check_security_action_validate(self): + if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): + raise UserError(_('Only an HR Officer or Manager can approve leave requests.')) + + @api.multi + def action_validate(self): + self._check_security_action_validate() + + current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + for holiday in self: + if holiday.state not in ['confirm', 'validate1']: + raise UserError(_('Leave request must be confirmed in order to approve it.')) + if holiday.state == 'validate1' and not holiday.env.user.has_group('hr_holidays.group_hr_holidays_manager'): + raise UserError(_('Only an HR Manager can apply the second approval on leave requests.')) + + holiday.write({'state': 'validate'}) + if holiday.double_validation: + holiday.write({'second_approver_id': current_employee.id}) + else: + holiday.write({'first_approver_id': current_employee.id}) + if holiday.holiday_type == 'employee': + holiday._validate_leave_request() + elif holiday.holiday_type in ['category', 'department']: + leaves = self.env['hr.leave'] + 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].double_validation: + leaves.action_validate() + return True + + @api.multi + def action_refuse(self): + self._check_security_action_refuse() + + current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + for holiday in self: + if holiday.state not in ['confirm', 'validate', 'validate1']: + raise UserError(_('Leave request must be confirmed or validated in order to refuse it.')) + + if holiday.state == 'validate1': + holiday.write({'state': 'refuse', 'first_approver_id': current_employee.id}) + else: + holiday.write({'state': 'refuse', 'second_approver_id': current_employee.id}) + # Delete the meeting + if holiday.meeting_id: + holiday.meeting_id.unlink() + # If a category that created several holidays, cancel all related + holiday.linked_request_ids.action_refuse() + self._remove_resource_leave() + return True + + @api.multi + def _check_security_action_refuse(self): + if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): + raise UserError(_('Only an HR Officer or Manager can refuse leave requests.')) + + #################################################### + # Messaging methods + #################################################### + + @api.multi + def _track_subtype(self, init_values): + if 'state' in init_values and self.state == 'validate': + return 'hr_holidays.mt_leave_approved' + elif 'state' in init_values and self.state == 'validate1': + return 'hr_holidays.mt_leave_first_validated' + elif 'state' in init_values and self.state == 'confirm': + return 'hr_holidays.mt_leave_confirmed' + elif 'state' in init_values and self.state == 'refuse': + return 'hr_holidays.mt_leave_refused' + return super(HolidaysRequest, self)._track_subtype(init_values) + + @api.multi + def _notification_recipients(self, message, groups): + """ Handle HR users and officers recipients that can validate or refuse holidays + directly from email. """ + groups = super(HolidaysRequest, self)._notification_recipients(message, groups) + + self.ensure_one() + hr_actions = [] + if self.state == 'confirm': + app_action = self._notification_link_helper('controller', controller='/leave/validate') + hr_actions += [{'url': app_action, 'title': _('Approve')}] + if self.state in ['confirm', 'validate', 'validate1']: + ref_action = self._notification_link_helper('controller', controller='/leave/refuse') + hr_actions += [{'url': ref_action, 'title': _('Refuse')}] + + new_group = ( + 'group_hr_holidays_user', lambda partner: bool(partner.user_ids) and any(user.has_group('hr_holidays.group_hr_holidays_user') for user in partner.user_ids), { + 'actions': hr_actions, + }) + + return [new_group] + groups + + @api.multi + def _message_notification_recipients(self, message, recipients): + result = super(HolidaysRequest, self)._message_notification_recipients(message, recipients) + title = _("See Leave") + for res in result: + if result[res].get('button_access'): + result[res]['button_access']['title'] = title + return result diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py new file mode 100644 index 00000000000..da1260fffef --- /dev/null +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -0,0 +1,311 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com) + +import logging + +from odoo import api, fields, models +from odoo.exceptions import UserError, AccessError +from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) + + +class HolidaysAllocation(models.Model): + _name = "hr.leave.allocation" + _description = "Allocation" + _inherit = ['mail.thread'] + + 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) + + name = fields.Char('Description') + state = fields.Selection([ + ('draft', 'To Submit'), + ('cancel', 'Cancelled'), + ('confirm', 'To Approve'), + ('refuse', 'Refused'), + ('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.") + holiday_status_id = fields.Many2one("hr.leave.type", string="Leave Type", required=True, readonly=True, + states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) + 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( + 'Allocation', copy=False, readonly=True, + 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') + 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, + 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', + help='This area is automaticly filled by the user who validate the leave with second level (If Leave type need second validation)') + double_validation = fields.Boolean('Apply Double Validation', related='holiday_status_id.double_validation') + can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') + + _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."), + ] + + @api.multi + @api.depends('number_of_days_temp') + def _compute_number_of_days(self): + for holiday in self: + holiday.number_of_days = holiday.number_of_days_temp + + @api.multi + def _compute_can_reset(self): + """ User can reset a leave request if it is its own leave request + or if he is an Hr Manager. + """ + user = self.env.user + group_hr_manager = self.env.ref('hr_holidays.group_hr_holidays_manager') + for holiday in self: + if group_hr_manager in user.groups_id or holiday.employee_id and holiday.employee_id.user_id == user: + holiday.can_reset = True + + @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 + elif self.holiday_type == 'category': + self.employee_id = None + self.department_id = None + + @api.onchange('employee_id') + def _onchange_employee(self): + if self.holiday_type == 'employee': + self.department_id = self.employee_id.department_id + + #################################################### + # ORM Overrides methods + #################################################### + + @api.multi + def name_get(self): + res = [] + for leave in self: + 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))) + return res + + @api.multi + def add_follower(self, employee_id): + employee = self.env['hr.employee'].browse(employee_id) + if employee.user_id: + self.message_subscribe_users(user_ids=employee.user_id.ids) + + @api.model + def create(self, values): + """ Override to avoid automatic logging of creation """ + 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}) + holiday = super(HolidaysAllocation, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(values) + holiday.add_follower(employee_id) + return holiday + + @api.multi + def write(self, values): + employee_id = values.get('employee_id', False) + result = super(HolidaysAllocation, self).write(values) + self.add_follower(employee_id) + return result + + @api.multi + def unlink(self): + for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']): + raise UserError(_('You cannot delete a leave which is in %s state.') % (holiday.state,)) + return super(HolidaysAllocation, self).unlink() + + @api.multi + def copy_data(self, default=None): + raise UserError(_('A leave cannot be duplicated.')) + + #################################################### + # Business methods + #################################################### + + @api.multi + def _prepare_holiday_values(self, employee): + self.ensure_one() + values = { + 'name': self.name, + 'holiday_type': 'employee', + 'holiday_status_id': self.holiday_status_id.id, + 'notes': self.notes, + 'number_of_days_temp': self.number_of_days_temp, + 'parent_id': self.id, + 'employee_id': employee.id + } + return values + + @api.multi + def action_draft(self): + for holiday in self: + if not holiday.can_reset: + raise UserError(_('Only an HR Manager or the concerned employee can reset to draft.')) + if holiday.state not in ['confirm', 'refuse']: + raise UserError(_('Leave request state must be "Refused" or "To Approve" in order to reset to Draft.')) + holiday.write({ + 'state': 'draft', + 'first_approver_id': False, + 'second_approver_id': False, + }) + linked_requests = holiday.mapped('linked_request_ids') + for linked_request in linked_requests: + linked_request.action_draft() + linked_requests.unlink() + return True + + @api.multi + def action_confirm(self): + if self.filtered(lambda holiday: holiday.state != 'draft'): + raise UserError(_('Leave request must be in Draft state ("To Submit") in order to confirm it.')) + return self.write({'state': 'confirm'}) + + @api.multi + def _check_security_action_approve(self): + if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): + raise UserError(_('Only an HR Officer or Manager can approve leave requests.')) + + @api.multi + def action_approve(self): + # if double_validation: this method is the first approval approval + # if not double_validation: this method calls action_validate() below + self._check_security_action_approve() + + current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + if any(holiday.state != 'confirm' for holiday in self): + raise UserError(_('Leave request must be confirmed ("To Approve") in order to approve it.')) + + self.filtered(lambda hol: hol.double_validation).write({'state': 'validate1', 'first_approver_id': current_employee.id}) + self.filtered(lambda hol: not hol.double_validation).action_validate() + return True + + @api.multi + def _check_security_action_validate(self): + if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): + raise UserError(_('Only an HR Officer or Manager can approve leave requests.')) + + @api.multi + def action_validate(self): + self._check_security_action_validate() + + current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + for holiday in self: + if holiday.state not in ['confirm', 'validate1']: + raise UserError(_('Leave request must be confirmed in order to approve it.')) + if holiday.state == 'validate1' and not holiday.env.user.has_group('hr_holidays.group_hr_holidays_manager'): + raise UserError(_('Only an HR Manager can apply the second approval on leave requests.')) + + holiday.write({'state': 'validate'}) + if holiday.double_validation: + 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].double_validation: + leaves.action_validate() + return True + + @api.multi + def action_refuse(self): + self._check_security_action_refuse() + + current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + for holiday in self: + if holiday.state not in ['confirm', 'validate', 'validate1']: + raise UserError(_('Leave request must be confirmed or validated in order to refuse it.')) + + if holiday.state == 'validate1': + holiday.write({'state': 'refuse', 'first_approver_id': current_employee.id}) + else: + holiday.write({'state': 'refuse', 'second_approver_id': current_employee.id}) + # If a category that created several holidays, cancel all related + holiday.linked_request_ids.action_refuse() + return True + + @api.multi + def _check_security_action_refuse(self): + if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): + raise UserError(_('Only an HR Officer or Manager can refuse leave requests.')) + + #################################################### + # Messaging methods + #################################################### + + @api.multi + def _track_subtype(self, init_values): + if 'state' in init_values and self.state == 'validate': + return 'hr_holidays.mt_leave_allocation_approved' + elif 'state' in init_values and self.state == 'validate1': + return 'hr_holidays.mt_leave_allocation_first_validated' + elif 'state' in init_values and self.state == 'confirm': + return 'hr_holidays.mt_leave_allocation_confirmed' + elif 'state' in init_values and self.state == 'refuse': + return 'hr_holidays.mt_leave_allocation_refused' + return super(HolidaysAllocation, self)._track_subtype(init_values) + + @api.multi + def _notification_recipients(self, message, groups): + """ Handle HR users and officers recipients that can validate or refuse holidays + directly from email. """ + groups = super(HolidaysAllocation, self)._notification_recipients(message, groups) + + self.ensure_one() + hr_actions = [] + if self.state == 'confirm': + app_action = self._notification_link_helper('controller', controller='/allocation/validate') + hr_actions += [{'url': app_action, 'title': _('Approve')}] + if self.state in ['confirm', 'validate', 'validate1']: + ref_action = self._notification_link_helper('controller', controller='/allocation/refuse') + hr_actions += [{'url': ref_action, 'title': _('Refuse')}] + + new_group = ( + 'group_hr_holidays_user', lambda partner: bool(partner.user_ids) and any(user.has_group('hr_holidays.group_hr_holidays_user') for user in partner.user_ids), { + 'actions': hr_actions, + }) + + return [new_group] + groups + + @api.multi + def _message_notification_recipients(self, message, recipients): + result = super(HolidaysAllocation, self)._message_notification_recipients(message, recipients) + title = _("See Allocation") + for res in result: + if result[res].get('button_access'): + result[res]['button_access']['title'] = title + return result diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py new file mode 100644 index 00000000000..f916000b10d --- /dev/null +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com) + +import logging + +from odoo import api, fields, models +from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) + + +class HolidaysType(models.Model): + _name = "hr.leave.type" + _description = "Leave Type" + + name = fields.Char('Leave Type', required=True, translate=True) + 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'), + ('blue', 'Blue'), + ('lightgreen', 'Light Green'), + ('lightblue', 'Light Blue'), + ('lightyellow', 'Light Yellow'), + ('magenta', 'Magenta'), + ('lightcyan', 'Light Cyan'), + ('black', 'Black'), + ('lightpink', 'Light Pink'), + ('brown', 'Brown'), + ('violet', 'Violet'), + ('lightcoral', 'Light Coral'), + ('lightsalmon', 'Light Salmon'), + ('lavender', 'Lavender'), + ('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('Allow to Override Limit', + help='If you select this check box, the system allows the employees to take more leaves ' + 'than the available ones for this type and will not take them into account for the ' + '"Remaining Legal Leaves" defined on the employee form.') + 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.") + + 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 negative value.') + 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', + help='Maximum Leaves Allowed - Leaves Already Taken - Leaves Waiting Approval') + + double_validation = fields.Boolean(string='Apply Double Validation', + help="When selected, the Allocation/Leave Requests for this type require a second validation to be approved.") + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id) + + @api.multi + def get_days(self, employee_id): + # need to use `dict` constructor to create a dict per id + result = dict((id, dict(max_leaves=0, leaves_taken=0, remaining_leaves=0, virtual_remaining_leaves=0)) for id in self.ids) + + requests = self.env['hr.leave'].search([ + ('employee_id', '=', employee_id), + ('state', 'in', ['confirm', 'validate1', 'validate']), + ('holiday_status_id', 'in', self.ids) + ]) + + allocations = self.env['hr.leave.allocation'].search([ + ('employee_id', '=', employee_id), + ('state', 'in', ['confirm', 'validate1', 'validate']), + ('holiday_status_id', 'in', self.ids) + ]) + + for request in requests: + status_dict = result[request.holiday_status_id.id] + status_dict['virtual_remaining_leaves'] -= request.number_of_days_temp + if request.state == 'validate': + status_dict['leaves_taken'] += request.number_of_days_temp + status_dict['remaining_leaves'] -= request.number_of_days_temp + + for allocation in allocations: + status_dict = result[allocation.holiday_status_id.id] + if allocation.state == 'validate': + # 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 + + return result + + @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 + + if employee_id: + data_days = self.get_days(employee_id) + + for holiday_status in self: + result = data_days.get(holiday_status.id, {}) + holiday_status.max_leaves = result.get('max_leaves', 0) + holiday_status.leaves_taken = result.get('leaves_taken', 0) + holiday_status.remaining_leaves = result.get('remaining_leaves', 0) + holiday_status.virtual_remaining_leaves = result.get('virtual_remaining_leaves', 0) + + @api.multi + def name_get(self): + if not self._context.get('employee_id'): + # leave counts is based on employee_id, would be inaccurate if not based on correct employee + return super(HolidaysType, self).name_get() + res = [] + for record in self: + name = record.name + 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) + } + res.append((record.id, name)) + return res + + @api.model + def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + """ Override _search to order the results, according to some employee. + The order is the following + + - limit (limited leaves first, such as Legal Leaves) + - virtual remaining leaves (higher the better, so using reverse on sorted) + + This override is necessary because those fields are not stored and depends + on an employee_id given in context. This sort will be done when there + is an employee_id in context and that no other order has been given + to the method. + """ + 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'): + leaves = self.browse(leave_ids) + sort_key = lambda l: (not l.limit, l.virtual_remaining_leaves) + return leaves.sorted(key=sort_key, reverse=True).ids + return leave_ids diff --git a/addons/hr_holidays/report/__init__.py b/addons/hr_holidays/report/__init__.py index 69b717e618b..4dcb9db9075 100644 --- a/addons/hr_holidays/report/__init__.py +++ b/addons/hr_holidays/report/__init__.py @@ -2,4 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import holidays_summary_report -from . import report_all_leaves +from . import hr_leave_report diff --git a/addons/hr_holidays/report/holidays_summary_report.py b/addons/hr_holidays/report/holidays_summary_report.py index 1520bffb99f..1532f49e0f3 100644 --- a/addons/hr_holidays/report/holidays_summary_report.py +++ b/addons/hr_holidays/report/holidays_summary_report.py @@ -112,7 +112,6 @@ class HrHolidaySummaryReport(models.AbstractModel): raise UserError(_("Form content is missing, this report cannot be printed.")) holidays_report = self.env['ir.actions.report']._get_report_from_name('hr_holidays.report_holidayssummary') - # TODO: See if that is the correct type holidays = self.env['hr.leave'].browse(self.ids) return { 'doc_ids': self.ids, diff --git a/addons/hr_holidays/report/hr_holidays_views.xml b/addons/hr_holidays/report/hr_holidays_views.xml deleted file mode 100644 index d12f7e2bfba..00000000000 --- a/addons/hr_holidays/report/hr_holidays_views.xml +++ /dev/null @@ -1,215 +0,0 @@ - - - - hr.holidays.report_pivot - hr.leave.report - 20 - - - - - - - - - - - hr.holidays.report_graph - hr.leave.report - 20 - - - - - - - - - - hr.holidays.report.calendar - hr.leave.report - - - - - - - - - Leaves Analysis - hr.leave.report - form - graph,pivot,calendar,form - - {'search_default_leaves':1} - - - - - - pivot - - - - - - - graph - - - - - - - - hr.holidays.filter - hr.leave.report - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - report.hr.holidays.report.leave_all.tree - hr.leave.report - - - - - - - - - - - - - - - report.hr.holidays.report.leave_all.kanban - hr.leave.report - - - - - - - - - -
-
- - - - -
- - - - -
-
- -
-
-
-
-
-
- - - - - - - - - -
from
to
-
-
- days -
-
-
-
-
-
-
-
- - - Leaves Summary - hr.leave.report - hr.employee - form - tree,kanban - - { - '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', - ] - } - -

- Click here to create a new leave request. -

-
- [('holiday_type','=','employee')] - -
- - - -
diff --git a/addons/hr_holidays/report/report_all_leaves.py b/addons/hr_holidays/report/hr_leave_report.py similarity index 100% rename from addons/hr_holidays/report/report_all_leaves.py rename to addons/hr_holidays/report/hr_leave_report.py diff --git a/addons/hr_holidays/report/hr_leave_reports.xml b/addons/hr_holidays/report/hr_leave_reports.xml new file mode 100644 index 00000000000..8439ad12e7a --- /dev/null +++ b/addons/hr_holidays/report/hr_leave_reports.xml @@ -0,0 +1,164 @@ + + + + + hr.holidays.filter + hr.leave.report + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + report.hr.holidays.report.leave_all.tree + hr.leave.report + + + + + + + + + + + + + + + report.hr.holidays.report.leave_all.kanban + hr.leave.report + + + + + + + + + +
+
+ + + + +
+ + + + +
+
+ +
+
+
+
+
+
+ + + + + + + + + +
from
to
+
+
+ days +
+
+
+
+
+
+
+
+ + + Leaves Summary + hr.leave.report + hr.employee + form + tree,kanban + + { + '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', + ] + } + +

+ Click here to create a new leave request. +

+
+ [('holiday_type','=','employee')] + +
+ + + Leaves + ir.actions.act_window + hr.leave.report + hr.employee + form + tree,form + {'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')] + + + + + +
diff --git a/addons/hr_holidays/views/hr_holidays_views.xml b/addons/hr_holidays/views/hr_holidays_views.xml deleted file mode 100644 index 6a6f22ef263..00000000000 --- a/addons/hr_holidays/views/hr_holidays_views.xml +++ /dev/null @@ -1,815 +0,0 @@ - - - - - - hr.holidays.graph - hr.leave - - - - - - - - - - - - hr.holidays.filter_allocations - hr.leave.allocation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - hr.holidays.filter - hr.leave - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Leave Request - hr.leave - - - - - - - - - Allocation Request - hr.leave.allocation - -
- -
-
- -
-

-
- - - - - - - - - - - - - -
-
- - -
- -
-
- - - Leave Request - hr.leave - -
- -
-
- -
-

-
- - - - - - - - - - - - -
- - -
-
-
-
- - -
- -
-
- - - hr.leave.allocation.tree - hr.leave.allocation - - - - - - - - - - - - - - - hr.holidays.my.allocation.tree - hr.leave.allocation - - - - - - - - - - - - - - hr.leave.allocation.tree.customize - hr.leave.allocation - - - - - - - - - - - - - - hr.holidays.report_tree - hr.leave.allocation - 20 - - - - - - - - - - - - - - - hr.holidays.kanban - hr.leave.allocation - - - - - - - -
-
- - - - -
- - - - -
-
- -
-
-
-
-
-
-
-
- days -
-
-
-
-
-
-
-
- - - hr.holidays.tree - hr.leave - - - - - - - - - - - - - - - - - - - - - - All Leaves - hr.leave - form - calendar - {'search_default_year': 1, 'search_default_group_employee': 1} - [('holiday_type','=','employee'), ('state', '!=', 'refuse')] - - - - - - - - - - - - - - - Leaves Request - hr.leave - form - - [('user_id', '=', uid)] - - -

- Click to create a new leave request. -

- Once you have recorded your leave request, it will be sent - to a manager for validation. Be sure to set the right leave - type (recuperation, legal leaves, sickness) and the exact - number of open days related to your leave. -

-
-
- - - - tree - - - - - - - form - - - - - - - calendar - - - - - - - - Requests to Approve - hr.leave - form - { - 'search_default_approve':1, - } - - - - - - - tree - - - - - - - form - - - - - - Allocation Request - hr.leave.allocation - form - tree,kanban,form - -

- Click here to create a new leave allocation request. -

-
- [('employee_id.user_id', '=', uid)] - - -
- - - - tree - - - - - - - form - - - - - - - - Allocation Request - hr.leave.allocation - form - { - 'search_default_approve':1, - } - - - - - - - tree - - - - - - - form - - - - - - Leaves Overview - hr.leave - form - tree,kanban,form,calendar - - -

- Click here to create a new leave request. -

-
- -
- - - Department Leaves - hr.leave - form - tree,form,calendar - {'search_default_department':1, 'search_default_approve':1} - -

- Click here to create a new leave request. -

-
- -
- - - - - Department Leaves Allocation - hr.leave.allocation - form - tree,form - {'search_default_department':1, 'search_default_approve':1} - -

- Click here to create a new leave allocation. -

-
- -
- - - - - - - - hr.leave.type.filter - hr.leave.type - - - - - - - - - - hr.leave.type.form - hr.leave.type - -
- -
- -
- - - - - - - - - - - - -
-
-
-
- - - hr.leave.type.tree - hr.leave.type - - - - - - - - - - - - hr.leave.type.kanban - hr.leave.type - - - - -
-
- -
-
- Max Leaves: - Leaves Taken: -
-
-
-
-
-
-
- - - hr.leave.type.normal.tree - hr.leave.type - 35 - - - - - - - - - - hr.leave.type.search - hr.leave.type - - - - - - - - - - Leave Types - ir.actions.act_window - hr.leave.type - form - tree,kanban,form - - - - - - - - - Leaves - ir.actions.act_window - hr.leave.report - hr.employee - form - tree,form - {'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')] - - - - - Leaves to be reported in Payslip - ir.actions.act_window - hr.leave - hr.employee - form - tree,form - {'search_default_employee_id': [active_id], 'search_default_validated': True, 'search_default_gray': True} - [('date_from','>=', context_today().strftime("%Y-%m-1")), ('date_from','<', ((context_today() + relativedelta(months=1)).strftime('%Y-%m-1')) )] - - - - - - hr.employee.leave.tree - hr.employee - - - - - - - - - - - Allocate Leaves for Employees - ir.actions.act_window - hr.employee - form - tree,form - - -

- You can assign remaining Legal Leaves for each employee, Odoo - will automatically create and validate allocation requests. -

-
-
- - - - Leave Request to Approve - hr.leave - form - tree,form - { - 'search_default_department_id': [active_id], - 'default_department_id': active_id, - 'search_default_approve':1} - - - - - - - Allocation Requests to Approve - hr.leave.allocation - form - tree,form - { - 'search_default_department_id': [active_id], - 'default_department_id': active_id, - 'search_default_approve':1} - - - - - - Absent Employees - hr.employee - form - kanban,tree,form - { - 'search_default_is_absent_totay': 1, - 'search_default_department_id': [active_id], - 'default_department_id': active_id} - - - - - - Leaves Analysis - hr.leave - form - graph,pivot - [('holiday_type','=','employee')] - - - - - Leaves Analysis - hr.leave - form - graph,pivot - [('holiday_type','=','employee')] - { - 'search_default_department_id': [active_id], - 'default_department_id': active_id} - - - - -
diff --git a/addons/hr_holidays/views/hr_leave_allocation_views.xml b/addons/hr_holidays/views/hr_leave_allocation_views.xml new file mode 100644 index 00000000000..606c0328136 --- /dev/null +++ b/addons/hr_holidays/views/hr_leave_allocation_views.xml @@ -0,0 +1,215 @@ + + + + + hr.holidays.filter_allocations + hr.leave.allocation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Allocation Request + hr.leave.allocation + +
+ +
+
+ +
+

+
+ + + + + + + + + + + + + +
+
+ + +
+ +
+
+ + + hr.leave.allocation.tree + hr.leave.allocation + + + + + + + + + + + + + + + hr.holidays.my.allocation.tree + hr.leave.allocation + + + + + + + + + + + + + + hr.leave.allocation.tree.customize + hr.leave.allocation + + + + + + + + + + + + + + Allocation Request + hr.leave.allocation + form + tree,kanban,form + +

+ Click here to create a new leave allocation request. +

+
+ [('employee_id.user_id', '=', uid)] + + +
+ + + Allocation Request + hr.leave.allocation + form + { + 'search_default_approve':1, + } + + + + + + Department Leaves Allocation + hr.leave.allocation + form + tree,form + {'search_default_department':1, 'search_default_approve':1} + +

+ Click here to create a new leave allocation. +

+
+ +
+ + + Allocation Requests to Approve + hr.leave.allocation + form + tree,form + { + 'search_default_department_id': [active_id], + 'default_department_id': active_id, + 'search_default_approve':1} + + + + + + + tree + + + + + + + form + + + + + + + tree + + + + + + + form + + + + +
diff --git a/addons/hr_holidays/views/hr_leave_type_views.xml b/addons/hr_holidays/views/hr_leave_type_views.xml new file mode 100644 index 00000000000..da4e973f70a --- /dev/null +++ b/addons/hr_holidays/views/hr_leave_type_views.xml @@ -0,0 +1,113 @@ + + + + + + hr.leave.type.filter + hr.leave.type + + + + + + + + + + hr.leave.type.form + hr.leave.type + +
+ +
+ +
+ + + + + + + + + + + + +
+
+
+
+ + + hr.leave.type.tree + hr.leave.type + + + + + + + + + + + + hr.leave.type.kanban + hr.leave.type + + + + +
+
+ +
+
+ Max Leaves: + Leaves Taken: +
+
+
+
+
+
+
+ + + hr.leave.type.normal.tree + hr.leave.type + 35 + + + + + + + + + + hr.leave.type.search + hr.leave.type + + + + + + + + + + Leave Types + ir.actions.act_window + hr.leave.type + form + tree,kanban,form + + + + +
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml new file mode 100644 index 00000000000..e11c257b9d7 --- /dev/null +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -0,0 +1,437 @@ + + + + + + hr.holidays.graph + hr.leave + + + + + + + + + + + + hr.holidays.filter + hr.leave + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leave Request + hr.leave + + + + + + + + + Leave Request + hr.leave + +
+ +
+
+ +
+

+
+ + + + + + + + + + + + +
+ + +
+
+
+
+ + +
+ +
+
+ + + hr.holidays.tree + hr.leave + + + + + + + + + + + + + + + + + + + + + + All Leaves + hr.leave + form + calendar + {'search_default_year': 1, 'search_default_group_employee': 1} + [('holiday_type','=','employee'), ('state', '!=', 'refuse')] + + + + + + + + + + + + + + + Leaves Request + hr.leave + form + + [('user_id', '=', uid)] + + +

+ Click to create a new leave request. +

+ Once you have recorded your leave request, it will be sent + to a manager for validation. Be sure to set the right leave + type (recuperation, legal leaves, sickness) and the exact + number of open days related to your leave. +

+
+
+ + + + tree + + + + + + + form + + + + + + + calendar + + + + + + + + Requests to Approve + hr.leave + form + { + 'search_default_approve':1, + } + + + + + + + tree + + + + + + + form + + + + + + Leaves Overview + hr.leave + form + tree,kanban,form,calendar + + +

+ Click here to create a new leave request. +

+
+ +
+ + + Department Leaves + hr.leave + form + tree,form,calendar + {'search_default_department':1, 'search_default_approve':1} + +

+ Click here to create a new leave request. +

+
+ +
+ + + + + + + + + + + + Leaves to be reported in Payslip + ir.actions.act_window + hr.leave + hr.employee + form + tree,form + {'search_default_employee_id': [active_id], 'search_default_validated': True, 'search_default_gray': True} + [('date_from','>=', context_today().strftime("%Y-%m-1")), ('date_from','<', ((context_today() + relativedelta(months=1)).strftime('%Y-%m-1')) )] + + + + + + Leave Request to Approve + hr.leave + form + tree,form + { + 'search_default_department_id': [active_id], + 'default_department_id': active_id, + 'search_default_approve':1} + + + + + + + Leaves Analysis + hr.leave + form + graph,pivot + [('holiday_type','=','employee')] + + + + + Leaves Analysis + hr.leave + form + graph,pivot + [('holiday_type','=','employee')] + { + 'search_default_department_id': [active_id], + 'default_department_id': active_id} + + + + + + hr.holidays.report_pivot + hr.leave + 20 + + + + + + + + + + + hr.holidays.report_graph + hr.leave + 20 + + + + + + + + + + Leaves Analysis + hr.leave + form + graph,pivot,calendar,form + + {'search_default_leaves':1} + + + + + + pivot + + + + + + + graph + + + + + + + + +
diff --git a/addons/hr_holidays/views/hr_views.xml b/addons/hr_holidays/views/hr_views.xml index 90b54965535..159681154d3 100644 --- a/addons/hr_holidays/views/hr_views.xml +++ b/addons/hr_holidays/views/hr_views.xml @@ -1,5 +1,59 @@ + + + Leaves + ir.actions.act_window + hr.leave.report + hr.employee + form + tree,form + {'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')] + + + + + hr.employee.leave.tree + hr.employee + + + + + + + + + + + Allocate Leaves for Employees + ir.actions.act_window + hr.employee + form + tree,form + + +

+ You can assign remaining Legal Leaves for each employee, Odoo + will automatically create and validate allocation requests. +

+
+
+ + + Absent Employees + hr.employee + form + kanban,tree,form + { + 'search_default_is_absent_totay': 1, + 'search_default_department_id': [active_id], + 'default_department_id': active_id} + + + + hr.department.kanban.inherit